-
Notifications
You must be signed in to change notification settings - Fork 3k
feat(release): user-facing bilingual digest for release notes #9216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
97daf26
9f6645b
cf3c712
1be70fb
713bf9a
20f7d0f
3b55730
f4bf092
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,218 @@ | ||
| # User-Facing Release Notes | ||
|
|
||
| ## Problem | ||
|
|
||
| Stable release notes are a developer-facing PR list. `finalize-release.yml` | ||
| runs `scripts/generate-release-notes.js`, which buckets every merged PR into | ||
| commit-type sections (Features / Bug Fixes / Performance / Documentation / | ||
| Internal Changes) and rewrites each entry with a one-sentence model summary. | ||
| For users this reads as a wall of PRs: | ||
|
|
||
| - Entries are grouped by change _type_, not by the area a user cares about | ||
| (Web Shell, Desktop, multi-agent, model support). | ||
| - Styles mix: model sentences ("Adds standard OpenTelemetry…") sit next to | ||
| raw conventional-commit titles ("feat(serve): bound daemon ACP NDJSON | ||
| buffers") whenever a summary fell back, which reads as unedited tooling | ||
| output. | ||
| - Highlights repeat full-list entries nearly verbatim, adding length without | ||
| a second level of abstraction. | ||
| - No Chinese version, despite a large Chinese-speaking user base. | ||
| - UI changes ship without visuals even when the PR body already carries | ||
| Before/After screenshots. | ||
|
|
||
| Measured context (2026-08-15): v0.21.11 listed 49 PRs; only 2 of those PR | ||
| bodies contain images (~4%), and 3 of the last 60 merged PRs overall. Image | ||
| support is therefore best-effort decoration, never structure. | ||
|
|
||
| ## Goals | ||
|
|
||
| 1. Replace the type-bucketed PR list with a **themed digest**: model groups | ||
| changes into user-facing themes, each with a short intro and items. | ||
| 2. Add a **Chinese digest** mirroring highlights and themes (PR-level list | ||
| stays English; PR titles are English by convention). | ||
| 3. **Attach screenshots** from PR bodies to digest items when available, | ||
| degrade silently when not. | ||
| 4. Lose no information and no robustness: the full PR list remains as a | ||
| collapsed appendix, and every model failure path keeps today's output. | ||
|
|
||
| ## Non-Goals | ||
|
|
||
| - Translating the full PR list into Chinese. | ||
| - Changing nightly/preview notes (they never run the AI path). | ||
| - Sourcing images from anywhere other than the merged PR body. | ||
| - Editing the GitHub Release creation step in `release.yml` (it still | ||
| publishes GitHub-generated notes immediately; finalize rewrites later). | ||
|
|
||
| ## Pipeline Recap | ||
|
|
||
| 1. `release.yml` → `gh api …/releases/generate-notes` anchored at the | ||
| previous tag → `cap-release-notes.mjs` → `gh release create`. | ||
| 2. `finalize-release.yml` → `generate-release-notes.js` parses the | ||
| GitHub-generated bullets, fetches PR bodies/labels via GraphQL, calls the | ||
| model (summaries in batches of 8, then highlights), renders Markdown, and | ||
| `gh release edit`s it in place. Marker: `<!-- qwen-release-notes:v1 -->`. | ||
| 3. `npm run changelog` (`generate-changelog.js`) rebuilds CHANGELOG.md from | ||
| the GitHub Releases API; bodies starting with the marker are embedded | ||
| verbatim (headings demoted one level). | ||
|
|
||
| ## Proposed Changes | ||
|
|
||
| ### 1. Model content: summaries gain Chinese; new themes call | ||
|
|
||
| `scripts/generate-release-notes.js` keeps the batched summaries call and the | ||
| highlights call, and adds one **themes** call: | ||
|
|
||
| - Summaries response becomes | ||
| `{"summaries":[{"pr","summary","summaryZh"}]}`. English rules unchanged | ||
| (≤180 chars, plain text). `summaryZh` is Simplified Chinese, ≤120 chars, | ||
| technical identifiers (commands, settings, product names) stay English. | ||
| An invalid `summaryZh` falls back to the English summary for that entry | ||
| with a warning — the Chinese section never drops wholesale. | ||
| - Highlights response gains `textZh` (same limits as `summaryZh`). | ||
| - New themes call input: every entry's number, category, English and Chinese | ||
| summary. Response: | ||
|
|
||
| ```json | ||
| { | ||
| "themes": [ | ||
| { | ||
| "title": "Web Shell", | ||
| "titleZh": "Web Shell", | ||
| "intro": "…≤200 chars, optional…", | ||
| "introZh": "…", | ||
| "items": [8780, 8973] | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| Validation mirrors the existing summary/highlight guards: ≤8 themes, | ||
| title ≤40 chars, items reference known PRs, a PR appears in at most one | ||
| theme. PRs the model leaves unassigned are collected into a deterministic | ||
| catch-all theme rendered last ("Other Changes" / "其他变更"). | ||
|
|
||
| All three calls share the existing retry/backoff/deadline machinery. | ||
| The themes call scales `max_tokens` with the entry count (capped at 8192); | ||
| summaries and highlights keep the fixed 4096 budget, which leaves headroom | ||
| for every reachable summaries batch (at most 8 entries × English + Chinese). | ||
|
|
||
| ### 2. Rendering: v2 layout | ||
|
|
||
| ``` | ||
| <!-- qwen-release-notes:v2 --> | ||
|
|
||
| ## Highlights | ||
|
|
||
| ## Breaking Changes ← bilingual when present: English item plus an | ||
| indented Chinese line ("No known breaking | ||
| changes." stays English-only) | ||
|
|
||
| ## <Theme title> ← intro + items; screenshots under items | ||
| ## <Theme title> … | ||
|
|
||
| --- | ||
|
|
||
| ## 中文摘要 | ||
|
|
||
| ### 亮点 ← Chinese highlights | ||
| ### <theme titleZh> ← introZh + Chinese items | ||
|
|
||
| <details><summary>Complete Change List (N pull requests)</summary> | ||
|
|
||
| ### Features | ||
| - web-shell: improve compact tool activity ([#8973](…)) by @ytahdn | ||
| … | ||
| </details> | ||
|
|
||
| ## New Contributors | ||
| **Full Changelog**: …compare/v0.21.11...v0.21.12 | ||
| ``` | ||
|
|
||
| Decisions: | ||
|
|
||
| - **Block layout, not interleaved**: English digest on top, one `---` | ||
| divider, then `## 中文摘要`. Each audience reads one contiguous block; | ||
| GitHub's TOC and release page stay scannable. | ||
| - **Themes use `##`**, matching today's section weight; Chinese themes use | ||
| `###` under the `## 中文摘要` umbrella. | ||
| - **Appendix uses normalized raw titles**, not model summaries: strip the | ||
| `type(scope):` prefix to `scope: description` (same rule as | ||
| `generate-changelog.js` `formatEntry`), keep ` by @author` and co-author | ||
| credits. This kills the mixed-style problem deterministically and makes | ||
| the appendix independent of model availability. Category sub-headings | ||
| (Features / Bug Fixes / …) remain — the appendix is the developer view. | ||
| - **Highlights** keep the v1 shape (text + PR links); no bolding tricks, | ||
| since highlight text already names the capability. | ||
| - Author attribution stays in the appendix only; digest items show just the | ||
| text + PR link, keeping lines short. | ||
|
|
||
| ### 3. Images from PR bodies | ||
|
|
||
| Deterministic extraction, no model involvement: | ||
|
|
||
| - Sources in the PR body (already fetched by the GraphQL query): Markdown | ||
| ``, `<img src="url">`, and bare image URLs. | ||
| - Host allowlist (https only): `github.com/user-attachments/`, | ||
| `user-images.githubusercontent.com`, | ||
| `private-user-images.githubusercontent.com`, and `raw.githubusercontent.com` | ||
| pinned to a 40-hex commit-SHA ref — a branch ref stays mutable after | ||
| publication, so its owner could swap the image in a shipped release. | ||
| Anything else is ignored — the release body must never become a hotlinking | ||
| vector. The camo image proxy is deliberately not allowed even though GitHub | ||
| serves it: its HMAC signs arbitrary external URLs without repository | ||
| binding, so admitting it would re-admit every excluded host. | ||
| - First two matches per entry; first eight images per release; images render | ||
| only under digest items (never in the collapsed appendix). | ||
|
|
||
| Measured coverage is ~4% of release PRs, so the extractor must be cheap and | ||
| its absence invisible: no images → identical output to the image-less case. | ||
|
|
||
| ### 4. Fallback ladder | ||
|
|
||
| | Failure | Result | | ||
| | ------------------------------ | ------------------------------------- | | ||
| | No model config | Today's v1 render (titles only) | | ||
| | Summaries batch fails | Circuit breaker as today; titles used | | ||
| | Highlights call fails | Digest without a highlights section | | ||
| | Themes call fails | Whole note falls back to v1 render | | ||
| | One `summaryZh` invalid | That item shows English in 中文摘要 | | ||
| | A theme intro invalid | Intro dropped; theme itself kept | | ||
| | No Chinese produced anywhere | 中文摘要 block omitted entirely | | ||
| | Image extraction finds nothing | No image lines | | ||
|
|
||
| Every rung emits the existing `::warning::` annotations, so degradation is | ||
| visible in the Actions run without failing the release. | ||
|
|
||
| ### 5. CHANGELOG.md handling | ||
|
|
||
| `generate-changelog.js` accepts markers `v1` and `v2`. For v2 bodies it: | ||
|
|
||
| - unwraps `<details><summary>…</summary>` into a heading and drops the | ||
| closing tag (a text changelog has no collapse affordance); the heading is | ||
| emitted at `##` so the demotion lands it at `###`, the same sibling rank | ||
| v1's `## Complete Change List` reaches, keeping one skeleton across v1/v2 | ||
| releases in the same file, | ||
| - drops image lines and the `---` divider that precedes the Chinese | ||
| digest (release-page chrome), | ||
| - otherwise applies the existing heading demotion. | ||
|
|
||
| v1 bodies keep today's verbatim embedding. | ||
|
|
||
| ## Files Affected | ||
|
|
||
| | File | Change | | ||
| | ---------------------------------------------- | ------------------------------------------- | | ||
| | `scripts/generate-release-notes.js` | prompts, themes call, extraction, v2 render | | ||
| | `scripts/generate-changelog.js` | v2 marker + details/image transform | | ||
| | `scripts/tests/generate-release-notes.test.js` | new coverage | | ||
| | `scripts/tests/generate-changelog.test.js` | v2 embedding coverage | | ||
|
|
||
| No workflow, package.json, or `cap-release-notes.mjs` changes: the body | ||
| size stays far below the 120,000-char cap, and the script's CLI contract is | ||
| unchanged. | ||
|
|
||
| ## Open Questions | ||
|
|
||
| None blocking. Chinese phrasing quality is prompt-controlled and reviewed | ||
| per release; if it disappoints, tightening the summaries prompt is a | ||
| follow-up, not a design change. | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -61,7 +61,8 @@ const SECTION_ORDER = SECTIONS.map((section) => section.name); | |||||||||||||
|
|
||||||||||||||
| /** Matches a stable `vX.Y.Z` tag (no `-preview` / `-nightly` suffix). */ | ||||||||||||||
| const STABLE_TAG_RE = /^v?(\d+)\.(\d+)\.(\d+)$/; | ||||||||||||||
| const CURATED_RELEASE_MARKER = '<!-- qwen-release-notes:v1 -->'; | ||||||||||||||
| /** Marker comment AI-assisted release notes start with (`v1`, `v2`, …). */ | ||||||||||||||
| const CURATED_RELEASE_MARKER_RE = /^<!-- qwen-release-notes:v(\d+) -->/; | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Matches a GitHub "What's Changed" bullet, e.g. | ||||||||||||||
|
|
@@ -143,6 +144,34 @@ export function formatEntry(entry, cat = categorize(entry.title)) { | |||||||||||||
| return `- ${text} ([#${entry.prNumber}](${entry.prUrl}))`; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Per-line adjustments applied before the heading demotion. v1 notes embed | ||||||||||||||
| * verbatim; v2 notes are a digest with a collapsed appendix and inline | ||||||||||||||
| * screenshots, and the changelog keeps the text while unwrapping the | ||||||||||||||
| * collapse and dropping the images and the Chinese-digest divider. | ||||||||||||||
| */ | ||||||||||||||
| export function transformCuratedLine(line, version) { | ||||||||||||||
| if (version < 2) { | ||||||||||||||
| return [line]; | ||||||||||||||
| } | ||||||||||||||
| if (/^\s*!\[[^\]]*\]\(/.test(line)) { | ||||||||||||||
| return []; | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+157
to
+159
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R6-9: this v2 image-drop matches any line merely beginning with the image-open prefix — strictly broader than the renderer's actual image lines (two-space-indented, closed paren, bracket-free alt). Witness (probe at HEAD):
Suggested change
中文说明R6-9:该 v2 图片剥离规则匹配任何以图片起始前缀开头的行——比渲染器实际输出的图片行(两空格缩进、闭合括号、无中括号的 alt)严格更宽。 — qwen3.8-max via Qwen Code /review (v0.21.12) |
||||||||||||||
| if (/^\s*---\s*$/.test(line)) { | ||||||||||||||
| return []; | ||||||||||||||
| } | ||||||||||||||
| if (/^\s*<\/?details>\s*$/.test(line)) { | ||||||||||||||
| return []; | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+163
to
+165
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R8-26: the cross-file contract Suggested fix: pin the contract on one side: build the changelog v2 test's fixture body by calling 中文说明R8-26: — qwen3.8-max via Qwen Code /review (v0.21.12) |
||||||||||||||
| const summary = /^\s*<summary>([\s\S]*?)<\/summary>\s*$/.exec(line); | ||||||||||||||
| if (summary) { | ||||||||||||||
| // Emit at ## so the heading demotion below lands the unwrapped appendix | ||||||||||||||
| // at ### — the same sibling rank v1's "Complete Change List" reaches. | ||||||||||||||
| return [`## ${summary[1]}`]; | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+166
to
+171
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R1-7: the unwrapped appendix heading is emitted at 中文说明R1-7:展开后的附录标题以 — qwen3.8-max via Qwen Code /review (v0.21.12) |
||||||||||||||
| return [line]; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** Render one release as a Markdown block. */ | ||||||||||||||
| export function formatRelease(release) { | ||||||||||||||
| const lines = []; | ||||||||||||||
|
|
@@ -151,10 +180,14 @@ export function formatRelease(release) { | |||||||||||||
| : `## [${release.version}] - ${release.date}`; | ||||||||||||||
| lines.push(heading, ''); | ||||||||||||||
|
|
||||||||||||||
| if (release.body?.trimStart().startsWith(CURATED_RELEASE_MARKER)) { | ||||||||||||||
| const marker = CURATED_RELEASE_MARKER_RE.exec(release.body?.trimStart()); | ||||||||||||||
| if (marker) { | ||||||||||||||
| const version = Number(marker[1]); | ||||||||||||||
| const curated = release.body | ||||||||||||||
| .trimStart() | ||||||||||||||
| .split(/\r?\n/) | ||||||||||||||
| .filter((line) => line.trim() !== CURATED_RELEASE_MARKER) | ||||||||||||||
| .filter((line) => !CURATED_RELEASE_MARKER_RE.test(line)) | ||||||||||||||
| .flatMap((line) => transformCuratedLine(line, version)) | ||||||||||||||
| .map((line) => line.replace(/^(#{2,5})(\s+)/, '#$1$2')) | ||||||||||||||
| .join('\n') | ||||||||||||||
| .trim(); | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] R7-3: the committed design doc claims appendix title normalization uses the "same rule as
generate-changelog.jsformatEntry", but the two strip sets differ:formatEntrystrips feat/refactor/revert/fix/perf/docs (6 types viaTYPE_TO_SECTION), whilenormalizeAppendixTitlestrips onlyAPPENDIX_STRIP_TYPES= feat/fix/perf/docs (4). Probe at HEAD:refactor(core): rework session storagekeeps its prefix in the appendix but is stripped in CHANGELOG.md;revert:likewise diverges (feat/fix agree in both — presumably how the wording slipped through). The 4-type set is deliberate in code (TYPE_CATEGORIEScomment: Internal Changes headings don't name refactor/revert) — the doc, not the code, is wrong. — Concrete cost: a maintainer or agent aligning the code to this doc would strip type info from refactor/revert appendix entries under Internal Changes, the one place no heading conveys it.中文说明
R7-3:已提交的设计文档声称附录标题归一化与
generate-changelog.jsformatEntry采用"相同规则",但两者的剥离集合并不相同:formatEntry剥离 feat/refactor/revert/fix/perf/docs(经TYPE_TO_SECTION共 6 类),而normalizeAppendixTitle只剥离APPENDIX_STRIP_TYPES= feat/fix/perf/docs(4 类)。HEAD 探针:refactor(core): rework session storage在附录中保留前缀、在 CHANGELOG.md 中被剥离;revert:同样出现分歧(feat/fix 两边一致——措辞大概因此溜过)。代码中的 4 类集合是有意为之(TYPE_CATEGORIES注释:Internal Changes 标题不点名 refactor/revert)——错的是文档而非代码。— 具体代价:若有人按此文档对齐代码,会把 Internal Changes 下 refactor/revert 附录条目的类型信息剥掉,而那是唯一没有标题传达类型的位置。修复见 suggestion。— qwen3.8-max via Qwen Code /review (v0.21.12)