Skip to content

fix(cli): record changelog entries for explicit --version and never wipe changelog.md#17005

Open
iamnamananand996 wants to merge 3 commits into
mainfrom
devin/1783691338-explicit-version-changelog
Open

fix(cli): record changelog entries for explicit --version and never wipe changelog.md#17005
iamnamananand996 wants to merge 3 commits into
mainfrom
devin/1783691338-explicit-version-changelog

Conversation

@iamnamananand996

@iamnamananand996 iamnamananand996 commented Jul 10, 2026

Copy link
Copy Markdown
Member

Description

Linear ticket: N/A

fern generate --group <group> --version 0.1.0 (explicit version, not AUTO) skipped the autoversioning pipeline entirely, so no changelog entry was written — and regeneration copy steps could wipe an existing changelog.md, losing version history forever.

Now:

  • Explicit versions record a version-only entry: ## [0.1.0] - <date> with an empty description, prepended to the existing changelog.
  • --version AUTO keeps its AI-generated changelog flow unchanged.
  • changelog.md is never wiped: existing entries are always preserved across regenerations.

Changes Made

  • New autoversion/changelogUtils.ts with prependChangelogBlock() (prepends a ## [version] - date block while preserving existing content and the # Changelog title) and changelogContainsVersion() (duplicate-entry guard); AutoVersionStep.prependChangelogEntry() now delegates to it.
  • LocalTaskHandler.copyGeneratedFiles():
    • captures changelog.md (case-insensitive) before copy operations and restores it if the copy removed it (generators don't emit changelog.md);
    • for explicit (non-AUTO) versions, prepends a version-only entry, skipping duplicates; a new file is only created when the output is a git repo.
  • GithubStep.ensureChangelogFile() now prepends into an existing changelog instead of only creating a missing file, and writes a version-only entry when a new version has no changelog entry (explicit-version runs); duplicate versions are skipped.
  • GitHub.push() / GitHub.pr() (generator-agent flow) restore a tracked changelog.md after overwriteLocalContents when the generated output doesn't include one, so existing entries are never deleted.
  • Changelog entries added under packages/cli/cli/changes/unreleased/ and packages/generator-cli/changes/unreleased/ (type: fix).
  • Updated README.md generator (if applicable)

Testing

  • Unit tests added/updated: changelog-utils.test.ts (prepend/duplicate/format cases) and LocalTaskHandler.explicitVersionChangelog.test.ts (preserve + prepend in git repos, no duplicates on regeneration with same version, restore of wiped changelog, no file created for plain local output)
  • Manual testing completed: pnpm turbo run test --filter @fern-api/generator-cli --filter @fern-api/local-workspace-runner — all 626 tests pass; pnpm lint:biome and pnpm format clean

Link to Devin session: https://app.devin.ai/sessions/1d8601f644ca491592d79696962aa730
Requested by: @iamnamananand996


Open in Devin Review

@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AI Review Summary

The PR preserves changelog history and records explicit-version entries across three code paths (local runner, GithubStep, GitHub push/PR). The core logic is sound and well-tested, but there are a few correctness concerns: a subtle regression in ensureChangelogFile when an existing changelog has an entry but no matching version, a potential path-matching bug in getChangelogFilesToPreserve when the source directory is nested, and prependChangelogBlock not deduplicating within its own logic (relies on callers).

  • 🟡 2 warning(s)
  • 🔵 3 suggestion(s)

Comment on lines +514 to 519
if (existing != null) {
if (version == null || changelogContainsVersion(existing, version)) {
// Already written (e.g. by AutoVersionStep) — don't duplicate the entry.
return false;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 warning

Behavior change: previously, if changelog.md already existed, ensureChangelogFile returned false unconditionally (assumed AutoVersionStep wrote it). Now, if the existing changelog doesn't contain version, it will prepend a new (possibly empty-description) block. In the normal AutoVersionStep flow, AutoVersionStep writes the entry with the version header, so changelogContainsVersion should match and skip — good. But if resolved.newVersion differs from what AutoVersionStep recorded, or timing differs, you could double-write. Confirm version here always matches the header AutoVersionStep used (both use ## [${version}]), otherwise you'll get duplicate blocks.

Comment on lines +161 to +168
const tracked = await repository.listTrackedFiles();
const trackedChangelogs = tracked.filter((file) => file.toLowerCase() === "changelog.md");
if (trackedChangelogs.length === 0) {
return [];
}
try {
const sourceFiles = await readdir(sourceDirectory);
const sourceHasChangelog = sourceFiles.some((file) => file.toLowerCase() === "changelog.md");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 warning

listTrackedFiles() likely returns repo-relative paths (possibly nested), while readdir(sourceDirectory) returns bare filenames of only the top level. The file.toLowerCase() === "changelog.md" match on tracked files only catches a changelog at the repo root, and the source check only looks at the top of sourceDirectory. If the changelog lives in a subdirectory, or the tracked path is like sub/changelog.md, neither comparison matches. Confirm the changelog is always at repo root; otherwise use path.basename and a recursive source scan.

* so callers can avoid prepending a duplicate block on regeneration.
*/
export function changelogContainsVersion(content: string, version: string): boolean {
return content.includes(`## [${version}]`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

changelogContainsVersion uses a naive includes("## [${version}]"). Version 1.0.0 will match a substring of e.g. ## [1.0.0-beta]? No — the ] guards that. But 1.0.0 would NOT match ## [11.0.0]... actually ## [11.0.0] does not contain ## [1.0.0], so fine. Just note it also matches inside code blocks or prose lines that happen to contain that string. Low risk, but a line-anchored regex (^## \[version\]) would be more robust.

if (current != null) {
return;
}
await writeFile(join(this.absolutePathToLocalOutput, RelativeFilePath.of(prior.filename)), prior.content);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

restoreChangelogFile writes back using prior.filename (original casing). Good — preserves CHANGELOG.md vs changelog.md. But note prependExplicitVersionChangelogEntry runs immediately after and re-reads via readChangelogFile (case-insensitive), so casing is retained there too. No bug, just confirming the two steps are consistent — they are.

};
return {
logger: { info: noop, debug: noop, warn: noop, error: noop }
} as LocalTaskHandler.Init["context"];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

The mock context only stubs logger. If copyGeneratedFiles touches any other context method on these paths (e.g. failAndThrow, logger.log), the cast-to-type will let it compile but throw at runtime. Tests pass now, but this stub is brittle — consider a fuller fake or documenting the assumption.

@devin-ai-integration devin-ai-integration Bot changed the title fix: record changelog entries for explicit --version and never wipe changelog.md fix(cli): record changelog entries for explicit --version and never wipe changelog.md Jul 10, 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 potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-07-10T05:18:08Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 72s (n=5) 113s (n=5) 63s -9s (-12.5%)
go-sdk square 135s (n=5) 285s (n=5) 108s -27s (-20.0%)
java-sdk square 218s (n=5) 272s (n=5) 196s -22s (-10.1%)
php-sdk square 62s (n=5) 84s (n=5) 44s -18s (-29.0%)
python-sdk square 146s (n=5) 237s (n=5) 127s -19s (-13.0%)
ruby-sdk-v2 square 92s (n=5) 124s (n=5) 68s -24s (-26.1%)
rust-sdk square 175s (n=5) 165s (n=5) 216s +41s (+23.4%)
swift-sdk square 61s (n=5) 437s (n=5) 51s -10s (-16.4%)
ts-sdk square 127s (n=5) 128s (n=5) 106s -21s (-16.5%)

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-07-10T05:18:08Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-07-10 14:59 UTC

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Docs Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-07-10T05:18:08Z).

Fixture main PR Delta
docs 241.5s (n=5) 238.1s (35 versions) -3.4s (-1.4%)

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-07-10T05:18:08Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-07-10 14:58 UTC

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