- name
- create-release-blog
- description
- Generate a narrative version release blog post from commits within a tag range. Use when the user wants to draft an article-style release blog (overview + a few feature highlights + a short list of minor changes), not a commit-dump release note. Asks the user to multi-select which features to highlight, then writes a bilingual draft under website/docs/{en,zh}/blog/.
- metadata
- {"internal":true}
# Create Release Blog
## Overview
Draft a bilingual release blog post under `website/docs/{en,zh}/blog/` for a given version
range. The post is an **article**, not a changelog: an opening overview, a few in-depth
highlight sections, then a single closing sentence linking to the GitHub release page.
The user will polish the draft manually. Aim for a strong starting point โ readable prose,
real code samples pulled from the actual PRs โ not a finished post. The **Style guidance**
section below defines the voice, wording, and formatting conventions to follow.
This is **not** a GitHub release-note generator. For that, use the `create-draft-release-notes`
skill. Release notes enumerate every commit; release blogs select 3โ5 features and tell a
story about them.
## Workflow
### 1. Resolve the version range
Accept these input shapes:
- `previous_tag..next_tag` โ explicit two-sided range. Use as-is; **skip the
range-start prompt** below.
- `next_version` (e.g. `0.10.0`) โ resolve the range start via the prompt below;
the end is `HEAD`.
- No input โ ask the user for the next version first (with `gh release list --limit 5`
as context), then run the prompt below.
The filename is `announcing-<major>-<minor>.mdx` (e.g. `0.10.0` โ `announcing-0-10.mdx`).
#### Prompt for the range start
Run the resolver to compute candidate predecessors:
```bash
node .agents/skills/create-release-blog/scripts/resolve-range-start.mjs <next-version>
```
It prints JSON like:
```json
{
"nextVersion": "0.10.0",
"bump": "minor",
"sameTier": "v0.9.0",
"latest": "v0.9.10",
"sameTierEqualsLatest": false,
"tagPrefix": "v"
}
```
`sameTier` is the predecessor in the same bumped tier (previous minor's `.0`,
previous patch, or last tag of the previous major). `latest` is the most recent
tag below `nextVersion`.
Use `AskUserQuestion` to pick the range start:
- If `sameTierEqualsLatest` is `false` and both are non-null, offer **two** options:
1. `<sameTier>` โ labeled e.g. "Previous minor (v0.9.0)" with a description
explaining "same biggest-version-tier predecessor; covers the full minor cycle".
2. `<latest>` โ labeled e.g. "Latest release (v0.9.10)" with a description
"most recent published tag; covers only commits since that patch".
- If `sameTierEqualsLatest` is `true`, offer **one** option (`<latest>`) and rely on
the auto-added "Other" entry for a custom tag.
- If either candidate is `null`, present whichever is non-null plus "Other".
The user picks one option, picks "Other" to type a custom tag, or types one inline.
The resolved range is `<chosen-start>..<next-end>` where `<next-end>` is `HEAD` (or
the user's explicit end tag). **The start tag is excluded from the search** โ `git`'s
`A..B` syntax yields commits reachable from `B` but not from `A`, so commits _on_ the
start tag itself are not included.
State the resolved range and the target filename before continuing.
### 2. Collect and bucket commits
Run the helper:
```bash
node .agents/skills/create-release-blog/scripts/collect-commits.mjs <previous_tag>..<next_tag>
```
It prints JSON with `breaking`, `feat`, `fix`, `perf`, `refactor`, `docs`, `other`. Each entry
has `sha`, `type`, `scope`, `subject`, `prNumber`, `raw`. Step 3 reads the `breaking` and
`feat` buckets; the other buckets are produced for completeness but unused by this flow.
### 3. Order highlight candidates
Sort entries in the `breaking` + `feat` buckets so the most blog-worthy ones appear first when
you present them. This is **ordering, not pruning** โ all candidates are still presented to
the user in the next step.
- `breaking` entries come first and are pre-recommended as defaults โ breaking changes almost
always warrant a highlight section.
- Within `feat`, rank by user-facing significance:
- **Scope priority**: `core` > `browser`/`browser-react`/`coverage-istanbul` >
`adapter-*`/`vscode` > internal/tooling scopes.
- **PR weight** (signal of size): `gh pr view <N> --json additions,deletions,files`; bigger
user-facing diffs outrank tiny ones. Discount lockfiles, generated code, and fixture-only
diffs.
- **Docs touched**: PRs that modified `website/docs/**` are user-facing โ bump them up.
- **Subject keywords**: subjects with "support", "add", "new" beat "tweak", "improve",
"expose".
### 4. Summarize every candidate and ask the user to pick
**Read each candidate PR before listing them** โ do not paste raw commit subjects. For every
candidate, fetch the PR:
```bash
gh pr view <PR> --json title,body,files,labels
```
Then write **one sentence** of user-side value per candidate:
- **Lead with the user-visible benefit, not the implementation.** Good: "Run only the tests
affected by your uncommitted changes for faster local iteration." Bad: "Add a `--changed`
flag wired through the module graph resolver."
- Drop framework-internal jargon (`taskContext`, `birpc`, `dispatch namespace`) unless the
feature is itself about that internal โ and even then, gloss it ("the worker-to-host
channel").
- If the PR body has a clear motivation paragraph, paraphrase it. If the PR body is thin
(one-liner, just a checklist), infer the user impact from the `files` field and the related
docs path, and say so explicitly: "PR body was thin; summary inferred from changed files."
Present **all** candidates as a numbered markdown list โ do not use `AskUserQuestion`, which
caps at 4 options per question and is too narrow for this step. Format:
```text
1. **`feat(core): subject`** ([#1234](https://github.com/web-infra-dev/rstest/pull/1234))
<one-sentence plain-language user-value summary>
2. **`feat(browser): subject`** ([#1235](https://github.com/web-infra-dev/rstest/pull/1235))
<summary>
...
```
End with: "Reply with the numbers you'd like to highlight (e.g. `1,3,5`). 2โ4 is the
sweet spot โ fewer reads thin, more dilutes the article." Wait for the user's reply before
continuing.
### 5. Gather code-sample context for each chosen highlight
The PR body was already loaded in step 4. For each _selected_ highlight, additionally:
```bash
gh pr diff <PR> | head -300 # see the API surface
```
Then:
- Look for a related docs page in `website/docs/en/`; if present, link to it from the
highlight section instead of restating the docs.
- Extract a small (5โ15 line) code sample that shows the new surface. Prefer real code from
PR-added tests, examples, or docs. **Do not invent API shapes** โ if the PR diff doesn't
show usage, search the test fixtures or website docs for an example.
### 6. Write the EN post
Path: `website/docs/en/blog/announcing-<major>-<minor>.mdx`. Structure:
```mdx
---
description: 'One-line summary used on the blog list and as OG description.'
date: YYYY-MM-DD HH:mm:ss
sidebar: false
authors:
- name: <github-handle>
avatar: 'https://github.com/<github-handle>.png'
---
_Month Day, Year_
# Announcing Rstest X.Y
<img
src="https://assets.rspack.rs/rstest/rstest-banner-vX-Y.png"
alt="Rstest X.Y"
style={{
boxShadow: '0 2px 6px rgba(0, 0, 0, 0.08)',
}}
/>
Rstest X.Y has been released.
Notable changes:
- <Feature one: short noun- or verb-phrase>
- <Feature two: short noun- or verb-phrase>
- <Feature three: short noun- or verb-phrase>
- <Feature four: short noun- or verb-phrase>
## <Feature one heading>
<2โ3 short paragraphs that lead with what shipped, then walk through motivation, mechanism,
and trade-offs. Stay factual, no narrative bridges.>
<Code block introduced by a concrete action verb โ see Style guidance.>
<Optional follow-up paragraph ending with a uniform docs link โ see Style guidance.>
## <Feature two heading>
...
---
For a full list of changes, see the [vX.Y.Z release notes](https://github.com/web-infra-dev/rstest/releases/tag/vX.Y.Z).
```
Rules for frontmatter `authors`:
- List the people who drafted **this specific post**, not the maintainer set. Without
`authors`, the blog list shows the generic `Rstest Team`.
- Ask the user who should be credited and in what order; do **not** infer from `git log`
or PR authorship.
- Each entry takes `name` (GitHub handle) and `avatar` (`https://github.com/<handle>.png`).
Keep the order identical between EN and ZH frontmatter.
Rules for the banner image:
- Insert the banner **between** the H1 and the greeting. Use `<img>` JSX (not markdown
``) so it can carry an inline `boxShadow` โ the shadow separates the lower
edge from a white page background in light mode.
- Do **not** set `width` / `aspectRatio` / `objectFit`. The banner is designed at its
intended on-page size; extra constraints crop or shrink the artwork.
- Asset URL: `https://assets.rspack.rs/rstest/rstest-banner-v<major>-<minor>.png` (no
patch segment; blog posts are per-minor). Banners ship via PR against
[rstackjs/rstack-design-resources](https://github.com/rstackjs/rstack-design-resources).
- Generate the banner and OG card together with `pnpm gen:release-image` from `website/`
and compress both with `pnpm dlx pngquant-bin` before committing them to
design-resources. `website/AGENTS.md` ("Release image generation") owns the exact
commands and flags.
- For a local preview before the CDN asset exists, temporarily copy the banner into
a `public` directory under `website/docs/` (create it if absent) and point `src` at
`/rstest-banner-v<major>-<minor>.png`. Revert both before opening the PR.
- Inline style: `{{ boxShadow: '0 2px 6px rgba(0, 0, 0, 0.08)' }}`. No border, no
border-radius.
Rules for the OG image:
- The OG card is a **separate** asset from the banner: `rstest-og-image-v<major>-<minor>.png`
(2400ร1260, 1.91:1 social-share aspect). Do not point `og:image` at the banner โ a wide
strip renders poorly as a Twitter/Slack card.
- Per-post override lives in `website/rspress.config.ts` via the top-level `head` function
(route-aware). Frontmatter `head` does **not** override the site-wide default reliably:
rspress doesn't dedupe `og:image`, so two tags end up in the HTML and scrapers pick the
first.
- Confirm the OG asset resolves before publish (`curl -I`).
Rules for the intro:
- Open with a one-line greeting: `Rstest X.Y has been released.` โ period, not exclamation;
factual, not celebratory. (See Style guidance for the broader tone rules.)
- Follow with `Notable changes:` and a bullet list of the chosen highlights. Each bullet
is a short noun- or verb-phrase naming the feature, wrapped as a markdown link to that
section's `\{#kebab-case}` anchor. The entire bullet is the link text โ do not link only
a fragment.
- Bullets carry **no** PR links, code fences, or extra prose, and **no** narrative
paragraph follows them. The bullets _are_ the overview; the section bodies do the
explaining.
Rules for the closing release-notes link:
- Close the post with a **single sentence** linking to the GitHub release page for this
version. No `## More improvements` heading, no per-PR bullet list โ the release page
already enumerates everything.
- Template: `For a full list of changes, see the [vX.Y.Z release notes](https://github.com/web-infra-dev/rstest/releases/tag/vX.Y.Z).`
The URL must use the `v`-prefixed tag (`v0.10.0`, not `0.10.0`).
- Place it as the **last** paragraph of the post, immediately after the last highlight
section. Do **not** wrap it in a heading.
- Separate it from the preceding section with a markdown horizontal rule (`---` on its
own line, blank lines above and below) โ it's a meta-pointer, not a continuation.
**Do not add an Acknowledgements / "Thanks to contributors" section.** The post ends with
the release-notes link. Contributors are surfaced on the release page itself.
### 7. Write the ZH post
Path: `website/docs/zh/blog/announcing-<major>-<minor>.mdx`. Mirror the EN structure exactly.
- Translate **every** heading and prose paragraph. Headings that must be translated include:
- `# Announcing Rstest X.Y` โ `# Rstest X.Y ๅๅธ`
- Each `##` highlight section heading.
- **Translate the date line** above the title: `_May 14, 2026_` โ `_2026 ๅนด 5 ๆ 14 ๆฅ_`.
- **Translate the intro greeting and bullet preamble**: `Rstest X.Y has been released.` โ
`Rstest X.Y ๅทฒ็ปๅๅธใ` (full-width period, no exclamation). `Notable changes:` โ
`ไธป่ฆๅๆด๏ผ`.
- **Translate the closing release-notes sentence** but keep the URL identical:
`For a full list of changes, see the [vX.Y.Z release notes](URL).` โ
`ๅฎๆดๅๆด่ฏทๅ่ [vX.Y.Z release notes](URL)ใ`. Keep the link text in English (it
reads as a proper noun referring to the GitHub page).
- **Keep technical terms in English** (Rspack, ESM, TypeScript, threads pool, snapshot,
worker, pool, cache, build, CLI, CI, TTY, artifact, etc.), per the website's `AGENTS.md`.
Do **not** translate them to ๅทฅไฝ่ฟ็จ / ๆฑ / ็ผๅญ / ๆๅปบ / ๅถๅ / ๅฝไปค่ก / ๆ็ปญ้ๆ / ็ป็ซฏ / ๅฟซ็
ง.
- **Keep code samples, frontmatter `date`, table contents, PR links, and `\{#kebab-case}`
anchor slugs identical to EN** โ the slug match is what makes the intro-bullet links work
across both locales.
- Use **full-width punctuation** in prose (๏ผใ๏ผ๏ผ๏ผ๏ผใใ๏ผ๏ผ). Half-width is fine inside
code, identifiers, and version strings.
### 8. Wire posts into the blog
For each locale, insert the new slug right after `index` in
`website/docs/<lang>/blog/_meta.json`:
```json
["index", "announcing-0-10", ...existing slugs in newest-first order]
```
### 9. Hand off to the user
Report:
- The two file paths created and the chosen highlight PRs.
- Where each code sample came from (PR or docs path) so the user can verify shape.
- A reminder that overview tone, example quality, and link accuracy still need manual polish.
## Style guidance
### Voice and framing
- **Feature-first leads.** Open every highlight section with what shipped: `Rstest X.Y now
supports ...` / `Rstest X.Y adds a new ... flag` / `Rstest X.Y has changed the default of
X from A to B`. Do **not** open with the user's pain (`When you're iterating on a single
file...`), the abstract benefit (`Test runs have a fixed cost...`), or a narrative bridge
between sections (`The other large fixed cost is...`).
- **No marketing register.** Skip "we're excited", "happy to announce", "huge thanks",
banner phrases. A factual greeting + bullets is the entire intro.
- **Link, don't restate.** When docs exist for the feature, link to them rather than
copying explanations into the blog.
View on GitHub