# Release notes generator

## Purpose

Generate release notes from git history between two refs. Parse conventional commits, group changes by type, include PR links, and output formatted markdown suitable for a changelog or GitHub release.

### Returns

A markdown document with these sections, in this order:

- A one-to-two-sentence plain-language summary at the top
- `## Breaking changes` (omitted if none)
- `## Features`
- `## Bug fixes`
- `## Performance` (omitted if none)
- `## Documentation` (omitted if none)
- `## Other` (chore/build/ci/style/test entries)
- `## Contributors` (de-duplicated authors)

Plus structured metadata for downstream skills: a flag indicating whether the release contains breaking changes, the contributors list as an array, and the resolved start/end refs. A composing skill (such as a release announcer or a status-page updater) can read these fields without parsing the markdown.

## When to use

- The user asks to generate release notes
- The user asks "what changed since the last release?"
- The user asks to write a changelog entry
- The user is preparing a release and needs a summary of changes

## When not to use

- The project is not a git repository, or its canonical changelog lives somewhere other than git (Jira releases, Linear cycles, Notion). Hand off to the user instead.
- The repo uses squash-merge with no PR linking and no conventional commits. The output will be a list of generic subject lines with no useful structure.
- You don't have read access to the affected repo. Without the git log, the rest of the skill is guesswork.
- The hosting platform is not GitHub and the user wants PR links. The skill assumes GitHub for PR URLs. Adapt the URL format yourself for GitLab, Forgejo, or Azure DevOps before invoking.

## Steps

### 0. Confirm the repo

Before any git command, confirm the working directory is a git repository. Run `git rev-parse --show-toplevel`. If the command fails, ask the user for the path to the repo. If the user mentioned a specific repo by name and your cwd is something else, double-check before continuing.

### 1. Determine the range

Figure out which commits to include:

- If the user specifies two refs (for example, "from v1.2.0 to v1.3.0"), use those: `git log v1.2.0..v1.3.0`.
- If the user specifies one ref ("since v1.2.0"), use that as the start and HEAD as the end: `git log v1.2.0..HEAD`.
- If the user says nothing about refs, find the most recent tag with `git describe --tags --abbrev=0`. Use that as the start and HEAD as the end.
- If there are no tags at all, ask the user: "No tags found. What commit or date should I start from?"

Pull the commit list with a single batched call that captures hash, subject, body, and author in one pass:

```bash
git log <start>..<end> --max-count=500 --format='---COMMIT---%n%H%n%s%n%aN%n%b'
```

The `--max-count=500` cap avoids stuffing thousands of commits into context on long-range queries. If the range exceeds 500 commits, warn the user and ask whether to proceed with the cap or narrow the range.

Tell the user what range you're using: "Generating release notes for v1.2.0..HEAD (14 commits)."

### 2. Parse each commit

Split the batched output by the `---COMMIT---` delimiter and read each block. From each commit, pull out:

- The type (the conventional commit prefix: feat, fix, chore, docs, refactor, perf, test, build, ci, style). If the commit doesn't use conventional commits, infer from the message content. "Fix login crash on empty password" is a `fix`. "Add dark mode support" is a `feat`.
- The scope (the parenthetical scope if present, so `feat(auth)` has scope `auth`).
- The description (the rest of the subject line after the type and scope).
- Whether it's a breaking change. Look for `BREAKING CHANGE:` anywhere in the body or a `!` after the type (`feat!:`).
- The PR number. Look for patterns like `(#123)` in the subject. Many merge workflows append this automatically.

You already have the body from the batched git log call in step 1. Don't run additional `git log <hash>` calls per commit; that's an unbounded fan-out.

### 3. Look up PR details (optional, GitHub-only)

If PR numbers were found and `gh` is available, fetch additional context. Use `--json url` so you don't have to construct URLs by hand:

```bash
gh pr view <number> --json title,author,labels,url
```

Use the PR title if it's more descriptive than the commit message. Record the author login for the contributors list. Use the `url` field directly when generating the markdown link in step 6 (do not construct `https://github.com/org/repo/pull/N` yourself: the URL constructor breaks on GitLab/Forgejo/Azure DevOps and the `gh pr view` URL is correct for whatever GitHub host the repo lives on, including Enterprise).

If `gh` is not available or the command fails, skip this step. Release notes still work without PR metadata.

### 4. Group by category

Sort the parsed commits into these groups, in this order:

1. Breaking changes: any commit marked breaking, regardless of type
2. Features: commits with type `feat`
3. Bug fixes: commits with type `fix`
4. Performance: commits with type `perf`
5. Documentation: commits with type `docs`
6. Other: everything else (chore, refactor, build, ci, style, test)

Within each group, sort commits alphabetically by scope (if scopes exist), then by description. Omit any group that has zero commits.

### 5. Build the contributors list

Collect unique authors. The batched git log already captured `%aN` per commit, so de-duplicate from that list:

```bash
git log <start>..<end> --format="%aN" | sort -u
```

If PR author logins were fetched in step 3, prefer those (they're usually GitHub usernames and can be linked). Otherwise, use the git author names.

### 6. Generate the release notes

Output the release notes in this format:

```markdown
# [version or range]

[One to two sentence summary of the release. Mention the most
significant change first. If there are breaking changes, call
that out here.]

## Breaking changes

- **scope:** description of the breaking change and what users
  need to do to migrate ([#PR](url))

## Features

- **scope:** description ([#PR](url))

## Bug fixes

- **scope:** description ([#PR](url))

## Contributors

@username, @username, @username
```

Formatting rules:

- If a commit has a scope, bold it at the start: `**auth:** add token refresh`. (This bold-and-colon format inside the output template is intentional, since it's the format the user actually wants in their CHANGELOG.)
- If a commit has no scope, just start with the description.
- If a PR number exists, link it at the end using the URL from `gh pr view --json url`.
- If no PR number exists, omit the link. Do not invent one.
- The summary at the top is for end users. "You can now export reports as PDF" is better than "feat: add PDF export to report controller."
- Capitalize the first word of each description.
- Do not end descriptions with periods.

### 7. Present and ask (human actions)

Show the full release notes and offer next steps. Frame them so the user understands these are actions they need to approve, not actions you've already taken:

```
Here are the release notes for v1.2.0..v1.3.0.

[release notes]

Want me to (each requires your approval):
1. Copy this to your clipboard
2. Write it to CHANGELOG.md (prepended, not overwritten)
3. Create a GitHub release with this content
4. Edit anything before any of the above
```

If the user approves writing to a file, use your file-writing tool. Don't use a shell heredoc: a `cat << 'EOF'` block silently truncates if the release notes contain a line that's exactly `EOF`, which is rare but ugly when it happens.

If the user approves a GitHub release, write the notes to a temporary file with the file-writing tool, then run:

```bash
gh release create <tag> --title "<version>" --notes-file /tmp/release-notes.md
```

If the user approves writing to `CHANGELOG.md`, prepend the new entry to the top of the file (below any existing header). Do not overwrite existing entries.

## Important rules

- NEVER invent PR numbers or links. If you can't find a PR reference, omit it.
- NEVER include merge commits (commits starting with "Merge ") unless they contain meaningful information.
- NEVER include commits that are purely version bumps or tag updates unless the user asks for them.
- NEVER take a writing or release action without explicit approval. Step 7's options describe human actions the user has to confirm.
- Before presenting the notes, scan for sensitive content: email addresses, internal hostnames, ticket numbers from internal trackers, anything that looks like a credential or API key. Flag any matches to the user before writing to a public location or creating a GitHub release.
- If the git history doesn't use conventional commits, do your best to categorize by reading the messages. Put anything ambiguous in "Other".
- Keep descriptions concise. One line per change. If a commit message is 200 characters, shorten it to the key point.
- The summary paragraph is for end users, not developers.
