
Generate Release Notes
- 558 installs
- 4 repo stars
- Updated June 15, 2026
- donnfelker/donnfelker-plugin-marketplace
Generate release notes or a changelog by parsing conventional commit messages from git log history into categorized markdown.
About
A release-notes skill that reads conventional commits over a range and produces categorized, formatted markdown as release notes, a CHANGELOG entry, or both. A developer uses it to prepare release documentation from git history.
- Parses feat/fix/docs conventional commits into grouped categories
- Outputs to RELEASE_NOTES.md and/or CHANGELOG.md
Generate Release Notes by the numbers
- 558 all-time installs (skills.sh)
- Ranked #40 of 248 Release Management skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/donnfelker/donnfelker-plugin-marketplace --skill generate-release-notesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 558 |
|---|---|
| repo stars | ★ 4 |
| Last updated | June 15, 2026 |
| Repository | donnfelker/donnfelker-plugin-marketplace ↗ |
What it does
Generate release notes or a changelog by parsing conventional commit messages from git log history into categorized markdown.
Files
Release Notes Generator
Generate release notes and changelogs by parsing conventional commit messages from git log history. Commits following the Conventional Commits specification (feat:, fix:, docs:, etc.) are categorized, grouped, and formatted into professional markdown output.
Workflow
Step 1: Determine Output Type
Ask the user what they need:
- Release Notes — a single release summary written to
RELEASE_NOTES.md - Changelog — append to an existing
CHANGELOG.md(or create one) - Both — generate both files
Step 2: Determine Commit Range
Identify which commits to include. Two approaches:
Since last tag (default): The parser script automatically detects the latest tag and uses <tag>..HEAD when no range is provided. To confirm the range with the user before generating, run git describe --tags --abbrev=0 to display the latest tag.
Custom range: Accept a user-specified range like v1.0.0..v2.0.0 or v1.0.0..HEAD and pass it as the first argument to the parser script.
If no tags exist, ask the user for a range or default to all commits (the script handles this automatically).
Step 3: Parse Commits
Run the parser script to extract structured commit data. The ${CLAUDE_PLUGIN_ROOT} variable is set automatically by Claude Code when the plugin is installed:
bash ${CLAUDE_PLUGIN_ROOT}/skills/generate-release-notes/scripts/parse-commits.sh [<range>]The script outputs one block per commit with fields: COMMIT, TYPE, SCOPE, BREAKING, DESCRIPTION, BODY, FOOTERS.
Step 4: Categorize and Format
Group parsed commits by type into sections using this mapping:
| Type | Section Heading |
|---|---|
| feat | Features |
| fix | Bug Fixes |
| perf | Performance Improvements |
| refactor | Code Refactoring |
| docs | Documentation |
| style | Styles |
| test | Tests |
| build | Build System |
| ci | Continuous Integration |
| chore | Chores |
| revert | Reverts |
Ordering: List sections in the order shown above. Within each section, list entries alphabetically by scope (ungrouped entries last).
Breaking changes: Collect all commits with BREAKING:yes into a dedicated "BREAKING CHANGES" section at the top, regardless of their type. These also appear in their normal type section.
Scope grouping: When multiple commits share a scope, group them under a bold scope label.
Commits with type "other": Collect into an "Other Changes" section at the bottom. These are commits that do not follow the conventional commit format.
Step 5: Determine Version
If the user provides a version number, use it. Otherwise, suggest a version based on SemVer rules:
- Any
BREAKING:yescommit → suggest MAJOR bump - Any
featcommit → suggest MINOR bump - Only
fix,docs,style, etc. → suggest PATCH bump
Read the latest tag to determine the current version and calculate the next version. Present the suggestion and let the user confirm or override.
Step 6: Write Output
Release Notes → Write to RELEASE_NOTES.md (overwrite previous content).
Changelog → Prepend new version entry to CHANGELOG.md. If the file does not exist, create it with a header. Preserve all existing entries below the new one.
Consult references/formats.md for the exact output templates.
Step 7: Summary
After writing, display:
- Version number used
- Number of commits processed
- Breakdown by category (e.g., "3 features, 2 bug fixes, 1 breaking change")
- File(s) written
Handling Edge Cases
No conventional commits found: Warn the user that no parseable commits were found. Offer to list raw commit messages instead.
Mixed conventional and non-conventional: Process conventional commits normally, list non-conventional commits under "Other Changes."
Empty range: If the range produces no commits, inform the user and suggest checking the range.
Monorepo with scopes: When scopes map to packages/modules, offer to group by scope as top-level sections instead of by type.
Additional Resources
Reference Files
For detailed output format templates and examples, consult:
- `references/formats.md` — Complete output templates for release notes and changelog, with examples of each section type
Scripts
- `scripts/parse-commits.sh` — Parses git log into structured commit data. Accepts an optional range argument. Handles conventional commit parsing, scope extraction, breaking change detection, and footer parsing.
Output Format Templates
Release Notes Format
Use the following template for RELEASE_NOTES.md:
# Release Notes — v{VERSION}
**Release Date:** {YYYY-MM-DD}
{One-paragraph summary of the release highlighting the most significant changes.}
## BREAKING CHANGES
{Only include this section if there are breaking changes.}
- **{scope}:** {description} — {migration note from BREAKING CHANGE footer or body}
## Features
- **{scope}:** {description} ({short-hash})
- {description (no scope)} ({short-hash})
## Bug Fixes
- **{scope}:** {description} ({short-hash})
## Performance Improvements
- **{scope}:** {description} ({short-hash})
## Documentation
- **{scope}:** {description} ({short-hash})
{Continue with remaining non-empty sections in standard order...}
---
**Full Changelog:** {link to compare view if remote URL available, e.g., https://github.com/org/repo/compare/v1.0.0...v2.0.0}Release Notes Rules
1. Include a brief summary paragraph at the top describing the release theme 2. Only include sections that have entries — omit empty sections entirely 3. Use the 7-character short hash in parentheses for each entry 4. When a scope is present, bold it and follow with a colon 5. Breaking changes appear both in the dedicated section AND in their type section 6. In the breaking changes section, include migration context from the BREAKING CHANGE footer or commit body 7. Link issue/PR references from Refs: footers as #number 8. End with a full changelog comparison link if a git remote URL is available
Changelog Format
Use the following template for entries prepended to CHANGELOG.md:
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/).
## [{VERSION}] — {YYYY-MM-DD}
### BREAKING CHANGES
- **{scope}:** {description} — {migration note}
### Added
- **{scope}:** {description} ({short-hash})
### Fixed
- **{scope}:** {description} ({short-hash})
### Changed
- **{scope}:** {description} ({short-hash})
### Removed
- {description} ({short-hash})
### Other
- {description} ({short-hash})
[{VERSION}]: https://github.com/org/repo/compare/{PREV_TAG}...v{VERSION}Changelog Section Mapping
The changelog uses Keep a Changelog section names:
| Commit Type | Changelog Section |
|---|---|
| feat | Added |
| fix | Fixed |
| perf | Changed |
| refactor | Changed |
| docs | Changed |
| style | Changed |
| build | Changed |
| ci | Changed |
| test | Changed |
| chore | Other |
| revert | Changed (use Removed only when the revert undoes a previously added feature) |
| other | Other |
Changelog Rules
1. The file header (title + description) is only written when creating a new CHANGELOG.md 2. When appending to an existing file, insert the new version block after the header and before existing entries 3. Use Keep a Changelog section names (Added, Fixed, Changed, Removed, Other) 4. Only include sections that have entries 5. Include the comparison link at the bottom of each version block 6. Breaking changes appear in the dedicated section AND in their mapped section 7. Maintain reverse chronological order (newest version at top) 8. Preserve the [Unreleased] section if one exists in the file
Example: Complete Release Notes
# Release Notes — v2.1.0
**Release Date:** 2026-03-02
This release introduces Polish language support and fixes a critical rate
limiting issue on public API endpoints. One breaking change affects
authentication — see migration notes below.
## BREAKING CHANGES
- **auth:** Replace session-based auth with JWT tokens — Clients must update
to handle the token refresh flow. Session cookies are no longer issued or
accepted. See the migration guide in docs/auth-migration.md.
## Features
- **auth:** Replace session-based auth with JWT tokens (a1b2c3d)
- **lang:** Add Polish language support (e4f5g6h)
- Add dark mode toggle to settings page (i7j8k9l)
## Bug Fixes
- **api:** Add rate limiting to public endpoints (m0n1o2p)
- **http:** Prevent racing of concurrent requests (q3r4s5t)
## Documentation
- Correct spelling in CHANGELOG (u6v7w8x)
---
**Full Changelog:** https://github.com/org/repo/compare/v2.0.0...v2.1.0Example: Complete Changelog Entry
## [2.1.0] — 2026-03-02
### BREAKING CHANGES
- **auth:** Replace session-based auth with JWT tokens — Clients must update
to handle the token refresh flow. Session cookies are no longer issued or
accepted.
### Added
- **auth:** Replace session-based auth with JWT tokens (a1b2c3d)
- **lang:** Add Polish language support (e4f5g6h)
- Add dark mode toggle to settings page (i7j8k9l)
### Fixed
- **api:** Add rate limiting to public endpoints (m0n1o2p)
- **http:** Prevent racing of concurrent requests (q3r4s5t)
### Changed
- Correct spelling in CHANGELOG (u6v7w8x)
[2.1.0]: https://github.com/org/repo/compare/v2.0.0...v2.1.0Scope Grouping (Optional)
When multiple entries share a scope within a section, group them:
## Features
- **auth:**
- Replace session-based auth with JWT tokens (a1b2c3d)
- Add remember-me checkbox to login form (y9z0a1b)
- **lang:** Add Polish language support (e4f5g6h)
- Add dark mode toggle to settings page (i7j8k9l)Apply scope grouping when 3 or more entries share the same scope within a single section. For fewer entries, keep them as flat list items with bold scope prefix.
Git Remote URL Detection
To generate comparison links, detect the remote URL:
git remote get-url origin 2>/dev/nullConvert SSH URLs to HTTPS:
git@github.com:org/repo.git→https://github.com/org/repohttps://github.com/org/repo.git→https://github.com/org/repo
If no remote is configured, omit the comparison link.
#!/usr/bin/env bash
# parse-commits.sh — Parse conventional commits from git log into structured output
#
# Usage:
# ./parse-commits.sh [<range>]
#
# Arguments:
# <range> Optional git revision range (e.g., v1.0.0..HEAD, v1.0.0..v2.0.0)
# If omitted, defaults to <latest-tag>..HEAD
# If no tags exist, includes all commits
#
# Output format (one block per commit, separated by blank lines):
# COMMIT:<hash>
# TYPE:<type>
# SCOPE:<scope> (empty if no scope)
# BREAKING:<yes|no>
# DESCRIPTION:<description>
# BODY:<body> (empty if no body)
# FOOTERS:<footers> (empty if no footers)
# ---
set -euo pipefail
RANGE="${1:-}"
# Determine range if not provided
if [[ -z "$RANGE" ]]; then
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [[ -n "$LATEST_TAG" ]]; then
RANGE="${LATEST_TAG}..HEAD"
else
# No tags — include all commits
RANGE="HEAD"
fi
fi
# Regex patterns — stored in variables to avoid bash parsing issues with parentheses
COMMIT_RE='^([a-z]+)(\(([^)]*)\))?(!)?: (.+)$'
FOOTER_RE='^([A-Za-z-]+|BREAKING CHANGE)(: | #)(.*)$'
BREAKING_RE='^BREAKING[\ -]CHANGE'
# Process a single commit from its hash and message lines.
# Globals: COMMIT_RE, FOOTER_RE, BREAKING_RE (read-only)
# Arguments: None — reads HASH and LINES global variables
process_commit() {
local subject="${LINES[0]:-}"
local type scope bang desc body footers in_footer breaking start
# Parse conventional commit subject
if [[ "$subject" =~ $COMMIT_RE ]]; then
type="${BASH_REMATCH[1]}"
scope="${BASH_REMATCH[3]:-}"
bang="${BASH_REMATCH[4]:-}"
desc="${BASH_REMATCH[5]}"
else
# Not a conventional commit — classify as "other"
type="other"
scope=""
bang=""
desc="$subject"
fi
body=""
footers=""
in_footer=false
breaking="no"
if [[ "$bang" == "!" ]]; then
breaking="yes"
fi
# Process remaining lines (skip subject and blank line after it)
start=1
if [[ ${#LINES[@]} -gt 1 && -z "${LINES[1]:-}" ]]; then
start=2
fi
for ((i=start; i<${#LINES[@]}; i++)); do
local line="${LINES[$i]}"
# Check for footer pattern: "Token: value" or "Token #value" or "BREAKING CHANGE: value"
if [[ "$line" =~ $FOOTER_RE ]]; then
in_footer=true
fi
if $in_footer; then
if [[ -n "$footers" ]]; then
footers="${footers}\n${line}"
else
footers="$line"
fi
# Check for BREAKING CHANGE footer
if [[ "$line" =~ $BREAKING_RE ]]; then
breaking="yes"
fi
else
if [[ -n "$body" ]]; then
body="${body}\n${line}"
else
body="$line"
fi
fi
done
echo "COMMIT:${HASH}"
echo "TYPE:${type}"
echo "SCOPE:${scope}"
echo "BREAKING:${breaking}"
echo "DESCRIPTION:${desc}"
echo "BODY:${body}"
echo "FOOTERS:${footers}"
echo "---"
}
# Use a delimiter unlikely to appear in commit messages
DELIM="---COMMIT-BOUNDARY---"
# Exclude merge commits — release notes should reflect individual changes, not merge boundaries
# Use process substitution (< <(...)) instead of a pipe so the while loop runs in the current
# shell. This ensures the "last commit" fallback block can access HASH/LINES after the loop.
while IFS= read -r line; do
if [[ "$line" == "$DELIM" ]]; then
if [[ -n "${HASH:-}" ]]; then
process_commit
fi
# Reset for next commit
unset HASH
unset LINES
continue
fi
if [[ -z "${HASH:-}" ]]; then
HASH="$line"
LINES=()
else
LINES+=("$line")
fi
done < <(git log "$RANGE" --pretty=format:"%H%n%B%n${DELIM}" --no-merges)
# Process last commit if no trailing delimiter
if [[ -n "${HASH:-}" ]]; then
process_commit
fi