Die Oberfläche ist lokalisiert; Skill-Beschreibungen und -Dokumente werden per KI automatisch in Ihre Sprache übersetzt. Code, Befehle, Pfade und Fachbegriffe bleiben im Original, maßgeblich ist das Dokument des Autors.
Skills entdecken
Entwicklung · SKILL.md

dev-loop-changelog-generator

dev-loop-changelog-generator

Use when the user asks about generating changelogs, release notes, conventional commits to changelogs, automated release notes, or CHANGELOG.md management. Do NOT use for: git commit messages (dev-loop-git-workflow), or code review (dev-loop-code-review).

Independent GitHub snapshot · Automated classification · Original documentation governs

Skill-Dokumentation

GitHub

name: dev-loop-changelog-generator description: > Use when the user asks about generating changelogs, release notes, conventional commits to changelogs, automated release notes, or CHANGELOG.md management. Do NOT use for: git commit messages (dev-loop-git-workflow), or code review (dev-loop-code-review). version: "2.0.0" author: "j4flmao" license: "MIT" compatibility: claude-code: true cursor: true codex: true windsurf: true tags: [dev-loop, changelog, release-notes, conventional-commits]

Changelog Generator

Purpose

Generate structured, human-readable changelogs and release notes from conventional commit history, GitHub release data, or JIRA release versions. Automated changelogs reduce manual release effort, enforce consistent formatting, and provide clear communication to users and stakeholders.

Agent Protocol

Trigger

Exact user phrases: "generate changelog", "release notes", "conventional changelog", "CHANGELOG.md", "auto-changelog", "git-cliff", "standard-version", "release-please", "semantic-release", "generate release notes".

Input Context

  • Commit message convention (Conventional Commits, Angular convention, custom)
  • Output format (Keep a Changelog, custom Markdown, GitHub release, Slack message)
  • Tool preference (git-cliff, standard-version, release-please, semantic-release, auto-changelog)
  • Source of truth (git log, GitHub releases, JIRA)
  • Release cadence (continuous delivery, scheduled releases, hotfixes)
  • Versioning strategy (semver, calver, date-based, custom)
  • Monorepo structure (single changelog vs per-package)

Output Artifact

Generated CHANGELOG.md or release notes document with categorized, versioned entries.

Completion Criteria

  • Tool selected and configured
  • Conventional commit convention established
  • Parsing rules defined (commit scopes, types, breaking changes)
  • Changelog generated for current version
  • Unreleased section included for upcoming changes
  • Breaking changes highlighted prominently
  • Links to commits, issues, PRs included
  • Per-package changelogs (if monorepo)
  • CI automated generation configured
  • Version bump integrated with changelog

Max Response Length

200 lines.

Framework/Methodology

Changelog Tool Decision Tree

What is the project setup?
├── Single package, standard git → git-cliff
│   Configurable, TOML config, conventional commits
├── Monorepo (lerna, nx, turborepo) → release-please
│   Per-package changelogs, GitHub releases, PR-based
├── npm package, simple → standard-version
│   npm-aware, version bump + changelog + tag
├── Full CI/CD pipeline → semantic-release
│   Automated release from CI, npm/GitHub/GCR publish
└── Custom needs → custom script with conventional-changelog
    Flexible, integrate with any workflow

Conventional Commit Format

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

[optional body]

[optional footer(s)]

Types:

  • feat: A new feature (triggers minor version bump)
  • fix: A bug fix (triggers patch version bump)
  • docs: Documentation only changes
  • style: Code style changes (formatting, missing semicolons)
  • refactor: Code refactoring (neither fix nor feature)
  • perf: Performance improvement
  • test: Adding or correcting tests
  • chore: Maintenance, build, dependencies
  • ci: CI/CD configuration changes
  • build: Build system changes

Breaking changes: Append ! after type/scope or add BREAKING CHANGE: in footer (triggers major version bump).

Workflow

Step 1: Configure git-cliff

# cliff.toml
[remote.github]
owner = "myorg"
repo = "myapp"

[changelog]
header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n"
body = """
{% for group, commits in commits | group_by(attribute="group") %}
  ### {{ group | upper_first }}
  {% for commit in commits %}
    - {% if commit.scope %}**{{ commit.scope }}:** {% endif %}{{ commit.message | upper_first }}
      {% if commit.breaking %}[**breaking**]{% endif %}
  {% endfor %}
{% endfor %}
"""
trim = true
postprocess = """
sed -i 's/- \[**breaking**\]/⚠️ **BREAKING CHANGE:**/' CHANGELOG.md
"""

[git]
conventional_commits = true
commit_preprocessors = [
  { pattern = "\\(#(\\d+)\\)", replace = "([#${1}](https://github.com/myorg/myapp/pull/${1}))" },
]

# Group definitions
[[git.parsers]]
message = "^feat"
group = "Features"

[[git.parsers]]
message = "^fix"
group = "Bug Fixes"

[[git.parsers]]
message = "^perf"
group = "Performance"

[[git.parsers]]
message = "^refactor"
group = "Refactoring"

[[git.parsers]]
message = "^docs"
group = "Documentation"

[[git.parsers]]
message = "^test"
group = "Tests"

[[git.parsers]]
message = ".*"
group = "Other"

Step 2: Generate Changelog

# git-cliff: generate from last tag to HEAD
git cliff -o CHANGELOG.md

# Generate for specific tag range
git cliff --tag 1.0.0..HEAD -o CHANGELOG.md

# Preview unreleased changes
git cliff --unreleased -o CHANGELOG.md

# Unreleased with specific date
git cliff --unreleased --date-strategy "current"

Step 3: Manual Changelog Structure (Keep a Changelog)

# Changelog

All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- New feature description ([#42](https://github.com/org/repo/pull/42))

### Changed
- Updated dependency from v1 to v2

### Fixed
- Bug fix description ([#41](https://github.com/org/repo/pull/41))

## [2.0.0] - 2026-05-15

### ⚠️ BREAKING CHANGES
- Removed deprecated `/old-endpoint` API. Use `/new-endpoint` instead.
- Minimum Node.js version raised to 18.

### Added
- New dashboard component with real-time updates
- Dark mode support for all pages

### Fixed
- Memory leak in websocket connection
- Incorrect sorting in data table

[2.0.0]: https://github.com/org/repo/releases/tag/v2.0.0

Step 4: CI Automation

# .github/workflows/release.yml
name: Release
on:
  push:
    branches: [main]

permissions:
  contents: write
  pull-requests: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Generate changelog
        uses: orhun/git-cliff-action@v3
        with:
          config: cliff.toml
          args: --latest --output CHANGELOG.md

      - name: Create Release
        uses: softprops/action-gh-release@v1
        with:
          body_path: CHANGELOG.md
          generate_release_notes: true

Step 5: Monorepo Changelogs with release-please

# .github/workflows/release-please.yml
name: Release Please
on:
  push:
    branches: [main]

jobs:
  release-please:
    runs-on: ubuntu-latest
    steps:
      - uses: googleapis/release-please-action@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          release-type: node
          monorepo-tags: true
          packages:
            packages/core: {}
            packages/cli: {}
            packages/ui: {}

Step 6: Changelog with Custom Sections

// scripts/generate-changelog.ts
import conventionalChangelog from 'conventional-changelog';
import { writeFileSync } from 'fs';

const config: conventionalChangelog.Options = {
  preset: {
    name: 'conventionalcommits',
    types: [
      { type: 'feat', section: '🚀 Features' },
      { type: 'fix', section: '🐛 Bug Fixes' },
      { type: 'perf', section: '⚡ Performance' },
      { type: 'refactor', section: '♻️ Refactoring' },
      { type: 'docs', section: '📚 Documentation' },
      { type: 'test', section: '✅ Tests' },
      { type: 'chore', section: '🔧 Chores' },
      { type: 'ci', section: '👷 CI/CD' },
    ],
  },
  releaseCount: 10,
  outputUnreleased: true,
  lernaPackage: null,
};

conventionalChangelog(config)
  .pipe(process.stdout)
  .on('data', (chunk: Buffer) => {
    writeFileSync('CHANGELOG.md', chunk.toString());
  });

Common Pitfalls

PitfallDescriptionPrevention
Non-conventional commitsCommits that don't match patterns are skippedEnforce commitlint in CI, educate team
No version tagsTool can't find previous release to diff fromAlways tag releases with v-prefix semver
Monorepo tag collisionMultiple packages creating same tagUse package-scoped tags: pkg@1.0.0
Breaking changes buriedUsers miss critical upgrade infoBREAKING CHANGE: in commit footer always
Changelog in wrong formatDoesn't follow Keep a ChangelogValidate with changelog-lint
Generated file committed staleOutdated if generated manuallyCI enforces fresh generation on release
Ignoring dependenciesDependency updates need visibilityGroup dep updates in "Dependencies" section
No unreleased sectionChanges between releases are invisibleAlways keep Unreleased section up to date

Best Practices

PracticeRationale
Follow Conventional CommitsAutomated tooling can parse reliably
Keep a Changelog formatIndustry standard, human-parseable
Always include Unreleased sectionTransparency, makes release prep trivial
Highlight breaking changes prominentlyUsers need to know before upgrading
Link to PRs/issuesTraceability from changelog to source
Group by typeUsers scan for "Features" or "Bug Fixes"
Generate at release time, not per-commitOne consistent document per version
Pin tool versionAvoid unexpected formatting changes
Include migration notes for breaking changesReduced support burden
Automate in CIManual changelogs get skipped or stale
Validate commit messages with commitlintCatch non-conventional commits before merge

Templates

Keep a Changelog Template

# Changelog

## [Unreleased]

### Added
### Changed
### Deprecated
### Removed
### Fixed
### Security

## [MAJOR.MINOR.PATCH] - YYYY-MM-DD

[MAJOR.MINOR.PATCH]: https://github.com/org/repo/releases/tag/vMAJOR.MINOR.PATCH

Release Notes Template (PR-based)

## v1.2.0 (June 1, 2026)

### Highlights
- [Summary of major changes this release]

### New Features
- #143: Add dark mode support
- #142: Export data as CSV

### Bug Fixes
- #140: Fix memory leak in WebSocket connection
- #139: Correct sort order in data table

### Breaking Changes
- #141: Remove deprecated v1 API endpoints (migration guide: [link])

### Contributors
- @user1, @user2

References

  • references/changelog-generator-advanced.md — Changelog Generator Advanced Topics
  • references/changelog-generator-fundamentals.md — Changelog Generator Fundamentals
  • references/conventional-commits.md — Conventional Commits Reference
  • references/release-workflow.md — Release Workflow Reference

Handoff

Hand off to dev-loop-git-workflow for version tagging strategy. Hand off to dev-loop-code-review for PR-based changelog entries.

Implementation Patterns

Changelog Generator (Python)

import subprocess
import re
from typing import List, Dict, Optional
from datetime import datetime

class ChangelogGenerator:
    def __init__(self, repo_path: str = "."):
        self.repo_path = repo_path
        self.commit_types = {
            "feat": "Features",
            "fix": "Bug Fixes",
            "docs": "Documentation",
            "refactor": "Refactoring",
            "perf": "Performance Improvements",
            "test": "Tests",
            "chore": "Chores",
            "ci": "CI/CD",
        }

    def generate_from_tags(self, from_tag: Optional[str] = None, to_ref: str = "HEAD") -> str:
        if not from_tag:
            tags = self._get_tags()
            if len(tags) < 1:
                tag1 = self._get_initial_commit()
                tag2 = None
                if not tag1:
                    return "No tags or commits found."
            else:
                tag1 = tags[-1] if len(tags) > 0 else None
                tag2 = tags[-2] if len(tags) > 1 else None
        else:
            tag1 = from_tag
            tag2 = None

        scope = f"{tag2}..{tag1}" if tag2 else tag1 if tag1 else to_ref
        commits = self._get_commits(scope)

        if not commits and tag1:
            commits = self._get_commits(tag1)

        return self._format_changelog(tag1 or "Unreleased", commits)

    def _get_tags(self) -> List[str]:
        try:
            result = subprocess.run(
                ["git", "tag", "--sort=-creatordate"],
                capture_output=True, text=True, cwd=self.repo_path
            )
            tags = [t.strip() for t in result.stdout.split("\n") if t.strip()]
            return [t for t in tags if re.match(r"^v?\d+\.\d+\.\d+", t)]
        except subprocess.CalledProcessError:
            return []

    def _get_initial_commit(self) -> Optional[str]:
        try:
            result = subprocess.run(
                ["git", "rev-list", "--max-parents=0", "HEAD"],
                capture_output=True, text=True, cwd=self.repo_path
            )
            return result.stdout.strip() or None
        except subprocess.CalledProcessError:
            return None

    def _get_commits(self, scope: str) -> List[Dict]:
        try:
            result = subprocess.run(
                ["git", "log", scope, "--oneline", "--format=%H|%s|%an|%ad", "--date=short"],
                capture_output=True, text=True, cwd=self.repo_path
            )
            commits = []
            for line in result.stdout.strip().split("\n"):
                if not line:
                    continue
                parts = line.split("|", 3)
                if len(parts) < 2:
                    continue
                hash_val = parts[0][:7]
                msg = parts[1]
                author = parts[2] if len(parts) > 2 else ""
                date = parts[3] if len(parts) > 3 else ""

                parsed = self._parse_conventional_commit(msg)
                commits.append({
                    "hash": hash_val,
                    "message": msg,
                    "raw_message": parsed["message"],
                    "type": parsed["type"],
                    "scope": parsed["scope"],
                    "breaking": parsed["breaking"],
                    "author": author,
                    "date": date,
                })
            return commits
        except subprocess.CalledProcessError:
            return []

    def _parse_conventional_commit(self, message: str) -> Dict:
        pattern = r"^(feat|fix|docs|refactor|perf|test|chore|ci|build|style|revert)(\((.+?)\))?(!)?:\s*(.+)"
        match = re.match(pattern, message)
        if match:
            return {
                "type": match.group(1),
                "scope": match.group(3),
                "breaking": match.group(4) == "!" or "BREAKING CHANGE" in message,
                "message": match.group(5),
            }
        return {"type": "other", "scope": None, "breaking": False, "message": message}

    def _format_changelog(self, version: str, commits: List[Dict]) -> str:
        grouped = {}
        for c in commits:
            ctype = c["type"]
            section = self.commit_types.get(ctype, "Other")
            if c["breaking"]:
                section = "Breaking Changes"
            if section not in grouped:
                grouped[section] = []
            grouped[section].append(c)

        section_order = ["Breaking Changes", "Features", "Bug Fixes", "Performance Improvements",
                         "Refactoring", "Documentation", "Tests", "CI/CD", "Chores", "Other"]

        date_str = datetime.now().strftime("%Y-%m-%d")
        lines = [f"## [{version}] - {date_str}\n"]

        for section in section_order:
            if section in grouped and grouped[section]:
                lines.append(f"### {section}")
                for c in grouped[section]:
                    scope = f"**{c['scope']}:** " if c["scope"] else ""
                    lines.append(f"- {scope}{c['raw_message']} ({c['hash']})")
                lines.append("")

        if not any(c["breaking"] for c in commits):
            pass

        return "\n".join(lines)

    def generate_unreleased(self) -> str:
        try:
            result = subprocess.run(
                ["git", "describe", "--tags", "--abbrev=0"],
                capture_output=True, text=True, cwd=self.repo_path
            )
            latest_tag = result.stdout.strip() if result.returncode == 0 else None
        except subprocess.CalledProcessError:
            latest_tag = None

        return self.generate_from_tags(from_tag=latest_tag)

Architecture Decision Trees

Version Bump Strategy

Given the commits since last release:
├── Has any commit with BREAKING CHANGE or feat!
│   └── MAJOR bump (1.0.0 → 2.0.0)
│       └── Update all consumers for breaking API changes
│
├── Has any commit with feat (and no breaking)
│   └── MINOR bump (1.0.0 → 1.1.0)
│       └── New features, backward compatible
│
├── Has any commit with fix, perf, refactor
│   └── PATCH bump (1.0.0 → 1.0.1)
│       └── Bug fixes, performance improvements
│
├── Has only docs, test, chore, ci
│   └── No version bump needed (or PATCH if desired)
│       └── No user-facing changes
│
└── Pre-release (before stable 1.0.0)
    └── Use 0.x.y — no automated semver, manual decision

Changelog Section Selection

What type is the commit?
├── feat → "Features" section
├── fix → "Bug Fixes" section
├── perf → "Performance Improvements" section
├── refactor → "Refactoring" section
├── docs → "Documentation" section
├── test → "Tests" section
├── chore → "Chores" section
├── ci → "CI/CD" section
├── BREAKING CHANGE → "Breaking Changes" section
└── anything else → "Other" section

Production Considerations

  • Release automation pipeline: Generate changelog automatically as part of the release CI pipeline. Tag the release commit, generate changelog, create GitHub Release with changelog content. Never manual.
  • Changelog linting: Validate changelog format in CI using changelog-lint or similar. Ensure all required sections exist. Verify links to releases are valid.
  • Monorepo changelogs: Use package-scoped changelogs (one per package) plus an overall monorepo changelog. Tools like lerna-changelog or changesets handle this well.
  • Dependency update visibility: Group automated dependency updates (Dependabot, Renovate) into a "Dependencies" section. Prevents noise from hiding in "Chores" or "Other".

Anti-Patterns

Anti-PatternWhy It FailsCorrect Approach
Manual changelog writingGets skipped or outdatedAuto-generate from commits
Only changelog at releaseUsers want to see what's comingAlways maintain Unreleased section
No version tagsCan't generate diff-based changelogGit tag every release with semver
Ignoring Conventional CommitsChangelog is a mess of random messagesEnforce commit convention in CI
Single changelog for monorepoHard to see per-package changesPer-package changelogs + summary
Breaking changes not highlightedUsers upgrade and things breakBreaking changes as first section
No migration notesUsers don't know how to migrateInclude migration guide for breaking changes
Stale generated fileDoesn't reflect current stateGenerate on CI, not committed manually

Performance Optimization

  • Limit commit history depth: When generating changelog for large repos, limit to last 1000 commits. Use git log --max-count=1000 to avoid slow full-history traversal.
  • Cache tag-to-commit mapping: Cache the tag-to-commit hash mapping. Avoids repeated git operations when rendering multiple changelogs (e.g., per-package in monorepo).
  • Incremental generation: Only process commits since the last generated changelog entry. Append new entries at the top of the Unreleased section.
  • Parallel monorepo generation: Generate per-package changelogs in parallel. Use a thread pool for repos with 10+ packages. Recombine into a summary changelog.
Gespeicherte Skills