Skip to content

fix(cli): send runtime bundles to sdk-gen-api (FSDK-1116) - #17505

Merged
zamn merged 13 commits into
mainfrom
FSDK-1116-fern-runtime-bundle
Aug 27, 2026
Merged

fix(cli): send runtime bundles to sdk-gen-api (FSDK-1116)#17505
zamn merged 13 commits into
mainfrom
FSDK-1116-fern-runtime-bundle

Conversation

@zamn

@zamn zamn commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Why this PR exists

The first sdk-gen-api CLI integration sent Fern source specifications plus high-level generator invocation metadata, then expected sdk-gen-api to translate that metadata into SdkConfigIrV1. That is not sufficient for a wrapped Fern generator runtime: the Fern runtime listener executes a generator from its native { config, ir } input, and the IR must already be migrated to the exact schema version expected by the requested Fern generator ID and version.

This PR moves ownership of that Fern-specific preparation back to the Fern CLI. sdk-gen-api remains responsible for authentication, build state, idempotency, orchestration, artifact status, and dispatch, but it treats each runtime bundle as opaque bytes.

Behavior before and after

Scenario Before this PR After this PR
FERN_USE_SDK_GEN_API unset or false Existing Fiddle remote-generation path Unchanged Fiddle path
Local/on-prem generation Existing local runner Unchanged
Unsupported/non-first-party generator Existing Fiddle path Unchanged Fiddle path
Supported first-party SDK generator with flag enabled Protocol v1 request; sdk-gen-api attempted Fern-to-Postman IR translation Protocol v2 request with one native Fern runtime bundle per target
Multi-generator group One sdk-gen-api build with target metadata One sdk-gen-api build with shared sources and one ordered bundle per target
Artifact download/local output Existing Fern download and .fernignore handling Same existing download and .fernignore handling

There is no intended change to the SDK configuration or IR semantics supplied to the generator. The new bundle is built with Fern’s existing generator configuration and IR migration helpers, including the exact requested generator identity/version and any explicit IR-version override. When the feature flag is enabled, execution moves to the image selected by the deployed generator-version index; that runtime/environment change is the purpose of this route.

Exact data flow

  1. Fern resolves the generator invocation, SDK version, sources, audiences, output mode, and enriched IR as it does today.

  2. Eligibility is checked before selecting sdk-gen-api:

    • recognized first-party SDK generator;
    • concrete SDK version (not AUTO);
    • source archive available;
    • generator language matches the mapped language;
    • no whitelabel configuration;
    • replay and full-project generation remain unsupported.
  3. Fern resolves the generator IR schema version from FDR or the explicit override and migrates the enriched IR for the exact generator ID/version.

  4. Fern builds the native GeneratorConfig with:

    • the requested SDK output version;
    • existing custom generator configuration with CLI-only keys removed;
    • runtime paths /tmp/fern-runtime/ir.json and /fern/output;
    • publishToRegistry: false;
    • raw publication credentials omitted.
  5. Fern gzips exactly:

    {
      "config": { "...": "serialized native GeneratorConfig" },
      "ir": { "...": "generator-version-compatible Fern IR" }
    }
  6. A protocol v2 multipart request is submitted to /v1/fern/build containing:

    • one shared specs.tar.gz source archive;
    • one <targetId>.json.gz runtime bundle per target;
    • target metadata and requested output intent.
  7. sdk-gen-api validates and stores transient build state, then uploads each bundle unchanged and emits one grouped OrchestratorRequestV1.

  8. sdk-gen-core resolves the exact generator version through the deployed index and dispatches the bundle to the selected runtime image.

  9. The wrapped Fern listener downloads the opaque bundle, verifies its checksum, writes config.json/ir.json, invokes the generator, uploads the artifact, and publishes formatter completion.

  10. Fern polls per-target status and uses the existing artifact download/output handling after success.

What this changes in existing Fern SDK generation

Only feature-flagged remote SDK generation for recognized first-party generators changes route and execution environment. The generator receives the same class of native inputs it receives through the existing Fern pipeline, prepared by the same config and IR migration code rather than reconstructed by sdk-gen-api.

The following remain unchanged:

  • Default remote generation through Fiddle.
  • Local Docker and on-prem generation.
  • Generator selection, group selection, SDK version resolution, audiences, casing, custom configuration, and API overrides.
  • Existing task UI, log forwarding, exit behavior, output directory handling, and .fernignore behavior.
  • Unsupported generators continuing through Fiddle.

Once the sdk-gen-api route is selected for a recognized generator, unsupported route capabilities fail explicitly rather than silently falling back after preparation. Replay, whitelabel generation, AUTO SDK versions, and full-project generation are not enabled by this PR.

Output and publication behavior

  • Download output continues to return an artifact that Fern downloads through the existing local-output path.
  • GitHub and registry publication intent is represented in the protocol request for downstream handling.
  • The runtime bundle itself cannot publish directly: publishToRegistry is false and raw registry credentials are stripped.
  • Postman collection publication remains unsupported.
  • Runtime selection is not supplied by the CLI. The deployed generator-version index remains authoritative for image digest, runtime revision, and pool selection.

Request determinism and validation

  • Protocol version is now 2.
  • Target IDs remain deterministic within a generator group.
  • Idempotency includes normalized request metadata, source bytes, and runtime-bundle bytes, so configuration or migrated-IR changes alter the input identity.
  • Bundle order is aligned to request target order.
  • Client-side limits reject invalid work before submission:
    • maximum 64 bundles;
    • 5 MiB compressed per bundle;
    • 25 MiB compressed across bundles;
    • 25 MiB decompressed per bundle;
    • 100 MiB decompressed across bundles;
    • 25 MiB compressed/decompressed source archive;
    • 60 MiB total multipart body.
  • Bundle gzip and decompressed-size validation occurs before the request is sent.
  • The sdk-gen-api companion independently validates file names, target coverage, gzip/JSON shape, and upload limits.

Deployment and rollout

postman-eng/sdk-gen-api#569 must deploy before this route is enabled. Protocol v1 does not contain listener-compatible runtime bundles and is intentionally not accepted as a fallback.

Recommended rollout order:

  1. Deploy sdk-gen-api Change Fetcher to not use buffers #569 with the route disabled.
  2. Deploy a generator-version index containing the exact tested generator versions and immutable runtime image digests.
  3. Deploy this Fern CLI change.
  4. Enable FERN_USE_SDK_GEN_API only for the intended beta environment/cohort.
  5. Validate download output first, then each supported GitHub/publication mode.

Non-goals

  • Replacing Fiddle for all users by default.
  • Changing local or on-prem generation.
  • Adding a generator/language/version rollout allowlist in the CLI.
  • Allowing runtime containers to receive registry credentials or publish directly.
  • Implementing replay, whitelabel, AUTO versioning, or full-project generation.
  • Choosing a runtime image in the Fern CLI.

Verification

  • Remote workspace runner: 279 tests passed.
  • Local workspace runner: 180 tests passed.
  • Rust dynamic-snippets regression: 4 tests passed after syncing latest main.
  • Remote and local workspace TypeScript compilation passed.
  • CI primary test, compile, lint, Biome, boundaries, dependency validation, and ETE checks pass; the seed matrix is still completing.

Jira: https://postmanlabs.atlassian.net/browse/FSDK-1116
API PR: https://github.com/postman-eng/sdk-gen-api/pull/569


Open in Devin Review

Cutover validation

Every protocol-v2 target is validated as legacy-fern before any sdk-gen-api request. The generator compatibility validator and private cutover policy are colocated with the remote workspace runner.

Fern preserves the internal diagnostic and adds CLI remediation, including fern sdk-config migrate when sdk-config/v1 is required. Unknown generators, language mismatches, invalid versions, exact cutover behavior, and multi-target fail-closed behavior are covered without duplicating the cutover matrix.

No private npm package, registry credentials, lockfile entry, or trust exception is required.

Tracked by FSDK-1155 and FSDK-1154.

@zamn zamn changed the title Send runtime bundles to sdk-gen-api (FSDK-1116) fix(cli): send runtime bundles to sdk-gen-api (FSDK-1116) Aug 21, 2026

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Docs Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-08-26T04:13:04Z).

Fixture main PR Delta
docs 255.9s (n=5) 263.8s (35 versions) +7.9s (+3.1%)

Docs generation runs fern generate --docs --preview end-to-end against the benchmark fixture with 35 API versions (each version: markdown processing + OpenAPI-to-IR + FDR upload).
Delta is computed against the nightly baseline on main.
Baseline from nightly run(s) on main (latest: 2026-08-26T04:13:04Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-08-26 22:33 UTC

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-08-26T04:13:04Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 75s (n=5) 111s (n=5) 67s -8s (-10.7%)
go-sdk square 136s (n=5) 285s (n=5) 129s -7s (-5.1%)
java-sdk square 225s (n=5) 279s (n=5) 193s -32s (-14.2%)
php-sdk square 68s (n=5) N/A 46s -22s (-32.4%)
python-sdk square 153s (n=5) 245s (n=5) 137s -16s (-10.5%)
ruby-sdk-v2 square 95s (n=5) 129s (n=5) 87s -8s (-8.4%)
rust-sdk square 227s (n=5) 215s (n=5) 195s -32s (-14.1%)
swift-sdk square 60s (n=5) 450s (n=5) 57s -3s (-5.0%)
ts-sdk square 138s (n=5) 150s (n=5) 136s -2s (-1.4%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-08-26T04:13:04Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-08-26 22:33 UTC

@zamn
zamn changed the base branch from main to sdk-gen-client-integration August 25, 2026 15:00
zamn added 3 commits August 25, 2026 10:01
… FSDK-1116-fern-runtime-bundle

# Conflicts:
#	packages/cli/generation/remote-generation/remote-workspace-runner/package.json
#	pnpm-lock.yaml
@zamn
zamn changed the base branch from sdk-gen-client-integration to inline-sdk-gen-client August 25, 2026 20:49
@zamn
zamn changed the base branch from inline-sdk-gen-client to main August 25, 2026 22:06
@zamn

zamn commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

@nitpickybot can you look at this pr?

@nitpickybot

nitpickybot Bot commented Aug 26, 2026

Copy link
Copy Markdown

Sure — here's what stands out. Note the diff is truncated for the biggest new files (src/getGeneratorConfig.ts +664, fernSdkGenApi.ts, and the sdk-gen-client/* files), so this is based on what's visible plus the tests.

Overall

The direction makes sense: Fern owns Fern-specific prep (config + IR migration), sdk-gen-api treats the bundle as opaque bytes. That's a much cleaner boundary than having sdk-gen-api reconstruct SdkConfigIrV1.

Things worth a second look

1. Dependency direction: local → remote

packages/cli/generation/local-generation/local-workspace-runner/src/getGeneratorConfig.ts is now just:

export { getGeneratorConfig, getGithubPublishConfig, getLicensePathFromConfig }
  from "@fern-api/remote-workspace-runner";

Local generation now depends on the remote generation package to build a config for a local Docker run. That's a bit backwards, and it drags @fern-api/configuration-loader, @fern-api/github, and @fern-fern/generator-exec-sdk into remote-workspace-runner's dep list. A shared generator-config package (or moving it into @fern-api/configuration) would avoid the inversion and the risk of a cycle if remote ever needs something from local. Did the boundaries check pass only because remote doesn't import local today?

2. getGithubPublishConfig signature change

Tests now call it with a second positional arg (getGithubPublishConfig(info, true)). Since it's a boolean flag whose semantics aren't obvious at the call site, and it gates credential handling, two asks:

  • Confirm every existing caller (including the local path) gets the intended default — a silently-defaulting security-relevant boolean is easy to get wrong.
  • Consider an options object or a named param so true at a call site reads as "omit raw credentials" rather than "true".

3. Bundle ↔ target association

The PR says "Bundle order is aligned to request target order," but files are named <targetId>.json.gz. Ordering and naming are two independent contracts; if sdk-gen-api matches by filename, the ordering guarantee is redundant, and if it matches by order, the filenames are decorative. Which one is authoritative? The multi-generator test (seeds "10" and "2", submitted python-first) suggests this is deliberately exercised — worth an explicit comment in fernSdkGenApi.ts so a future refactor doesn't quietly break it.

4. No v1 fallback

protocolVersion: 2 with no fallback means the CLI is hard-coupled to sdk-gen-api #569. The rollout order in the description covers it, but since this is behind FERN_USE_SDK_GEN_API anyway, is there a plan for what a user sees if they enable the flag against an older API — a clean error, or a 400 with a stack trace?

5. Secret stripping

The test asserting gunzipSync(compressed).toString("utf8") doesn't contain "raw-publish-secret" is a nice, blunt check. The OIDC-marker test (OIDC / <USE_OIDC> preserved, real secrets → "") covers the tricky case for npm/nuget/pypi. Is maven/rubygems/crates covered too, or do those not have an OIDC path? The emptyRegistriesConfigV2 shape suggests they at least have credential fields.

Smaller notes

  • Test "passes a known generator language mismatch to canonical compatibility validation" expects true from isEligibleForFernSdkGenApi for a mismatched language, but the description lists "generator language matches the mapped language" as an eligibility check. Presumably the mismatch now fails later in the compatibility validator (fail-closed) rather than falling back to Fiddle — the test name hints at that, but the assertion alone reads confusingly. A comment would help.
  • The description mentions a fern sdk-config migrate remediation hint; I don't see that command in the changed files, so I'll assume it exists already.
  • Client-side limits (64 bundles, 5/25/100 MiB, 60 MiB body) — are these constants shared with sdk-gen-api, or duplicated? Duplicated magic numbers across two repos tend to drift right around the time someone ships a large IR.

Nothing here looks blocking; the dependency direction and the boolean flag are the two I'd want addressed before merge.

@zamn

zamn commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the concrete review items:

  • Replaced the security-relevant positional boolean with { omitPublishCredentials: true }.
  • Documented that <targetId>.json.gz is authoritative for API correlation; ordering only stabilizes batching.
  • Added credential-omission coverage for Maven signatures/credentials, RubyGems, and crates, alongside the existing npm/NuGet/PyPI OIDC coverage.
  • Clarified that eligibility only checks route prerequisites and canonical batch preflight owns mismatch diagnostics.

The local-to-remote dependency extraction remains outside this PR: remote-workspace-runner owns the shared configuration implementation today, local only re-exports it, and the repository boundaries/compile checks pass without a cycle. A dedicated shared package would be a broader migration.

Protocol v2 remains intentionally flag-gated and deploy-ordered after sdk-gen-api #569. An older API rejects the versioned request rather than silently falling back. Client limits mirror the API contract locally for early feedback; sdk-gen-api remains authoritative and revalidates every limit.

Sent by Claude on behalf of Adam

@zamn
zamn merged commit fe08e09 into main Aug 27, 2026
223 checks passed
@zamn
zamn deleted the FSDK-1116-fern-runtime-bundle branch August 27, 2026 15:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants