feat(cli): add --prerelease flag for --version AUTO generation - #17306
feat(cli): add --prerelease flag for --version AUTO generation#17306devin-ai-integration[bot] wants to merge 2 commits into
Conversation
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| context, | ||
| generatorGroup, | ||
| version, | ||
| prerelease, |
There was a problem hiding this comment.
🟡 Prerelease label is dropped for cloud generation runs
The prerelease label chosen on the command line is never handed to the per-generator worker for cloud runs (missing prerelease in the generateOne({...}) call at packages/cli/generation/remote-generation/remote-workspace-runner/src/runRemoteGenerationForAPIWorkspace.ts:153-190), so it always arrives empty downstream.
Impact: Even once the server side supports it, cloud generations will silently ignore the requested prerelease label.
Plumbing gap between the workspace-level function and the per-generator worker
runRemoteGenerationForAPIWorkspace accepts prerelease (...:34, ...:68-69) and generateOne declares it (...:222, ...:260-261) and forwards it to runRemoteGenerationForGenerator (...:350), but the object literal passed to generateOne inside the generatorGroup.generators.map(...) block never includes prerelease. Because the property is optional, TypeScript does not flag it, so prerelease is undefined for every remote generator and the value never reaches createAndStartJob's createJobV3 payload.
Prompt for agents
In packages/cli/generation/remote-generation/remote-workspace-runner/src/runRemoteGenerationForAPIWorkspace.ts, the top-level function accepts a new `prerelease` option and the internal `generateOne` helper declares and forwards it, but the call site that constructs `generateOne({...})` inside the `generatorGroup.generators.map(...)` loop omits `prerelease`. Since the field is optional, this compiles but means the flag never reaches `runRemoteGenerationForGenerator`/`createAndStartJob`. Add `prerelease` to that argument object alongside `version`.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in 2e9b591 — prerelease is now passed in the generateOne({...}) object literal. As noted in the description, the value still needs Fiddle to forward it into AutoVersionStepConfig before it changes remote behavior.
| if (parsed.prerelease[0] !== identifier) { | ||
| return `${core}-${identifier}.0`; | ||
| } |
There was a problem hiding this comment.
🟡 Version can move backwards when the prerelease label is changed
An in-progress prerelease version is rewritten with a brand-new counter of zero while keeping the same release numbers (return ${core}-${identifier}.0`` at packages/generator-cli/src/autoversion/VersionUtils.ts:249) even when the new label sorts below the old one, so the next generated version can be lower than the one already published.
Impact: Switching the label (for example from rc to beta) produces a version that publishing tools reject or that ships as an apparent downgrade.
Identifier-switch branch ignores ordering of the old and new prerelease identifiers
incrementPrerelease handles a label change by keeping the pending core and resetting the counter: 1.6.0-beta.3 + rc → 1.6.0-rc.0 (increasing, since beta < rc). The reverse direction is not guarded: 1.6.0-rc.3 + beta → 1.6.0-beta.0, which is less than 1.6.0-rc.3 under semver precedence, contradicting the documented invariant at packages/generator-cli/src/autoversion/VersionUtils.ts:229 ("Every transition is monotonically increasing under semver precedence"). A fix would compare the candidate with the current version (e.g. semver.gt) and, when not greater, either bump the core or continue the counter instead of resetting it.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in 2e9b591 — the identifier-switch branch now only keeps the pending core when semver.gt(switched, version); otherwise it falls through to re-anchoring the core (1.6.0-rc.3 + beta -> 1.6.1-beta.0 for PATCH, 1.7.0-beta.0 for MINOR, 2.0.0-beta.0 for MAJOR). Covered by a new test case.
| private initialVersion(mappedMagicVersion: string): string { | ||
| const version = mappedMagicVersion.startsWith("v") ? "v0.0.1" : "0.0.1"; | ||
| return this.prerelease != null ? applyPrereleaseIdentifier(version, this.prerelease) : version; | ||
| } |
There was a problem hiding this comment.
🟡 First version stamped on a brand new repo can lose the prerelease label
The starting version used when the previous version cannot be determined is built without the requested prerelease label (mappedMagicVersion.startsWith("v") ? "v0.0.1" : "0.0.1" at packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts:557), so that run publishes a stable version instead of staying on the prerelease line.
Impact: A run that was asked to stay on a prerelease line can stamp a plain stable version instead.
One of three initial-version sites was not migrated to the new helper
The PR introduces LocalTaskHandler.initialVersion() (...:651-654), which appends the identifier via applyPrereleaseIdentifier, and uses it at ...:274 and ...:318. The third initial-version site — the catch (error) { if (error instanceof AutoVersioningException) ... } fallback at ...:550-557 — still constructs "0.0.1" / "v0.0.1" inline, so the --prerelease identifier is dropped on that path.
Prompt for agents
packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts introduces a private `initialVersion(mappedMagicVersion)` helper that appends the `--prerelease` identifier, and uses it in the two 'new SDK repository' paths (around lines 274 and 318). The third initial-version fallback — inside the `catch` block that handles `AutoVersioningException` (around line 557) — still hardcodes `mappedMagicVersion.startsWith("v") ? "v0.0.1" : "0.0.1"`, so runs with `--prerelease` stamp a stable version there. Switch that site to the helper as well.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in 2e9b591 — the AutoVersioningException fallback site now uses initialVersion(mappedMagicVersion) too.
| if (this.prerelease != null) { | ||
| return incrementVersion(version, versionBump, { prerelease: this.prerelease }); | ||
| } |
There was a problem hiding this comment.
🟡 A malformed stored version silently resets the SDK to 0.0.1 when a prerelease label is used
When a prerelease label is requested, a version string that cannot be parsed now raises the same kind of error the surrounding code treats as "no previous version found" (incrementVersion(version, versionBump, { prerelease: this.prerelease }) at packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts:662), so generation quietly restarts numbering at 0.0.1 instead of failing.
Impact: An SDK with an unparsable recorded version is silently republished as 0.0.1, a large version downgrade, instead of surfacing an error.
Error-type change flips a hard failure into the initial-version fallback
Without --prerelease, LocalTaskHandler.incrementVersion throws a CliError for an unparsable version (...:668-669); CliError is not an AutoVersioningException, so the outer catch at ...:549-565 rethrows and the run fails loudly. With --prerelease, the call delegates to @fern-api/generator-cli's incrementVersion, which throws AutoVersioningException("Invalid semantic version format: ...") (packages/generator-cli/src/autoversion/VersionUtils.ts:184-186). All incrementVersion calls (...:350, ...:458, ...:506, ...:528) sit inside that try block, so the exception is absorbed by the AutoVersioningException branch and the run returns version 0.0.1. Previous versions can come from .fern/metadata.json or git tags, which are not guaranteed to be strict semver.
| if (this.prerelease != null) { | |
| return incrementVersion(version, versionBump, { prerelease: this.prerelease }); | |
| } | |
| if (this.prerelease != null) { | |
| try { | |
| return incrementVersion(version, versionBump, { prerelease: this.prerelease }); | |
| } catch (error) { | |
| throw new CliError({ | |
| message: `Failed to increment version: ${version} (${extractErrorMessage(error)})`, | |
| code: CliError.Code.VersionError | |
| }); | |
| } | |
| } |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in 2e9b591 — the delegated call is wrapped and rethrown as CliError with Code.VersionError, so an unparsable stored version fails loudly instead of being absorbed by the initial-version fallback.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Docs Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on
Docs generation runs |
SDK Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on Full benchmark table (click to expand)
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 |
Description
Linear ticket: Refs
fern generate --version AUTO --prerelease rckeeps an SDK on a prerelease line instead of stamping a stable version each run, so several merges can accumulate before one stable publish.Today
incrementVersiondiscards the AI-selected bump whenever the current version already has any prerelease tag (4.0.0-rc.1+ MAJOR →4.0.0-rc.2), and from a stable version it never attaches a prerelease at all. The new option makes the bump decide the core, and the identifier carry a counter:Every transition is strictly increasing under semver precedence: the counter only resets when the core moves up, and a prerelease of a higher core outranks any prerelease of a lower one. "Outranks" is decided from the pending core's shape — a pending
x.y.zwithz != 0is a patch line, so a MINOR re-anchors it;y != 0 || z != 0means a MAJOR re-anchors it. Switching identifiers keeps the pending core only when that still moves the version forward (1.6.0-beta.3+rc→1.6.0-rc.0); a switch that would go backwards re-anchors the core instead (1.6.0-rc.3+beta→1.6.1-beta.0for PATCH).NO_CHANGEstill short-circuits, and behavior with no--prereleaseis untouched (existing prerelease lines keep advancing their counter, ignoring the bump).Changes Made
VersionUtils:incrementVersion(version, bump, { prerelease })plusisValidPrereleaseIdentifierandapplyPrereleaseIdentifierhelpers. Identifiers must be alphanumeric and start with a letter — numeric-leading identifiers would be parsed by semver as counters and wouldn't round-trip.cli.ts:--prerelease <identifier>option, rejected with a config error unless--version AUTOis also passed, and rejected for malformed identifiers.generateAPIWorkspaces→generateAPIWorkspace→ both generation paths: local (runLocalGenerationForWorkspace→runGenerator→LocalTaskHandler) and remote (runRemoteGenerationForAPIWorkspace→runRemoteGenerationForGenerator→createAndStartJob).AutoVersionStepConfig.prerelease, honored byAutoVersionStepin the replay, non-replay and chunked-consolidation paths; first generation on a fresh repo stamps0.0.1-<identifier>.0.CliError, so it fails loudly instead of being absorbed byLocalTaskHandler's "new SDK repo" fallback and silently restarting at0.0.1.createJobV3payload, but Fiddle must forward it intoAutoVersionStepConfigbefore it takes effect on remote generation.--localworks end-to-end with this PR.Testing
packages/generator-cli/src/__test__/VersionUtils.test.tscovers stable→prerelease transitions per bump level, counter advancement, re-anchoring on escalation, identifier switching in both directions,vprefixes, build metadata,NO_CHANGE, unchanged no-flag behavior, and invalid identifiers/versions.pnpm turbo run test --filter @fern-api/generator-cli --filter @fern-api/local-workspace-runner --filter @fern-api/cli— 1276 tests passing.pnpm turbo run compilefor the four touched packages, pluspnpm lint:biome --fix,pnpm format:fix,pnpm check:fix.Link to Devin session: https://app.devin.ai/sessions/de0fb13aee134af3a6d24cb48e2040e8