-
-
Notifications
You must be signed in to change notification settings - Fork 10.1k
AI: Add skill to construct minor release changelog #34723
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
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
463a51b
add skill to write changelog entry for minor and major releases
JReinhold 860afe7
Minor-release skill: add explicit rules to prevent wasteful exploration
JReinhold 33064f5
Merge branch 'next' into jeppe/cahngelog-writing-skill
JReinhold 6fc8fc5
add language identifiers to code blocks
JReinhold 68f4080
Merge branch 'jeppe/cahngelog-writing-skill' of github.com:storybookj…
JReinhold 3691be3
cleanup
JReinhold File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| --- | ||
| name: minor-release | ||
| description: Write the changelog entry for a new minor or major Storybook release. Use when preparing a CHANGELOG.md entry for a X.Y.0 version. | ||
| allowed-tools: Bash, Read, Write, Edit | ||
| --- | ||
|
|
||
| # Write Minor/Major Release Changelog | ||
|
|
||
| Assembles and writes a polished changelog entry for a Storybook minor or major (`X.Y.0`) release into `CHANGELOG.md`. | ||
|
|
||
| ## Rules — read before doing anything | ||
|
|
||
| - **The two helper scripts are at `.agents/skills/minor-release/get-minor-changelog-summary.ts` and `.agents/skills/minor-release/write-minor-changelog-section.ts`. Do not search for them anywhere else in the repo. Do not look in `scripts/`, `package.json`, `AGENTS.md`, or elsewhere. Do not read the script source files. Just run them.** | ||
| - **Do not read `CHANGELOG.md` or `CHANGELOG.prerelease.md` before running the scripts.** The scripts handle version detection and entry collection automatically. | ||
| - **Do not read `CHANGELOG.md` for style guidance.** The style examples in Step 3 of this skill are the authoritative reference. Do not grep or read the changelog to infer format. | ||
| - Run all commands from the repository root. | ||
|
|
||
| ## Step 1: Collect the changelog entries | ||
|
|
||
| Run the helper script to see all unique changelog entries from the prereleases, with patch-backported changes filtered out: | ||
|
|
||
| ```bash | ||
| node .agents/skills/minor-release/get-minor-changelog-summary.ts [version] | ||
| ``` | ||
|
|
||
| - Omit `[version]` to auto-detect from the most recent prerelease in `CHANGELOG.prerelease.md` | ||
| - Pass `--verbose` to see which prerelease versions were found and how many patch PRs were filtered | ||
| - The output is a plain list of `- Entry text` lines, one per change | ||
|
|
||
| Read the output carefully — you will use these entries in Step 2 to choose highlights. | ||
|
|
||
| ## Step 2: Select highlights | ||
|
|
||
| From the entries collected in Step 1, identify the **4–8 most significant changes** to feature as highlights. If fewer than 4 significant changes exist, include all available significant changes rather than padding the list with minor items. Good candidates: | ||
|
|
||
| - Major new features or capabilities | ||
| - New framework/ecosystem support (new renderer, builder, or major version support) | ||
| - Prominent DX or performance improvements | ||
| - Significant experimental features worth calling out | ||
|
|
||
| Exclude from highlights (they still belong in the full list): | ||
|
|
||
| - Bug fixes | ||
| - Maintenance, cleanup, dependency updates | ||
| - Minor improvements or internal refactors | ||
| - Reverts | ||
|
|
||
| ## Step 3: Compose the highlights text | ||
|
|
||
| Based on the entries from Step 2, write the highlights section — the text that goes between the `## X.Y.0` heading and the full entry list. This is what you will pass to the write script. Follow the `Writing style` rules below. | ||
|
|
||
| ### Writing style | ||
|
|
||
| The highlights text should contain only these parts, in order: | ||
|
|
||
| - An optional tagline line in the form `> _..._` | ||
| - One intro sentence | ||
| - One or more highlight bullet lists | ||
|
|
||
| Do not include the `## X.Y.0` heading — the script adds that. | ||
|
|
||
| The default format is: | ||
|
|
||
| ```text | ||
| > _Tagline phrase_ | ||
|
|
||
| Storybook X.Y [intro sentence]: | ||
|
|
||
| - 🔣 Highlight one: brief description | ||
| - 🔣 Highlight two: brief description | ||
| - 🔣 Highlight three: brief description | ||
| ``` | ||
|
|
||
| For releases with 7-8 highlights, a second group of bullets with its own intro sentence is fine (see 10.1.0 example). | ||
|
|
||
| **Style rules:** | ||
| - The tagline is wrapped in `> _italics_`. It is a short (5–10 word) noun phrase summarising the release theme — no verbs, no "we". | ||
| - Each highlight bullet: relevant emoji, feature/area name, brief description. Terse and specific. | ||
| - The intro sentence uses `Storybook X.Y contains hundreds of fixes and improvements including:` for most releases. For a major version, the intro is more impactful (see 10.0.0 example). | ||
|
|
||
| ### Examples | ||
|
|
||
| **10.3.0** — a typical release with a single highlight group: | ||
|
|
||
| ```text | ||
| > _Improved developer experience, AI-assisting tools, and broader ecosystem support_ | ||
|
|
||
| Storybook 10.3 contains hundreds of fixes and improvements including: | ||
|
|
||
| - 🤖 Storybook MCP: Agentic component dev, docs, and test (Preview release for React) | ||
| - ⚡ Vite 8 support | ||
| - ▲ Next.js 16.2 support | ||
| - 📝 ESLint 10 support | ||
| - 〰️ Addon Pseudo-States: Tailwind v4 support | ||
| - 🔧 Addon-Vitest: Simplified configuration - no more setup files required | ||
| - ♿ Numerous accessibility improvements across the UI | ||
| ``` | ||
|
|
||
| **10.1.0** — a release with multiple highlight groups: | ||
|
|
||
| ```text | ||
| > _Easier to setup, more accessible to use_ | ||
|
|
||
| Storybook 10.1 focuses on two key improvements: installation and accessibility: | ||
|
|
||
| - ♿ UI overhaul to fix hundreds of a11y issues | ||
| - 🧑💻 CLI overhaul for faster, more reliable install | ||
| - ✅ Checklist-based onboarding guide for new users | ||
|
|
||
| The release also contains compatibility fixes for: | ||
|
|
||
| - 🅰️ Angular 21 support | ||
| - 🦀 RSbuild install support in CLI | ||
| - ⚡️ Preact support for Vitest addon | ||
|
|
||
| Finally, it contains two highly-requested experimental features: | ||
|
|
||
| - 📋 Component manifest for Storybook MCP | ||
| - ⚛️ Improved JSX code snippets for React | ||
| ``` | ||
|
|
||
| **10.0.0** — a major release with a more impactful intro (no `>` tagline, two-sentence intro): | ||
|
|
||
| ```text | ||
| Storybook 10 contains one breaking change: it's ESM-only. This simplifies our distribution and reduces install size by 29% while simultaneously unminifying dist code for easier debugging. | ||
| It also includes features to level up your UI development, documentation, and testing workflows: | ||
|
|
||
| - 🧩 Module automocking for easier testing | ||
| - 🏭 Typesafe CSF factories Preview for React | ||
| - 💫 UI editing and sharing optimizations | ||
| - 🏷️ Tag filtering exclusion and configuration for sidebar management | ||
| - 🔀 Next 16, Vitest 4, Svelte async components, and more! | ||
| ``` | ||
|
|
||
| ## Step 4: Write the changelog entry | ||
|
|
||
| Pass the highlights text to the write script via stdin. The script will call the summary script internally, combine the full entry list with your highlights, and write the complete section to `CHANGELOG.md` — inserting at the top or overwriting any existing section for this version. | ||
|
|
||
| ```bash | ||
| node .agents/skills/minor-release/write-minor-changelog-section.ts [version] << 'HIGHLIGHTS' | ||
| > _Your tagline here_ | ||
|
|
||
| Storybook X.Y contains hundreds of fixes and improvements including: | ||
|
|
||
| - 🔣 Highlight one | ||
| - 🔣 Highlight two | ||
| HIGHLIGHTS | ||
| ``` | ||
|
|
||
| Use `--dry-run` to preview the composed section before writing: | ||
|
|
||
| ```bash | ||
| node .agents/skills/minor-release/write-minor-changelog-section.ts --dry-run << 'HIGHLIGHTS' | ||
| ... | ||
| HIGHLIGHTS | ||
| ``` | ||
206 changes: 206 additions & 0 deletions
206
.agents/skills/minor-release/get-minor-changelog-summary.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| import { readFile } from 'node:fs/promises'; | ||
| import { createRequire } from 'node:module'; | ||
| import { extname } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| import { Command } from 'commander'; | ||
| import picocolors from 'picocolors'; | ||
| import semver from 'semver'; | ||
|
|
||
| const PRERELEASE_CHANGELOG_PATH = fileURLToPath( | ||
| new URL('../../../CHANGELOG.prerelease.md', import.meta.url) | ||
| ); | ||
| const STABLE_CHANGELOG_PATH = fileURLToPath(new URL('../../../CHANGELOG.md', import.meta.url)); | ||
|
|
||
| const program = new Command(); | ||
|
|
||
| program | ||
| .name('get-minor-changelog-summary') | ||
| .description( | ||
| 'Collects all changelog entries from prereleases of a minor/major version, filtering out changes that were already shipped in patch releases of the previous minor. If no version is given, the stable version of the most recent prerelease is used.' | ||
| ) | ||
| .arguments('[version]') | ||
| .option('-V, --verbose', 'Enable verbose logging', false); | ||
|
|
||
| /** | ||
| * Parse a markdown changelog file into a map of version string -> section body. | ||
| * Sections are delineated by `## <version>` headings. | ||
| */ | ||
| function parseChangelogSections(content: string): Map<string, string> { | ||
| const sections = new Map<string, string>(); | ||
| const parts = content.split(/^## /m); | ||
|
JReinhold marked this conversation as resolved.
|
||
| for (const part of parts) { | ||
| if (!part.trim()) { | ||
| continue; | ||
| } | ||
| const newlineIdx = part.indexOf('\n'); | ||
| const version = newlineIdx === -1 ? part.trim() : part.slice(0, newlineIdx).trim(); | ||
| const body = newlineIdx === -1 ? '' : part.slice(newlineIdx + 1); | ||
| sections.set(version, body); | ||
| } | ||
|
JReinhold marked this conversation as resolved.
|
||
| return sections; | ||
| } | ||
|
|
||
| /** Extract all PR numbers referenced in a changelog section body. */ | ||
| function extractPRNumbers(content: string): Set<number> { | ||
| const prNumbers = new Set<number>(); | ||
| const prRegex = /\[#(\d+)\]\(https:\/\/github\.com\//g; | ||
| let match; | ||
| while ((match = prRegex.exec(content)) !== null) { | ||
| prNumbers.add(parseInt(match[1], 10)); | ||
| } | ||
| return prNumbers; | ||
| } | ||
|
|
||
| /** Extract individual changelog list items (lines starting with `- `) from a section body. */ | ||
| function extractEntries(content: string): string[] { | ||
| return content | ||
| .split('\n') | ||
| .filter((line) => line.startsWith('- ')) | ||
| .map((line) => line.trim()); | ||
| } | ||
|
|
||
| export const getMinorChangelogSummary = async (args: { version?: string; verbose?: boolean }) => { | ||
| const { verbose } = args; | ||
|
|
||
| const log = (...msg: unknown[]) => { | ||
| if (verbose) { | ||
| console.error(...msg); | ||
| } | ||
| }; | ||
|
|
||
| const [prereleaseChangelog, stableChangelog] = await Promise.all([ | ||
| readFile(PRERELEASE_CHANGELOG_PATH, 'utf-8'), | ||
| readFile(STABLE_CHANGELOG_PATH, 'utf-8'), | ||
| ]); | ||
|
|
||
| const preSections = parseChangelogSections(prereleaseChangelog); | ||
| const stableSections = parseChangelogSections(stableChangelog); | ||
|
|
||
| let version = args.version; | ||
| if (!version) { | ||
| const firstPrerelease = [...preSections.keys()][0]; | ||
| if (!firstPrerelease) { | ||
| throw new Error( | ||
| `No prerelease entries found in ${picocolors.green(PRERELEASE_CHANGELOG_PATH)}` | ||
| ); | ||
| } | ||
| const p = semver.parse(firstPrerelease); | ||
| if (!p) { | ||
| throw new Error( | ||
| `Could not parse version from first prerelease entry: ${picocolors.red(firstPrerelease)}` | ||
| ); | ||
| } | ||
| version = `${p.major}.${p.minor}.0`; | ||
| log( | ||
| `🔍 No version specified, inferred ${picocolors.blue(version)} from most recent prerelease ${picocolors.green(firstPrerelease)}` | ||
| ); | ||
| } | ||
|
JReinhold marked this conversation as resolved.
|
||
|
|
||
| const parsed = semver.parse(version); | ||
| if (!parsed || parsed.patch !== 0 || parsed.prerelease.length > 0) { | ||
| throw new Error( | ||
| `Version must be a stable minor or major release (e.g. 10.4.0), got: ${picocolors.red(version)}` | ||
| ); | ||
| } | ||
|
|
||
| const { major, minor } = parsed; | ||
|
|
||
| // Collect all prerelease versions for the target (e.g. 10.4.0-alpha.*, 10.4.0-beta.*, 10.4.0-rc.*) | ||
| const prereleaseVersionPrefix = `${major}.${minor}.0-`; | ||
| const prereleaseVersions = [...preSections.keys()] | ||
| .filter((v) => v.startsWith(prereleaseVersionPrefix)) | ||
| .sort((a, b) => semver.compare(a, b)); | ||
|
|
||
| if (prereleaseVersions.length === 0) { | ||
| throw new Error( | ||
| `No prerelease versions found for ${picocolors.blue(version)} in ${picocolors.green(PRERELEASE_CHANGELOG_PATH)}` | ||
| ); | ||
| } | ||
|
|
||
| log( | ||
| `🔍 Found ${picocolors.blue(prereleaseVersions.length)} prerelease version(s): ${prereleaseVersions.join(', ')}` | ||
| ); | ||
|
|
||
| // Collect PR numbers that already shipped in patch releases of the previous minor (e.g. 10.3.1+) | ||
| const patchPRNumbers = new Set<number>(); | ||
|
|
||
| if (minor > 0) { | ||
| const prevMinor = minor - 1; | ||
| const patchVersions = [...stableSections.keys()].filter((v) => { | ||
| const p = semver.parse(v); | ||
| return p && p.major === major && p.minor === prevMinor && p.patch > 0; | ||
| }); | ||
|
|
||
| log( | ||
| `🔍 Patch versions to filter out: ${picocolors.yellow(patchVersions.join(', ') || 'none')}` | ||
| ); | ||
|
|
||
| for (const pv of patchVersions) { | ||
| const body = stableSections.get(pv)!; | ||
| for (const pr of extractPRNumbers(body)) { | ||
| patchPRNumbers.add(pr); | ||
| } | ||
| } | ||
|
|
||
| log( | ||
| `🔍 Found ${picocolors.yellow(patchPRNumbers.size)} PR(s) already shipped in patch releases — these will be filtered out` | ||
| ); | ||
| } else { | ||
| log(`ℹ️ Version ${picocolors.blue(version)} is a major release, no patches to filter`); | ||
| } | ||
|
|
||
| // Aggregate changelog entries from all prereleases, deduplicating by PR number | ||
| const entriesByPR = new Map<number, string>(); | ||
| const directCommitEntries: string[] = []; | ||
|
|
||
| for (const preVersion of prereleaseVersions) { | ||
| const body = preSections.get(preVersion)!; | ||
| const entries = extractEntries(body); | ||
|
|
||
| for (const entry of entries) { | ||
| const prMatch = entry.match(/\[#(\d+)\]/); | ||
| if (prMatch) { | ||
| const prNum = parseInt(prMatch[1], 10); | ||
| if (!patchPRNumbers.has(prNum) && !entriesByPR.has(prNum)) { | ||
| entriesByPR.set(prNum, entry); | ||
| } | ||
| } else if (!directCommitEntries.includes(entry)) { | ||
| directCommitEntries.push(entry); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const allEntries = [...entriesByPR.values(), ...directCommitEntries].sort(); | ||
| const text = allEntries.join('\n'); | ||
|
|
||
| log(`\n✅ Generated changelog summary (${picocolors.blue(allEntries.length)} entries):\n`); | ||
|
|
||
| return { text, entryCount: allEntries.length, resolvedVersion: version }; | ||
| }; | ||
|
|
||
| function esMain(url: string): boolean { | ||
| if (!url || !process.argv[1]) { | ||
| return false; | ||
| } | ||
| const require = createRequire(url); | ||
| const scriptPath = require.resolve(process.argv[1]); | ||
| const modulePath = fileURLToPath(url); | ||
| const extension = extname(scriptPath); | ||
| return extension | ||
| ? modulePath === scriptPath | ||
| : modulePath.slice(0, -extname(modulePath).length) === scriptPath; | ||
| } | ||
|
|
||
| if (esMain(import.meta.url)) { | ||
| const parsed = program.parse(); | ||
| const [version] = parsed.args; | ||
| if (version && !semver.valid(version)) { | ||
| console.error( | ||
| `🚨 Invalid argument, expected a semver version e.g. ${picocolors.green('10.4.0')}, got: ${picocolors.red(version)}` | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| const { text } = await getMinorChangelogSummary({ version, verbose: parsed.opts().verbose }); | ||
| console.log(text); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.