Files

165 lines
9.3 KiB
Markdown

---
name: changelog
description: Regenerate the [Unreleased] section of every affected CHANGELOG.md in Keep a Changelog style. Reads commits since the last release tag plus any uncommitted or staged changes, classifies them by Conventional Commit prefix, and rewrites each [Unreleased] block. Works in single-package repos and monorepos (one CHANGELOG.md per package). Use when preparing a release or drafting changelog entries. Idempotent — safe to re-run as work lands.
argument-hint: [--since <ref>]
allowed-tools: Bash(git *), Read, Edit
---
# Generate CHANGELOG entries
You are tasked with regenerating the `## [Unreleased]` section of every affected `CHANGELOG.md` in the repository so it reflects all change since the last release tag — committed and uncommitted alike.
## Range hint
`$ARGUMENTS` (empty/literal → range starts at the last release tag from `git describe --tags --abbrev=0`)
## Workflow
1. Bail-out checks
2. Determine the change range
3. Determine each CHANGELOG's scope and collect commits + uncommitted hunks
4. Classify and draft entries
5. Preview and confirm
6. Apply
## Step 1: Bail-out checks
1. Run `git rev-parse --is-inside-work-tree`. If not a git repo, tell the user "This directory is not a git repository." and stop.
2. Run `git ls-files 'CHANGELOG.md' '**/CHANGELOG.md'` to discover every tracked changelog. If zero results, tell the user "No `CHANGELOG.md` found in the repository — create one (root or per-package) before running this skill." and stop.
3. Run `git describe --tags --abbrev=0` to confirm at least one release tag exists. If none, ask the user to supply `--since <ref>` and stop until they do.
## Step 2: Determine the change range
1. Parse `$ARGUMENTS` for a `--since <ref>` flag. If absent, set `SINCE=$(git describe --tags --abbrev=0)`.
2. The range is `$SINCE..HEAD` for committed changes, plus the current uncommitted+staged working tree.
## Step 3: Determine each CHANGELOG's scope, then collect commits + uncommitted hunks
Each `CHANGELOG.md` discovered in Step 1.2 owns a path scope:
- **Nested CHANGELOG** (e.g. `packages/foo/CHANGELOG.md`, `apps/web/CHANGELOG.md`): scope is its parent directory — `packages/foo/`, `apps/web/`.
- **Root CHANGELOG** (`CHANGELOG.md` at repo root):
- If no nested CHANGELOGs exist: scope is the entire repository.
- If nested CHANGELOGs also exist: scope is the repository **excluding** every directory that owns a nested CHANGELOG. The root file captures repo-wide change (CI, build config, root README) that no per-package file would claim.
For each scope:
1. Committed: `git log $SINCE..HEAD --pretty=format:"%H%x09%s%x09%b%x1e" -- <scope>`. For root-with-exclusions, pass `:(exclude)<dir>` pathspecs for every nested-CHANGELOG directory. Records are `\x1e`-delimited; parse subject (`%s`) and body (`%b`).
2. Uncommitted: `git diff HEAD -- <scope>` and `git diff --cached -- <scope>` with the same pathspec rules. Treat the union as a single virtual "pending" change set with no commit message — the model classifies it from the diff itself.
3. Skip CHANGELOGs whose scope has no committed and no uncommitted changes in range.
## Step 4: Classify and draft entries
For each affected CHANGELOG, produce entries grouped under the Keep a Changelog 1.1.0 sections, in this order: `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`, `Performance`. Append a `Breaking / Upgrade Notes` section only when a breaking change exists.
### Conventional Commit → section mapping
- `feat:`**Added**
- `fix:`**Fixed**
- `perf:`**Performance**
- `refactor:`, `style:`, `build:`, `ci:`, `chore:`**Changed**
- `docs:`**Changed** (only if user-facing docs; skip internal `thoughts/` or research notes)
- `test:` → omit (not user-visible)
- `revert:`**Changed** (note what was reverted)
### Always-skip commits
Skip any commit whose subject matches one of these — they are release pipeline housekeeping, not user-visible change:
- `Release v<x.y.z>` or `chore(release): v<x.y.z>` (common release-bot patterns)
- `Add [Unreleased] section for next cycle`
- Version-only bumps with no other content (`<x.y.z>` as the entire subject)
- Merge commits with no diff content of their own
### Breaking change detection
Flag a commit as breaking if any of these are true:
- The type has a `!` suffix (`feat!:`, `refactor!:`, etc.)
- The commit body contains a `BREAKING CHANGE:` footer
- The diff removes or renames an exported symbol, removes a CLI flag, or removes a public file
For each breaking change, add an entry to **Breaking / Upgrade Notes** in addition to the regular section, written as a one-line upgrade instruction.
### Style rules — match Keep a Changelog 1.1.0 prose
- One short user-facing sentence per entry. Imperative mood ("Add", "Fix", "Remove").
- Write for the plugin's **users**, not its maintainers. No internal symbol names, file paths, regex literals, or precedent commit hashes inside entries.
- If a feature has a user-visible name (a slash command, a CLI flag, a skill name), name it in backticks. Example: `` Added `--locale` flag for per-invocation language override. ``
- Group entries by category, not by commit. Merge duplicate-topic commits into one entry.
- If a commit reverses something earlier in the same `[Unreleased]` window (e.g. add → remove → add-back), reflect only the net effect.
- Skip entries that have zero user-visible impact: dependency bumps with no behavior change, internal refactors invisible to users, test additions, type-only changes.
### Worked example
Input commits in `packages/api/`:
```
abc1234 feat(api): add /v2/search endpoint with cursor pagination
def5678 feat(api): support webhook retries with exponential backoff
ghi9abc fix(api): rotate session secret on every JWT refresh
jkl0def docs(api): document rate-limit headers in OpenAPI spec
mno1234 chore(deps): bump @types/node to 20.11
pqr5678 test(api): coverage for cursor edge cases
stu9abc refactor(api): inline httpClient factory (no behavior change)
```
Output `[Unreleased]`:
```markdown
## [Unreleased]
### Added
- `/v2/search` endpoint with cursor-based pagination.
- Webhook delivery retries with exponential backoff.
### Changed
- OpenAPI spec documents rate-limit response headers.
### Fixed
- JWT refresh rotates the session secret on every renewal.
```
What this example demonstrates:
- Two `feat:` commits → two **Added** entries (one per user-visible feature).
- `docs:` for a user-facing API spec → **Changed** (skip if the docs touched were internal notes).
- `fix:`**Fixed**, written as the corrected behavior in imperative mood, not as the bug.
- `chore(deps):` with no behavior change → omitted.
- `test:` → omitted (not user-visible).
- `refactor:` flagged "no behavior change" → omitted (the rule is user-visible impact, not commit type).
- Commit hashes never appear in entries.
## Step 5: Preview and confirm
1. Print a per-CHANGELOG summary: file path, count by section, breaking-change flag.
2. Print the proposed `[Unreleased]` body for each affected CHANGELOG, in full.
3. Call `ask_user_question`:
- Question: "Apply regenerated `[Unreleased]` to {N} CHANGELOG(s)?"
- Header: "Changelog"
- Options:
- "Apply (Recommended)" — Write the regenerated sections to disk. Refinement, if needed, happens afterward in normal chat (`Edit` tool) or via `git restore` to roll back.
- "Show Preview" — For each affected CHANGELOG, render a unified diff between the **current** `[Unreleased]` body on disk and the **proposed** regenerated body. Lines marked `-` are about to be removed; lines marked `+` are about to be added. After printing, re-ask this same question.
## Step 6: Apply
For each affected CHANGELOG:
1. Read the file.
2. Locate the `## [Unreleased]` heading. The block runs from that heading up to (but not including) the next `## [` heading — or end of file if no later version exists. If no `## [Unreleased]` heading exists, insert one above the first `## [` heading (or after the file's intro prose if no version sections exist yet).
3. Use `Edit` to replace the entire block with `## [Unreleased]\n\n` followed by the regenerated sections.
4. **Never** touch any heading below `[Unreleased]`. Released version sections are immutable.
After all writes complete, print the list of modified files and remind the user to commit them before invoking their release pipeline — most release scripts require a clean working tree.
## Important Notes
- ALWAYS preview before writing. Never apply without the user's `ask_user_question` confirmation.
- ALWAYS replace the full `[Unreleased]` body, not append. The skill is idempotent regeneration, not accumulation.
- NEVER modify released version sections (anything below the first `## [x.y.z]` heading).
- NEVER write Conventional Commit prefixes (`feat:`, `fix:`, etc.) into the changelog body. They classify the entry; they don't appear in the prose.
- NEVER include commit hashes, PR numbers, or author names in entries. The audience is end users, not git archaeologists.
- NEVER pick or suggest a version number. The release pipeline owns the bump.
- NEVER invoke a release script from this skill. Authoring is a separate step from releasing.
- If a CHANGELOG has changes in the range but every commit is omit-worthy by the style rules (test-only, type-only, internal refactor), leave its `[Unreleased]` body empty — do not invent entries.