feat(ci): add auto-generated CHANGELOG.md synced from releases (#4872)#4881
Conversation
Add a Keep a Changelog CHANGELOG.md that is fully derived from the project's GitHub Releases, so users no longer have to dig through commit history to see what changed between versions. - scripts/generate-changelog.js: fetch releases via the gh CLI, keep only stable vX.Y.Z tags (nightly/preview omitted), and re-group each release's auto-generated "What's Changed" list into Added/Changed/Fixed/Performance/ Documentation/Other sections by the conventional-commit prefix every PR title uses. Zero runtime deps (node builtins + gh). - release.yml: regenerate and commit CHANGELOG.md on stable releases; the commit rides the existing release-branch PR into main. - package.json: add 'npm run changelog'. - .prettierignore: skip the generated file. - Seed CHANGELOG.md from the current stable release history. Closes #4872
|
Thanks for the PR, @LaZzyMan! Template has all the substance — what, how, why, and verification are covered. Missing a few template headings verbatim (Reviewer Test Plan table, Risk & Scope, 中文说明), but nothing material is absent. Passing. On direction: straightforward win. The project has 81 stable releases and no consolidated changelog — you have to dig through individual release pages to see what changed. A single On approach: scope is tight and well-justified. A ~290-line generator script, 20 unit tests, a 22-line workflow step, and the seeded output file. Design decisions are sound — stable-only avoids nightly noise, conventional-commit grouping is a natural fit since the project already uses those prefixes, and deriving everything from the Releases API keeps regeneration idempotent. Nothing to cut here. Moving on to code review and testing. 🔍 中文说明感谢贡献! 模板的核心内容都有——做什么、怎么做、为什么、怎么验证。缺少几个模板标准标题(Reviewer Test Plan 表格、Risk & Scope、中文说明),但实质内容没有缺失。通过。 方向:明确的价值。项目有 81 个稳定版本但没有统一的变更日志——只能翻 release 页面才能看到变化。在仓库根目录放一个 方案:范围紧凑且合理。~290 行生成脚本、20 个单元测试、22 行 workflow 步骤、以及初始化的输出文件。设计决策合理——只保留稳定版本避免 nightly 噪音,conventional-commit 分组与项目已有的提交前缀天然匹配,从 Releases API 派生一切保证幂等性。没有需要砍掉的部分。 进入代码审查和测试 🔍 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal: I'd write a Node.js script that calls The PR's approach matches this almost exactly — and exceeds it in polish. The script is clean: pure functions, zero runtime deps (reuses existing No blockers found:
TestingBefore (main branch)After (this PR)Unit TestsThe 1 failed file is Verdict: all 20 new tests pass, generator produces correct output. 中文说明代码审查独立方案:写一个 Node.js 脚本调用 PR 的方案与此完全一致,且在细节上更精致。脚本干净:纯函数、零运行时依赖(复用已有的 无阻塞问题:
测试
结论:所有 20 个新测试通过,生成器输出正确。 — Qwen Code · qwen3.7-max |
|
Stepping back to look at the whole picture: this is a clean, well-executed infrastructure PR. The motivation is genuine — 81 releases with no consolidated changelog is a real gap. The implementation matches what I'd propose independently, and actually exceeds it in polish (the noise filtering for release bot bumps, the fallback for old release formats, the prettier-ignore for the generated file). The code is straightforward — ~290 lines of pure functions with clear names, no unnecessary abstractions, no over-engineering. Tests are thorough without being excessive. The workflow integration is minimal and correctly positioned. After seeing it run, the output matches what the PR promised: proper Keep a Changelog format, stable-only, newest-first, conventional-commit grouping works correctly. If I had to maintain this in six months, I'd thank the author — it's self-documenting, idempotent, and the test suite catches regressions in the parsing logic. Approving. ✅ 中文说明整体评估:这是一个干净、执行良好的基础设施 PR。动机真实——81 个版本没有统一变更日志是一个实际的缺口。实现与我独立提出的方案一致,且在细节上更精致(过滤发布机器人的噪音条目、旧格式 release 的回退处理、生成文件的 prettier-ignore)。 代码简洁——~290 行纯函数,命名清晰,无不必要的抽象,无过度工程。测试充分但不过度。Workflow 集成最小化且位置正确。 运行后输出与 PR 承诺一致:正确的 Keep a Changelog 格式、仅稳定版、最新优先、conventional-commit 分组正常工作。 如果六个月后需要维护这个,我会感谢作者——自文档化、幂等、测试套件能捕获解析逻辑的回归。 批准 ✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Add continue-on-error to the CHANGELOG step. Its only realistic failures (the gh API read and the git push) are transient, and the generator rebuilds from the full release history each run, so a skipped update self-heals on the next stable release. This keeps a changelog hiccup from blocking the version-bump PR to main that follows.
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
|
One small suggestion: could we make new
|
|
@zzhenyao Thanks — and good eye. The generator already passes flag/env names straight through whenever they're in the PR title: e.g. Going further — guaranteeing every new flag/env var is called out the way Claude Code does — would mean either parsing PR diffs to extract the tokens (an unreliable new subsystem) or hand-curating entries. The latter breaks the property that makes this safe to run on every release: the file is rebuilt from the full release history each time, fully automated and idempotent. Claude Code gets that polish because its changelog is written by hand. So the reliable lever here is upstream: a PR that adds a flag and names it in its title ( |
Got it, makes sense. Maybe a lightweight compromise: keep the generator fully automatic, but add a small PR convention/template for new flags/env vars. For example, the PR body could include a standardized line like: Then the generator just passes that through, and the “hand‑crafted changelog” work is effectively shifted to PR authors instead of the release step. |
wenshao
left a comment
There was a problem hiding this comment.
Re-review with qwen3.7-max: all findings from this round overlap with the 8 existing inline comments from the prior review. No new issues to report. The implementation is solid — pure-function design, good test coverage (20/20), clean workflow integration with self-healing semantics. The open Critical findings (command injection in execSync, empty API response guard) from the prior round remain valid concerns to address.
— qwen3.7-max via Qwen Code /review
Address review findings on the changelog generator: - Command injection: fetchReleasesJsonl built a shell string with the repo interpolated; switch to execFileSync (no shell) plus an owner/name format check so --repo can never be a shell payload. - Empty-response clobber: if the API returns zero stable releases (rate limit, auth, 5xx), refuse to overwrite CHANGELOG.md and exit 1 instead of committing a header-only stub. Paired with set -euo pipefail in the workflow so the failure is visible and the non-blocking step self-heals next release. - Arg parsing: switch getArgs() to parseArgs() (already imported) so a --dryrun typo errors instead of silently overwriting, and -h works as documented. - Breaking changes: capture the conventional-commit ! marker and prefix the entry with **BREAKING** (the repo uses feat()!:/refactor()!: in practice). - Bot authors: ENTRY_RE now accepts a trailing [bot] so GitHub App authors (e.g. @dependabot[bot]) are not dropped from release notes. Regenerates CHANGELOG.md (two entries now flagged **BREAKING**).
|
@zzhenyao Agreed that's the right shape — a PR-title/template convention is the clean way to get there, and the generator already surfaces whatever the title contains, so no generator change is needed for it. That's a contribution-guideline change rather than something for this PR, so I'll keep this one scoped to the generator itself. |
Review round addressed in 942a8d0Thanks @wenshao / the Fixed
Declined (out of scope / not worth it this round)
|
Quality-only cleanup (output is byte-identical; 23 tests green): - Single source of truth for sections: derive TYPE_TO_SECTION and SECTION_ORDER from one SECTIONS list so they can't drift. - Parse each entry once: formatRelease now reuses the categorize() result for the noise check, section lookup, and formatEntry (was parsed up to 3x). - Drop the redundant sortKey field; sort directly from version. - Collapse the duplicated double-.map() setup in the selectStableReleases test.
| * lack the " by @… in …/pull/N" tail) are skipped. The author group allows a | ||
| * trailing `[bot]` so GitHub App authors (e.g. `@dependabot[bot]`) still match. | ||
| */ | ||
| const ENTRY_RE = |
There was a problem hiding this comment.
[Suggestion] ENTRY_RE is tightly coupled to GitHub's auto-generated release notes format. If GitHub changes the format (different bullet marker, different phrasing), parseReleaseEntries silently returns empty arrays for all releases. selectStableReleases still finds releases (the zero-releases guard won't trigger), formatRelease falls back to bare "See GitHub release for details" links, and the script exits 0 and overwrites CHANGELOG.md — old content is lost.
Consider adding a sanity check: after parsing, compare entries.length against the count of * / - bullet lines in the raw body. Log a warning to stderr if there's a significant mismatch.
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| * are noise in a user-facing changelog, so they are dropped. Takes a parsed | ||
| * `categorize()` result so callers that already parsed the title don't re-parse. | ||
| */ | ||
| export function isNoiseEntry({ type, scope }) { |
There was a problem hiding this comment.
[Suggestion] isNoiseEntry checks scope === 'release' without lowercasing. categorize() lowercases the type but not the scope. A commit like chore(Release): v1.2.3 would bypass the noise filter and appear in the changelog as a bare version-bump line.
| export function isNoiseEntry({ type, scope }) { | |
| export function isNoiseEntry({ type, scope }) { | |
| return type === 'chore' && scope?.toLowerCase() === 'release'; | |
| } |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo)) { | ||
| throw new Error(`Invalid repository "${repo}"; expected "owner/name".`); | ||
| } | ||
| return execFileSync( |
There was a problem hiding this comment.
[Suggestion] execFileSync('gh', ...) has no timeout. If the GitHub API hangs (network partition, API latency), the process will hang until the CI job timeout (up to 6 hours in GitHub Actions), rather than failing fast and letting continue-on-error handle the graceful degradation.
| return execFileSync( | |
| return execFileSync( | |
| 'gh', | |
| [ | |
| 'api', | |
| `repos/${repo}/releases?per_page=100`, | |
| '--paginate', | |
| '--jq', | |
| '.[] | {tag: .tag_name, date: .published_at, prerelease: .prerelease, draft: .draft, url: .html_url, body: .body}', | |
| ], | |
| { encoding: 'utf-8', maxBuffer: 256 * 1024 * 1024, timeout: 120_000 }, | |
| ); |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| # main that follows. | ||
| if: |- | ||
| ${{ needs.prepare.outputs.is_dry_run == 'false' && needs.prepare.outputs.is_nightly == 'false' && needs.prepare.outputs.is_preview == 'false' }} | ||
| continue-on-error: true |
There was a problem hiding this comment.
[Suggestion] continue-on-error: true makes this step's failures completely unobservable. The comment says failures are "transient" and will "self-heal on the next stable release", but if the root cause is persistent (e.g., token permissions revoked, script runtime error after a Node.js upgrade), the changelog will silently stop updating across all future releases, CI stays green, and no alert fires.
Consider adding a failure() handler to emit a workflow annotation or log a visible warning so the team can notice persistent failures:
- if: failure()
run: echo "::warning::CHANGELOG.md generation failed; will retry on next release"— DeepSeek/deepseek-v4-pro via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅ — qwen3.7-max via Qwen Code /review
tanzhenxin
left a comment
There was a problem hiding this comment.
Review
Re-reviewed at the latest head. The hardening round landed well: both earlier gaps (bot-authored bullets dropped, -h unparsed) are fixed, and the new guards are sound — the gh invocation no longer goes through a shell, and the script refuses to write when the release fetch comes back empty. We also verified the generator end-to-end: regenerating from all 81 stable releases reproduces the committed file byte-for-byte with nothing dropped, and the workflow can't re-trigger itself — the changelog push rides the existing release→main PR with checks intact.
1. Historical version-bump entries still slip past the noise filter (severity: low · confidence: very high)
The filter drops modern chore(release): vX.Y.Z bumps, but older releases titled them differently (chore: bump version to 0.15.1, or just bump version to 0.14.2), so twenty-odd pure version bumps appear under "Other" in the shipped changelog — exactly the noise the description says is dropped. A small filter widening plus a regenerate would clean these up; fine as a follow-up.
2. Two take-or-leave nits (severity: low · confidence: high)
GitHub-style Revert "…" titles don't parse as conventional commits, so they land under "Other" and the revert mapping never fires in practice. And the verify command in the description needs -- before --dry-run — without it npm swallows the flag and the script writes the file instead of previewing.
Verdict
APPROVE — well-built, well-tested CI tooling with the high-risk axes (injection, workflow loops, clobbering) verified clean; the rest is cosmetic.
Closes #4872
What
Adds a
CHANGELOG.mdthat is generated automatically from the project's GitHub Releases and kept in sync on every stable release. Today, seeing what changed between versions means digging through commit history or release notes; this gives a single, browsable, per-version summary in the Keep a Changelog format (the same style as Claude Code's CHANGELOG).How it works (script + workflow)
scripts/generate-changelog.js— the generator. It fetches every release via theghCLI, keeps only stablevX.Y.Ztags, and re-groups each release's auto-generated "What's Changed" list into Keep a Changelog sections. Runnable locally and in CI:ghCLI (which CI already has)..github/workflows/release.yml— a new Regenerate CHANGELOG.md step runs on stable releases (gatedis_nightly == false && is_preview == false && is_dry_run == false), right after the GitHub Release is created. It regenerates the file, commits it onto the release branch, and pushes — so the change rides the existing release-branch →mainPR (auto-merge), with no new push-to-main machinery.continue-on-error: true(non-blocking): its only realistic failures are transient (theghAPI read or thegit push), and because the generator rebuilds from the full release history every run, a skipped update self-heals on the next stable release. A changelog hiccup must never block the version-bump PR tomain.CHANGELOG.mdis seeded from the current 81 stable releases (v0.0.2 → v0.17.1).Design decisions
Reuse
.github/release.ymlcategories? — It turns out.github/release.ymlhas nocategories, only askip-changelogexclude, so release notes are a flat "What's Changed" list. Instead of inventing a parallel taxonomy, the generator groups by the conventional-commit prefix every PR title already uses in this repo:featfixperfrefactor/revertdocschore(release): vX.Y.Zbot bumps are dropped as noise; everything else (incl. dep bumps and un-prefixed older titles) is kept verbatim under Other so nothing is silently lost.Stable only — nightly and preview pre-releases ship daily and would bury the signal, so they're omitted (matches Claude Code).
No
[Unreleased]section — the file is a pure function of published releases, which keeps regeneration deterministic and idempotent. Easy to add later if wanted.Generated file is prettier-ignored (
.prettierignore) sonpm run formatnever fights the generator.Example output
Releases whose body predates the "What's Changed" format (e.g. v0.0.2) fall back to a link to the GitHub release.
How to verify
The workflow change is validation-only here (can't run a real release); the YAML parses and the new step is correctly ordered before the release PR step.
Notes / open to maintainer preference
by @userif desired.## [Unreleased]section can be layered on later (would require listing merged PRs since the last tag).This change is dev/CI infrastructure only — no product code under
packages/is touched.