Get the FREE Ultimate OpenClaw Setup Guide →

Changelog Generator

npx machina-cli add skill alirezarezvani/claude-skills/changelog-generator --openclaw
Files (1)
SKILL.md
13.5 KB

Changelog Generator

Tier: POWERFUL
Category: Engineering
Domain: Release Management / Documentation


Overview

Parse conventional commits, determine semantic version bumps, and generate structured changelogs in Keep a Changelog format. Supports monorepo changelogs, GitHub Releases integration, and separates user-facing from developer changelogs.

Core Capabilities

  • Conventional commit parsing — feat, fix, chore, docs, refactor, perf, test, build, ci
  • SemVer bump determination — breaking change → major, feat → minor, fix → patch
  • Keep a Changelog format — Added, Changed, Deprecated, Removed, Fixed, Security
  • Monorepo support — per-package changelogs with shared version strategy
  • GitHub/GitLab Releases — auto-create release with changelog body
  • Audience-aware output — user-facing (what changed) vs developer (why + technical details)

When to Use

  • Before every release to generate the CHANGELOG.md entry
  • Setting up automated changelog generation in CI
  • Converting git log into readable release notes for GitHub Releases
  • Maintaining monorepo changelogs for individual packages
  • Generating internal release notes for the engineering team

Conventional Commits Reference

<type>(<scope>): <description>

[optional body]

[optional footer(s)]

Types and SemVer impact

TypeChangelog sectionSemVer bump
featAddedminor
fixFixedpatch
perfChangedpatch
refactorChanged (internal)patch
docs— (omit or include)patch
chore— (omit)patch
test— (omit)patch
build— (omit)patch
ci— (omit)patch
securitySecuritypatch
deprecatedDeprecatedminor
removeRemovedmajor (if breaking)
BREAKING CHANGE: footer— (major bump)major
! after type— (major bump)major

Examples

feat(auth): add OAuth2 login with Google
fix(api): correct pagination offset calculation
feat!: rename /users endpoint to /accounts (BREAKING)
perf(db): add index on users.email column
security: patch XSS vulnerability in comment renderer
docs: update API reference for v2 endpoints

Changelog Generation Script

#!/usr/bin/env bash
# generate-changelog.sh — generate CHANGELOG entry for the latest release

set -euo pipefail

CURRENT_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${CURRENT_TAG}^" 2>/dev/null || echo "")
DATE=$(date +%Y-%m-%d)

if [ -z "$CURRENT_TAG" ]; then
  echo "No tags found. Create a tag first: git tag v1.0.0"
  exit 1
fi

RANGE="${PREVIOUS_TAG:+${PREVIOUS_TAG}..}${CURRENT_TAG}"
echo "Generating changelog for: $RANGE"

# Parse commits
ADDED=""
CHANGED=""
DEPRECATED=""
REMOVED=""
FIXED=""
SECURITY=""
BREAKING=""

while IFS= read -r line; do
  # Skip empty lines
  [ -z "$line" ] && continue
  
  # Detect type
  if [[ "$line" =~ ^feat(\([^)]+\))?\!:\ (.+)$ ]]; then
    desc="${BASH_REMATCH[2]}"
    BREAKING="${BREAKING}- **BREAKING** ${desc}\n"
    ADDED="${ADDED}- ${desc}\n"
  elif [[ "$line" =~ ^feat(\([^)]+\))?:\ (.+)$ ]]; then
    ADDED="${ADDED}- ${BASH_REMATCH[2]}\n"
  elif [[ "$line" =~ ^fix(\([^)]+\))?:\ (.+)$ ]]; then
    FIXED="${FIXED}- ${BASH_REMATCH[2]}\n"
  elif [[ "$line" =~ ^perf(\([^)]+\))?:\ (.+)$ ]]; then
    CHANGED="${CHANGED}- ${BASH_REMATCH[2]}\n"
  elif [[ "$line" =~ ^security(\([^)]+\))?:\ (.+)$ ]]; then
    SECURITY="${SECURITY}- ${BASH_REMATCH[2]}\n"
  elif [[ "$line" =~ ^deprecated(\([^)]+\))?:\ (.+)$ ]]; then
    DEPRECATED="${DEPRECATED}- ${BASH_REMATCH[2]}\n"
  elif [[ "$line" =~ ^remove(\([^)]+\))?:\ (.+)$ ]]; then
    REMOVED="${REMOVED}- ${BASH_REMATCH[2]}\n"
  elif [[ "$line" =~ ^refactor(\([^)]+\))?:\ (.+)$ ]]; then
    CHANGED="${CHANGED}- ${BASH_REMATCH[2]}\n"
  fi
done < <(git log "${RANGE}" --pretty=format:"%s" --no-merges)

# Build output
OUTPUT="## [${CURRENT_TAG}] - ${DATE}\n\n"

[ -n "$BREAKING" ] && OUTPUT="${OUTPUT}### ⚠ BREAKING CHANGES\n${BREAKING}\n"
[ -n "$SECURITY" ] && OUTPUT="${OUTPUT}### Security\n${SECURITY}\n"
[ -n "$ADDED" ] && OUTPUT="${OUTPUT}### Added\n${ADDED}\n"
[ -n "$CHANGED" ] && OUTPUT="${OUTPUT}### Changed\n${CHANGED}\n"
[ -n "$DEPRECATED" ] && OUTPUT="${OUTPUT}### Deprecated\n${DEPRECATED}\n"
[ -n "$REMOVED" ] && OUTPUT="${OUTPUT}### Removed\n${REMOVED}\n"
[ -n "$FIXED" ] && OUTPUT="${OUTPUT}### Fixed\n${FIXED}\n"

printf "$OUTPUT"

# Optionally prepend to CHANGELOG.md
if [ "${1:-}" = "--write" ]; then
  TEMP=$(mktemp)
  printf "$OUTPUT" > "$TEMP"
  
  if [ -f CHANGELOG.md ]; then
    # Insert after the first line (# Changelog header)
    head -n 1 CHANGELOG.md >> "$TEMP"
    echo "" >> "$TEMP"
    printf "$OUTPUT" >> "$TEMP"
    tail -n +2 CHANGELOG.md >> "$TEMP"
  else
    echo "# Changelog" > CHANGELOG.md
    echo "All notable changes to this project will be documented here." >> CHANGELOG.md
    echo "" >> CHANGELOG.md
    cat "$TEMP" >> CHANGELOG.md
  fi
  
  mv "$TEMP" CHANGELOG.md
  echo "✅ CHANGELOG.md updated"
fi

Python Changelog Generator (more robust)

#!/usr/bin/env python3
"""generate_changelog.py — parse conventional commits and emit Keep a Changelog"""

import subprocess
import re
import sys
from datetime import date
from dataclasses import dataclass, field
from typing import Optional

COMMIT_RE = re.compile(
    r"^(?P<type>feat|fix|perf|refactor|docs|test|chore|build|ci|security|deprecated|remove)"
    r"(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?: (?P<desc>.+)$"
)

SECTION_MAP = {
    "feat": "Added",
    "fix": "Fixed",
    "perf": "Changed",
    "refactor": "Changed",
    "security": "Security",
    "deprecated": "Deprecated",
    "remove": "Removed",
}

@dataclass
class Commit:
    type: str
    scope: Optional[str]
    breaking: bool
    desc: str
    body: str = ""
    sha: str = ""

@dataclass
class ChangelogEntry:
    version: str
    date: str
    added: list[str] = field(default_factory=list)
    changed: list[str] = field(default_factory=list)
    deprecated: list[str] = field(default_factory=list)
    removed: list[str] = field(default_factory=list)
    fixed: list[str] = field(default_factory=list)
    security: list[str] = field(default_factory=list)
    breaking: list[str] = field(default_factory=list)


def get_commits(from_tag: str, to_tag: str) -> list[Commit]:
    range_spec = f"{from_tag}..{to_tag}" if from_tag else to_tag
    result = subprocess.run(
        ["git", "log", range_spec, "--pretty=format:%H|%s|%b", "--no-merges"],
        capture_output=True, text=True, check=True
    )
    
    commits = []
    for line in result.stdout.splitlines():
        if not line.strip():
            continue
        parts = line.split("|", 2)
        sha = parts[0] if len(parts) > 0 else ""
        subject = parts[1] if len(parts) > 1 else ""
        body = parts[2] if len(parts) > 2 else ""
        
        m = COMMIT_RE.match(subject)
        if m:
            commits.append(Commit(
                type=m.group("type"),
                scope=m.group("scope"),
                breaking=m.group("breaking") == "!" or "BREAKING CHANGE" in body,
                desc=m.group("desc"),
                body=body,
                sha=sha[:8],
            ))
    
    return commits


def determine_bump(commits: list[Commit], current_version: str) -> str:
    parts = current_version.lstrip("v").split(".")
    major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
    
    has_breaking = any(c.breaking for c in commits)
    has_feat = any(c.type == "feat" for c in commits)
    
    if has_breaking:
        return f"v{major + 1}.0.0"
    elif has_feat:
        return f"v{major}.{minor + 1}.0"
    else:
        return f"v{major}.{minor}.{patch + 1}"


def build_entry(commits: list[Commit], version: str) -> ChangelogEntry:
    entry = ChangelogEntry(version=version, date=date.today().isoformat())
    
    for c in commits:
        scope_prefix = f"**{c.scope}**: " if c.scope else ""
        desc = f"{scope_prefix}{c.desc}"
        
        if c.breaking:
            entry.breaking.append(desc)
        
        section = SECTION_MAP.get(c.type)
        if section == "Added":
            entry.added.append(desc)
        elif section == "Fixed":
            entry.fixed.append(desc)
        elif section == "Changed":
            entry.changed.append(desc)
        elif section == "Security":
            entry.security.append(desc)
        elif section == "Deprecated":
            entry.deprecated.append(desc)
        elif section == "Removed":
            entry.removed.append(desc)
    
    return entry


def render_entry(entry: ChangelogEntry) -> str:
    lines = [f"## [{entry.version}] - {entry.date}", ""]
    
    sections = [
        ("⚠ BREAKING CHANGES", entry.breaking),
        ("Security", entry.security),
        ("Added", entry.added),
        ("Changed", entry.changed),
        ("Deprecated", entry.deprecated),
        ("Removed", entry.removed),
        ("Fixed", entry.fixed),
    ]
    
    for title, items in sections:
        if items:
            lines.append(f"### {title}")
            for item in items:
                lines.append(f"- {item}")
            lines.append("")
    
    return "\n".join(lines)


if __name__ == "__main__":
    tags = subprocess.run(
        ["git", "tag", "--sort=-version:refname"],
        capture_output=True, text=True
    ).stdout.splitlines()
    
    current_tag = tags[0] if tags else ""
    previous_tag = tags[1] if len(tags) > 1 else ""
    
    if not current_tag:
        print("No tags found. Create a tag first.")
        sys.exit(1)
    
    commits = get_commits(previous_tag, current_tag)
    entry = build_entry(commits, current_tag)
    print(render_entry(entry))

Monorepo Changelog Strategy

For repos with multiple packages (e.g., pnpm workspaces, nx, turborepo):

# packages/api/CHANGELOG.md     — API package only
# packages/ui/CHANGELOG.md      — UI package only
# CHANGELOG.md                  — Root (affects all)

# Filter commits by package path
git log v1.2.0..v1.3.0 --pretty=format:"%s" -- packages/api/

With Changesets (recommended for monorepos):

# Install changesets
pnpm add -D @changesets/cli
pnpm changeset init

# Developer workflow: create a changeset for each PR
pnpm changeset
# → prompts for: which packages changed, bump type, description

# On release branch: version all packages
pnpm changeset version

# Publish and create GitHub release
pnpm changeset publish

GitHub Releases Integration

#!/usr/bin/env bash
# create-github-release.sh

set -euo pipefail

VERSION=$(git describe --tags --abbrev=0)
NOTES=$(python3 generate_changelog.py)

# Using GitHub CLI
gh release create "$VERSION" \
  --title "Release $VERSION" \
  --notes "$NOTES" \
  --verify-tag

# Or via API
curl -s -X POST \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "Content-Type: application/json" \
  "https://api.github.com/repos/${REPO}/releases" \
  -d "$(jq -n \
    --arg tag "$VERSION" \
    --arg name "Release $VERSION" \
    --arg body "$NOTES" \
    '{tag_name: $tag, name: $name, body: $body, draft: false}')"

User-Facing vs Developer Changelog

User-facing (product changelog)

  • Plain language, no jargon
  • Focus on what changed, not how
  • Skip: refactor, test, chore, ci, docs
  • Include: feat, fix, security, perf (if user-visible)
## Version 2.3.0 — March 1, 2026

**New:** You can now log in with Google.
**Fixed:** Dashboard no longer freezes when loading large datasets.
**Improved:** Search results load 3x faster.

Developer changelog (CHANGELOG.md)

  • Technical details, scope, SemVer impact
  • Include all breaking changes with migration notes
  • Reference PR numbers and issue IDs
## [2.3.0] - 2026-03-01

### Added
- **auth**: OAuth2 Google login via passport-google (#234)
- **api**: GraphQL subscriptions for real-time updates (#241)

### Fixed
- **dashboard**: resolve infinite re-render on large datasets (closes #228)

### Performance
- **search**: switch from Elasticsearch to Typesense, P99 latency -67% (#239)

GitHub Actions — Automated Changelog CI

name: Release

on:
  push:
    tags: ['v*']

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for git log

      - name: Generate changelog
        id: changelog
        run: |
          NOTES=$(python3 scripts/generate_changelog.py)
          echo "notes<<EOF" >> $GITHUB_OUTPUT
          echo "$NOTES" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          body: ${{ steps.changelog.outputs.notes }}
          generate_release_notes: false

Common Pitfalls

  • --depth=1 in CI — git log needs full history; use fetch-depth: 0
  • Merge commits polluting log — always use --no-merges
  • No conventional commits discipline — enforce with commitlint in CI
  • Missing previous tag — handle first-release case (no previous tag)
  • Version in multiple places — single source of truth; read from git tag, not package.json

Best Practices

  1. commitlint in CI — enforce conventional commits before merge
  2. Tag before generating — tag the release commit first, then generate
  3. Separate user/dev changelog — product team wants plain English
  4. Keep a link section[2.3.0]: https://github.com/org/repo/compare/v2.2.0...v2.3.0
  5. Automate but review — generate in CI, human reviews before publish

Source

git clone https://github.com/alirezarezvani/claude-skills/blob/main/engineering/changelog-generator/SKILL.mdView on GitHub

Overview

Parses conventional commits to determine semantic version bumps and outputs structured changelogs in Keep a Changelog format. It supports monorepo changelogs, GitHub/GitLab Releases integration, and separates user-facing notes from developer details.

How This Skill Works

It analyzes commit messages using conventional-commit types (feat, fix, docs, refactor, etc.), maps them to SemVer bumps (major/minor/patch), and assembles sections like Added, Changed, Deprecated, Removed, Fixed, and Security. For monorepos it can generate per-package changelogs with a shared version strategy and can auto-create release notes for GitHub or GitLab.

When to Use It

  • Before every release to generate the CHANGELOG.md entry
  • Setting up automated changelog generation in CI
  • Converting git log into readable release notes for GitHub Releases
  • Maintaining monorepo changelogs for individual packages
  • Generating internal release notes for the engineering team

Quick Start

  1. Step 1: Ensure your commits follow the conventional commits format (type(scope): description)
  2. Step 2: Run the changelog generation script for the latest tag (e.g., using git describe --tags)
  3. Step 3: Integrate the script into your CI and push the new tag to publish CHANGELOG.md

Best Practices

  • Enforce conventional commit formatting (type(scope): description) across the repo
  • Use per-package changelogs in a monorepo with a shared version strategy
  • Produce distinct user-facing and developer notes in the output
  • Document breaking changes, deprecations, and security fixes clearly
  • Integrate the generator into CI and tag releases to auto-publish changelogs

Example Use Cases

  • Generate CHANGELOG.md for v1.2.0 after a release
  • Maintain per-package changelogs in a mono-repo like packages/*
  • Auto-create GitHub release bodies from changelog entries
  • Provide user-facing notes alongside developer details for each release
  • Capture security advisories and deprecations in dedicated sections

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers