docs/design/2026-08-15-user-facing-release-notes.md
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:
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.
release.yml (it still
publishes GitHub-generated notes immediately; finalize rewrites later).release.yml → gh api …/releases/generate-notes anchored at the
previous tag → cap-release-notes.mjs → gh release create.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 edits it in place. Marker: <!-- qwen-release-notes:v1 -->.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).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:
{
"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).
<!-- 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:
---
divider, then ## 中文摘要. Each audience reads one contiguous block;
GitHub's TOC and release page stay scannable.##, matching today's section weight; Chinese themes use
### under the ## 中文摘要 umbrella.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.Deterministic extraction, no model involvement:
, ``, and bare image URLs.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.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.
| 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.
generate-changelog.js accepts markers v1 and v2. For v2 bodies it:
<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,--- divider that precedes the Chinese
digest (release-page chrome),v1 bodies keep today's verbatim embedding.
| 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.
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.