Automated Changelog Generation: From Commit History to Release Notes

Build automated changelog pipelines from git commit history using conventional commits, conventional-changelog, and semantic-release. Learn parsing, templating, and production patterns.

published: reading time: 12 min read updated: March 31, 2026

Introduction

A changelog is the bridge between your code and your users. It answers the question every stakeholder asks: “What changed?” Yet most teams write changelogs manually — a tedious, error-prone process that falls behind within weeks. Automated changelog generation solves this by parsing your git history and producing structured release notes.

The secret sauce is conventional commits. When every commit follows a structured format, machines can categorize, filter, and format them into beautiful changelogs. Tools like conventional-changelog, auto, and semantic-release turn your commit log into a living document that updates with every release.

This post covers the architecture of automated changelog systems, parsing strategies, template customization, and production-hardened patterns. If your team ships software regularly, this is infrastructure you can’t afford to skip.

When to Use / When Not to Use

Use automated changelogs when:

  • You release software on a regular cadence (weekly, biweekly, monthly)
  • You use conventional commits or another structured commit format
  • You have external users or consumers who need to know what changed
  • You publish packages to npm, PyPI, or other registries
  • You want to reduce release overhead

Skip them when:

  • You release rarely (quarterly or less) and manual notes are manageable
  • Your commit history is chaotic with no structure to parse
  • You’re building internal tools where release notes aren’t consumed
  • Your team isn’t ready to adopt commit conventions

Core Concepts

Automated changelog generation works in three phases:

  1. Parse — Read git log and extract structured data from commit messages
  2. Categorize — Group commits by type (features, fixes, breaking changes, etc.)
  3. Render — Apply a template to produce formatted markdown

The input is your git log. The output is a CHANGELOG.md file. The engine is a parser-template pipeline.

flowchart LR
    A[Git Repository] --> B[git log]
    B --> C[Commit Parser]
    C --> D{Categorize}
    D -->|feat| E[Features]
    D -->|fix| F[Bug Fixes]
    D -->|BREAKING| G[Breaking Changes]
    D -->|other| H[Other]
    E --> I[Template Engine]
    F --> I
    G --> I
    H --> I
    I --> J[CHANGELOG.md]

Architecture and Flow Diagram

sequenceDiagram
    participant Rel as Release Trigger
    participant Git as Git Log
    participant Parser as Commit Parser
    participant Filter as Commit Filter
    participant Sort as Sorter
    participant Tmpl as Template Engine
    participant FS as CHANGELOG.md

    Rel->>Git: Get commits since last tag
    Git-->>Rel: Raw commit list
    Rel->>Parser: Parse each commit
    Parser->>Parser: Extract type, scope, subject
    Parser->>Parser: Detect breaking changes
    Parser-->>Rel: Structured commits
    Rel->>Filter: Remove chore/docs/test commits
    Filter-->>Rel: Release-worthy commits
    Rel->>Sort: Group by type, sort by scope
    Sort-->>Rel: Categorized commits
    Rel->>Tmpl: Render with template
    Tmpl->>Tmpl: Apply markdown template
    Tmpl-->>Rel: Formatted changelog
    Rel->>FS: Prepend to CHANGELOG.md

Step-by-Step Guide

1. Install conventional-changelog

npm install --save-dev conventional-changelog-cli

2. Generate Your First Changelog

npx conventional-changelog -p angular -i CHANGELOG.md -s

The -p angular preset uses Angular’s commit convention. The -i flag specifies the input/output file, and -s enables in-place editing.

3. Configure as an npm Script

{
  "scripts": {
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0"
  }
}

The -r 0 flag regenerates the entire changelog from all commits. Without it, only new commits since the last run are added.

4. Customize the Template

Create a custom template for your project’s branding:

// changelog-template.js
module.exports = function (options) {
  return {
    writerOpts: {
      transform: (commit, context) => {
        // Skip certain commits
        if (commit.type === "chore" || commit.type === "test") {
          return null;
        }
        // Add link to commit
        commit.shortHash = commit.hash.substring(0, 7);
        commit.link = `${context.host}/${context.owner}/${context.repository}/commit/${commit.hash}`;
        return commit;
      },
      groupBy: "type",
      commitGroupsSort: (a, b) => {
        const order = [
          "Breaking Changes",
          "Features",
          "Bug Fixes",
          "Performance",
          "Documentation",
        ];
        return order.indexOf(a.title) - order.indexOf(b.title);
      },
      commitsSort: ["scope", "subject"],
      noteGroupsSort: "title",
      notesSort: "text",
    },
  };
};

5. Integrate with semantic-release

For fully automated releases with changelog generation:

npm install --save-dev semantic-release @semantic-release/changelog @semantic-release/git

Create .releaserc.json:

{
  "branches": ["main"],
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    [
      "@semantic-release/changelog",
      {
        "changelogFile": "CHANGELOG.md"
      }
    ],
    [
      "@semantic-release/git",
      {
        "assets": ["CHANGELOG.md", "package.json"],
        "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
      }
    ],
    "@semantic-release/github"
  ]
}

6. Add to CI Pipeline

# .github/workflows/release.yml
name: Release
on:
  push:
    branches: [main]
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx semantic-release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Production Failure Scenarios + Mitigations

ScenarioImpactMitigation
Parser fails on non-conventional commitChangelog generation stopsUse --skip-unparsed flag or pre-filter commits
Duplicate entries in changelogConfusing release notesUse -s flag for in-place deduplication
Missing git tagsCan’t determine commit rangeEnsure tags are pushed; use git fetch --tags in CI
Template rendering errorBroken changelog formatTest template locally before CI deployment
Large repository historySlow changelog generationUse shallow clone with sufficient depth; cache parsed results
Merge commits polluting logDuplicate feature entriesConfigure parser to skip merge commits or use --first-parent

Trade-offs

AspectAutomated ChangelogManual Changelog
AccuracyHigh (directly from commits)Variable (human error)
EffortZero after setupOngoing per release
CustomizationTemplate-boundUnlimited
Commit discipline requiredYes (conventional commits)No
Breaking change detectionAutomaticManual review
Historical migrationCan regenerate from scratchMust write manually

Implementation Snippets

Generate changelog for specific version range:

# From last tag to HEAD
npx conventional-changelog -p angular -i CHANGELOG.md -s

# Specific range
npx conventional-changelog -p angular -i CHANGELOG.md -s -r 1

# All history
npx conventional-changelog -p angular -i CHANGELOG.md -s -r 0

Custom parser configuration:

// conventional-changelog.config.js
module.exports = {
  preset: "angular",
  tagPrefix: "v",
  skip: {
    bump: false,
    changelog: false,
    commit: false,
    tag: false,
  },
  gitRawCommitsOpts: {
    merges: null, // Exclude merge commits
  },
};

Programmatic API usage:

import conventionalChangelog from "conventional-changelog";
import { createWriteStream } from "fs";

const stream = conventionalChangelog({
  preset: "angular",
  tagPrefix: "v",
});

stream.pipe(createWriteStream("CHANGELOG.md", { flags: "a" }));
stream.on("end", () => console.log("Changelog updated"));

Filter commits by scope:

# Only include commits with 'api' scope
npx conventional-changelog -p angular -i CHANGELOG.md -s \
  --config ./changelog-config.js
// changelog-config.js
module.exports = {
  writerOpts: {
    transform: (commit) => {
      if (commit.scope === "api") {
        return commit;
      }
      return false; // Filter out non-api commits
    },
  },
};

Observability Checklist

  • Logs: Log number of commits processed per release
  • Metrics: Track changelog generation time and file size growth
  • Alerts: Alert when changelog generation fails in CI
  • Dashboards: Monitor release frequency and commit type distribution
  • Traces: Trace commit → changelog entry → release note for audit

Security/Compliance Notes

  • Changelogs are public documents — never include internal ticket numbers that expose security vulnerabilities
  • For regulated software, ensure changelogs include compliance-relevant changes (security patches, data handling updates)
  • Link to CVE IDs when fixing security issues: fix(auth): patch XSS vulnerability (CVE-2024-1234)
  • Consider generating separate internal and external changelogs for sensitive projects

Common Pitfalls / Anti-Patterns

Anti-PatternWhy It’s BadFix
Including every commitNoise overwhelms signalFilter out chore, test, style commits
No deduplicationSame change listed multiple timesUse -s flag for in-place editing
Ignoring merge commitsDuplicate entries in changelogConfigure parser to skip or squash merges
Manual edits to CHANGELOG.mdGets overwritten on next runAdd CHANGELOG.md to .gitignore for auto-generation or use semantic-release git plugin
Not testing templatesBroken formatting in productionRender template locally before CI
Forgetting tag prefixCan’t find version tagsSet tagPrefix: 'v' in config

Quick Recap Checklist

  • Install conventional-changelog-cli
  • Configure preset matching your commit convention
  • Add npm script for changelog generation
  • Set up custom template if needed
  • Integrate with semantic-release for full automation
  • Add CI workflow for automated releases
  • Configure commit filtering to reduce noise
  • Test changelog generation locally before deploying

Interview Q&A

How does conventional-changelog determine which commits to include?

It reads git log between the current HEAD and the last git tag. The tag is determined by parsing version numbers from existing tags. Commits are then filtered by type (features, fixes, breaking changes) and formatted using the selected preset template.

What's the difference between conventional-changelog and semantic-release?

conventional-changelog only generates the changelog file. semantic-release is a full release automation tool that analyzes commits, determines the next version, generates the changelog, creates git tags, and publishes to registries. semantic-release uses conventional-changelog under the hood.

How do you handle a changelog that needs both automated and manual entries?

Use the prepend mode of conventional-changelog. Generate the automated section, then manually add entries above it. Alternatively, use semantic-release with a custom template that includes a manual section, or maintain a separate RELEASE_NOTES.md for human-written content alongside the auto-generated changelog.

Why might your automated changelog miss commits after a squash merge?

Squash merges create a single new commit that doesn't preserve the original commit messages' structure. The squashed commit message may not follow conventional commit format, causing the parser to skip it. Configure your repository to use rebase merges or ensure the squash message follows the convention.

How do you generate a changelog for a monorepo with multiple packages?

Use lerna-changelog or changesets which are designed for monorepos. They track changes per package and generate individual changelogs. Alternatively, configure conventional-changelog with custom filters to group commits by package scope.

Extended Architecture Diagram

flowchart LR
    subgraph "Input"
        A[Git Repository] --> B[git log]
        B --> C[Raw Commits]
    end

    subgraph "Parsing"
        C --> D[Commit Parser]
        D --> E{Conventional Format?}
        E -->|Yes| F[Extract type, scope, subject]
        E -->|No| G[Skip or flag]
        F --> H[Detect BREAKING CHANGE]
    end

    subgraph "Categorization"
        H --> I{Group by Type}
        I -->|feat| J[Features]
        I -->|fix| K[Bug Fixes]
        I -->|perf| L[Performance]
        I -->|BREAKING| M[Breaking Changes]
        I -->|docs/chore/test| N[Excluded]
    end

    subgraph "Rendering"
        J --> O[Template Engine]
        K --> O
        L --> O
        M --> O
        O --> P[Apply Markdown Template]
        P --> Q[CHANGELOG.md]
    end

Extended Production Failure Scenario

Missing Conventional Commits Causing Incomplete Changelog

A developer squash-merges a PR with 15 commits into a single commit message that doesn’t follow conventional format: “Merge pull request #42 from feature/auth”. The changelog generator processes this commit, finds no recognized type prefix, and skips it entirely. The release ships with new authentication features but the changelog shows zero features — only unrelated bug fixes from other commits. Users have no idea what changed.

Mitigation: Enforce conventional commits at the PR level with commitlint on the CI pipeline. Configure squash merge to preserve the PR title as the commit message, and require PR titles to follow the conventional format. Add a changelog validation step that fails if a release contains zero feature entries but has code changes.

Extended Trade-offs

Aspectconventional-changeloggit-cliffManual
AutomationFull — runs as CLI or libraryFull — single binary, config-drivenNone
CustomizationTemplate-based, JavaScript configTOML config, custom templates, regexUnlimited
Maintenancenpm dependency, regular updatesStandalone binary, minimal depsOngoing human effort
EcosystemNode.js native, semantic-release integrationLanguage-agnostic, works with any repoN/A
Learning curveMedium — JS config, template syntaxLow — TOML config is straightforwardN/A
Monorepo supportVia lerna-changelog or custom configBuilt-in with git-cliff groupsManual per-package

Implementation Snippet: git-cliff Configuration

# cliff.toml
[changelog]
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
body = """
{% if version %}\
    ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
    ## [Unreleased]
{% endif %}\
{% 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 }}\
    {% endfor %}
{% endfor %}\n
"""
footer = """
<!-- generated by git-cliff -->
"""
trim = true

[git]
conventional_commits = true
filter_unconventional = true
split_commits = false
commit_parsers = [
    { message = "^feat", group = "Features" },
    { message = "^fix", group = "Bug Fixes" },
    { message = "^perf", group = "Performance" },
    { message = "^docs", group = "Documentation" },
    { message = "^BREAKING", group = "Breaking Changes" },
    { message = "^chore\\(release\\)", skip = true },
    { message = "^chore", skip = true },
    { message = "^ci", skip = true },
    { message = "^test", skip = true },
]
protect_breaking_changes = true
filter_commits = true
tag_pattern = "v[0-9].*"
sort_commits = "newest"
# Generate changelog with git-cliff
git-cliff --output CHANGELOG.md

# Generate for specific tag range
git-cliff v1.0.0..v1.2.0 --output CHANGELOG.md

# Unreleased changes only
git-cliff --unreleased --output CHANGELOG.md

Resources

Category

Related Posts

Commit Message Conventions: Conventional Commits, Angular Style, and Semantic Commits

Master commit message conventions including Conventional Commits, Angular style, and semantic commits. Learn automated changelog generation, linting enforcement, and team-wide standards.

#git #version-control #conventional-commits

Automated Releases and Tagging

Automate Git releases with tags, release notes, GitHub Releases, and CI/CD integration for consistent, repeatable software delivery.

#git #version-control #automation

Automated Release Pipeline: From Git Commit to Production Deployment

Build a complete automated release pipeline with Git, CI/CD, semantic versioning, changelog generation, and zero-touch deployment. Hands-on tutorial for production-ready releases.

#git #version-control #ci-cd