Skip to content
Back to skills

Release Notes

ASecurity

Generate release notes from git history

  • 7 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 26, 2026
developmentgobashgitapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned May 27, 2026

npx -y skills add jhlee0409/all-for-claudecode --skill release-notes --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Release Notes?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Release Notes
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/jhlee0409-release-notes/badge)](https://www.skillsdirectory.com/skills/jhlee0409-release-notes)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: afc:release-notes
description: "Generate release notes from git history"
argument-hint: "[v1.0.0..v2.0.0 | v2.0.0 | --post]"
allowed-tools:
  - Read
  - Bash
  - Glob
  - Grep
model: sonnet
---

# /afc:release-notes — Generate Release Notes

> Rewrites commit/PR history into user-facing release notes and optionally publishes to GitHub Releases.
> This is a **standalone utility** — not part of the auto pipeline.
> Does NOT modify local files (CHANGELOG updates are handled by `/afc:launch`).

## Arguments

- `$ARGUMENTS` — (optional) Version range and flags
  - `v2.3.0..v2.4.0` — specific tag range
  - `v2.4.0` — from that tag to HEAD
  - Not specified — auto-detect last tag to HEAD
  - `--post` — publish to GitHub Releases after preview (can combine with any range)

Parse the arguments:
1. Extract `--post` flag if present
2. Parse remaining as version range:
   - If contains `..`: split into `{from_tag}..{to_tag}`
   - If single version: use `{version}..HEAD`
   - If empty: auto-detect with `git describe --tags --abbrev=0`

## Git Context (auto-loaded)

!`git describe --tags --abbrev=0 2>/dev/null || echo "[NO_TAGS]"`
!`git log $(git describe --tags --abbrev=0 2>/dev/null || echo "")..HEAD --pretty=format:"%H %s" --no-merges 2>/dev/null || git log --pretty=format:"%H %s" --no-merges`

## Execution Steps

### 1. Determine Range

- If a tag is found, set `from_tag` to that tag, `to_tag` to HEAD
- If no tags exist, inform the user: "No tags found. Include all commits? (y/n)"
- If a `to_tag` is specified (not HEAD), verify it exists: `git rev-parse --verify {to_tag} 2>/dev/null`
- Determine the version label for the notes header:
  - If `to_tag` is a version tag: use it (e.g., `v2.4.0`)
  - If `to_tag` is HEAD: use "Unreleased" or the `from_tag` bumped (ask user)

### 2. Collect Raw Data

Run these commands to gather change context:

```bash
# Commit history (no merges)
git log {from_tag}..{to_tag} --pretty=format:"%H %s" --no-merges

# Merged PRs since the from_tag date
from_date=$(git log -1 --format=%aI {from_tag})
gh pr list --state merged --search "merged:>$from_date" --json number,title,author,labels,body --limit 100
```

If `gh` is not available or the repo has no remote, skip PR collection — proceed with git-only data.

### 3. Detect Breaking Changes

Search commit messages and PR titles for breaking change indicators:

- Patterns: `BREAKING`, `BREAKING CHANGE`, `!:` (conventional commits `feat!:`, `fix!:`)
- Also check PR labels for: `breaking`, `breaking-change`, `semver-major`

Flag any matches for the Breaking Changes section.

### 4. Categorize and Rewrite

Categorize each commit/PR into one of:

| Category | Conventional Commit Prefixes | Fallback (no prefix) |
|----------|------------------------------|----------------------|
| Breaking Changes | `!:` suffix, `BREAKING` | Label: `breaking` |
| New Features | `feat:` | Commit introduces new user-facing functionality |
| Bug Fixes | `fix:` | Commit fixes broken or incorrect behavior |
| Other Changes | `chore:`, `docs:`, `ci:`, `refactor:`, `perf:`, `test:`, `style:`, `build:` | Everything else |

**Fallback classification rule** — For commits without conventional prefixes, read the commit message semantically. Classify based on what the commit actually does: did it introduce new user-facing functionality (New Features)? Did it fix broken behavior (Bug Fixes)? Or is it a maintenance/improvement change (Other Changes)? Do not match individual words — understand the commit's purpose.

The word "add" in "add a new API endpoint" indicates a feature. The same word in "add missing test coverage" indicates a test (Other Changes). Context determines classification.

**Rewriting rules** — transform each entry from developer-speak to user-facing language:

1. Remove conventional commit prefixes (`feat:`, `fix(scope):`, etc.)
2. Rewrite in terms of **what the user experiences**, not what the developer changed
   - Bad: "Refactor ThemeProvider to use context API"
   - Good: "Improved theme switching reliability"
   - Bad: "Fix race condition in afc-state.sh"
   - Good: "Fixed an issue where pipeline state could become corrupted during concurrent execution"
3. Merge related commits into a single entry when they address the same feature/fix
4. Include PR number references where available: `(#42)`
5. For breaking changes, add a brief migration note after the description

### 5. Collect Contributor Info

Build a contributors section:

```bash
# Get commit authors with their commit counts
git log {from_tag}..{to_tag} --format="%aN" | sort | uniq -c | sort -rn

# Resolve repo identity for GitHub username lookup
gh repo view --json owner,name --jq '"\(.owner.login)/\(.name)"'

# Map git authors to GitHub usernames via commit SHAs
git log {from_tag}..{to_tag} --format="%H" | head -100 | while read sha; do
  gh api "repos/{owner}/{repo}/commits/$sha" --jq '.author.login // empty' 2>/dev/null
done | sort -u
```

For each contributor:
- Try to resolve GitHub username via the commit SHA lookup above, or from PR author data collected in step 2
- List their PR numbers if available
- Fall back to git author name if no GitHub username found
- If `gh` is not available, skip username resolution entirely — use git author names

### 6. Compose Release Notes

Assemble the final release notes in this format:

```markdown
# {version}

{2-3 sentence summary: what is the most important thing in this release? Written for end users.}

## Breaking Changes

- {description + migration guide}

## New Features

- {user-facing description} (#{pr_number})

## Bug Fixes

- {user-facing description} (#{pr_number})

## Other Changes

- {description} (#{pr_number})

## Contributors

{contributor list with @mentions and PR numbers}
```

**Format rules**:
- Omit empty sections entirely (if no breaking changes, skip that section)
- Breaking Changes section always comes first when present
- Each entry is a single bullet point, max 2 sentences
- Summary paragraph should highlight the top 1-2 changes
- Contributors section: `@username (#42, #45)` format, or `Name (#42)` if no GitHub username

### 7. Preview Output

Display the complete release notes to the user in the console.

Print a summary:
```
Release notes generated
├─ Version: {version}
├─ Range: {from_tag}..{to_tag}
├─ Commits: {N}
├─ PRs referenced: {N}
├─ Breaking changes: {count or "none"}
├─ Contributors: {N}
└─ --post: {will publish / preview only}
```

### 8. Publish to GitHub Releases (if --post)

If `--post` flag is present:

1. Determine the tag name:
   - If `to_tag` is a specific tag: use it
   - If `to_tag` is HEAD: ask the user what tag to create (suggest next version)

2. Ask user to confirm using AskUserQuestion:
   - **Post as-is** — Publish the GitHub Release immediately
   - **Post as draft** — Create as draft release (can review and publish later from GitHub)
   - **Edit first** — Let me modify the notes before posting
   - **Cancel** — Do not post

3. On approval, write to a temp file and publish:
   ```bash
   tmp_file=$(mktemp)
   cat > "$tmp_file" << 'NOTES_EOF'
   {release notes content}
   NOTES_EOF
   # Add --draft if user chose "Post as draft"
   gh release create {tag} --notes-file "$tmp_file" --title "{version}" [--draft]
   rm -f "$tmp_file"
   ```

4. Print result:
   ```
   GitHub Release published
   ├─ Tag: {tag}
   ├─ Title: {version}
   ├─ Status: {published / draft}
   └─ URL: {release URL from gh output}
   ```

If `--post` is not present, print:
```
To publish these notes as a GitHub Release, run again with --post flag.
```

## Notes

- **Read-only by default**: Without `--post`, this command only outputs to console. No files are created or modified.
- **Complements `/afc:launch`**: Use `launch` for local artifact generation (CHANGELOG, README). Use `release-notes` for GitHub Release publishing.
- **Conventional Commits aware**: Projects using conventional commits get better categorization. Projects without them still get reasonable results via heuristic matching.
- **Contributor attribution**: Best effort. GitHub username resolution requires `gh` CLI and repository access.
- **User confirmation required**: `--post` always asks for explicit approval before publishing to GitHub.
- **Verify after publishing**: After `gh release create` completes, verify success by checking the exit code and output URL. If it fails, report the error and suggest manual creation.
- **Idempotent**: Running without `--post` is safe to repeat. With `--post`, GitHub Releases for existing tags will fail (GitHub does not allow duplicate release tags).

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.