StackPractices
beginner By Mathias Paulenko

Changelog Template

A structured changelog template following Keep a Changelog conventions for tracking project releases.

Topics: devops

Overview

A changelog is a curated, chronologically ordered list of notable changes for each version of a project. It answers the question every consumer asks before upgrading: what will break or improve if I update? This template follows the conventions of Keep a Changelog — the de facto standard — combined with Semantic Versioning for version numbering.

When to Use

  • You maintain a library, framework, or application with versioned releases
  • You need to communicate breaking changes to consumers — pair it with the API Deprecation Notice Template when sunsetting functionality
  • You want to automate release note generation from commits

Template

# 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 functionality

### Changed
- Changes in existing functionality

### Deprecated
- Soon-to-be removed functionality

### Removed
- Now removed functionality

### Fixed
- Bug fixes

### Security
- Vulnerability fixes

## [1.0.0] - YYYY-MM-DD

### Added
- Initial release with core functionality

Filled Example

# Changelog

All notable changes to payment-service are documented here.
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
- Partial refund endpoint: `POST /payments/:id/refund-partial` (#456)
- Metadata field in payment responses for custom data (#449)

### Changed
- `amount` field now returns as integer in cents instead of decimal (#451)

## [2.5.0] - 2026-07-15

### Added
- Passkey authentication via WebAuthn (#412)
- CSV data export from the admin panel (#389)

### Changed
- Full-text search is ~3x faster after adding GIN indexes in PostgreSQL (#402)
- Default pagination limit raised from 50 to 200 results (#407)

### Fixed
- 500 error when creating an account with an email longer than 50 chars (#401)
- Push notifications not delivered on iOS 17.4+ (#405)

### Security
- Updated `jsonwebtoken` 2.1.0 → 2.3.0 to patch CVE-2026-11482 (#410)
- Added origin validation to prevent CSRF on payment endpoints (#413)

### Deprecated
- API v1 endpoints — removed in v3.0.0, migrate to `/v2/` paths (#390)
- `user.name` field — use `user.firstName` and `user.lastName` instead (#395)

## [2.4.0] - 2026-06-15

### Added
- Real-time metrics dashboard (#367)
- Webhooks for payment events (#372)

### Fixed
- Race condition in concurrent payment processing (#358)
- Memory leak in the notification worker (#361)

Categories Explained

Keep a Changelog defines exactly six categories — stick to them and don’t invent new ones:

CategoryUse For
AddedNew functionality
ChangedChanges to existing functionality
DeprecatedFunctionality marked for removal in a future release
RemovedFunctionality removed in this release
FixedBug fixes
SecuritySecurity vulnerability fixes

Breaking changes aren’t a separate category — they go under Deprecated (still works, scheduled for removal) or Removed (already gone). Flag them clearly in the entry text and repeat them at the top of your release notes. The API Deprecation Notice Template covers the announcement side of this.

Semver and Changelog

Version changeChangelog entryExample
MAJOR (X.0.0)Breaking changes under Removed/Changed## [3.0.0] - Removed: XML response format
MINOR (X.Y.0)New functionality under Added## [2.5.0] - Added: CSV export
PATCH (X.Y.Z)Bug fixes under Fixed/Security## [2.4.1] - Fixed: Login redirect loop

Release Workflow

Cutting a release becomes mechanical once entries land in Unreleased as PRs merge:

  1. During development — every user-facing PR adds one line under ## [Unreleased] in the right category, with the issue or PR number.
  2. At release time — rename ## [Unreleased] to ## [X.Y.Z] - YYYY-MM-DD and open a fresh empty ## [Unreleased] section on top.
  3. Tag and publish — git tag v2.5.0, push, and create the GitHub/GitLab release pointing at the new changelog section.
  4. Verify — a CI check that fails when a tag has no matching ## [X.Y.Z] heading catches forgotten updates.
flowchart diagram: Merge PRs<br/>update Unreleased

Enforcing It in CI

A changelog that depends on memory dies quietly. Two cheap CI checks keep it honest:

# .github/workflows/changelog.yml — require a changelog entry on every PR
name: Changelog
on: [pull_request]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - name: Verify CHANGELOG.md was touched
        run: |
          git diff --name-only origin/main...HEAD | grep -q '^CHANGELOG.md$' \
            || (echo 'PR is missing a changelog entry' && exit 1)

For release-time validation, grep the tag: grep -q "## \[$(git describe --tags)\]" CHANGELOG.md fails the pipeline when the version heading is missing. Teams that hate forcing every PR to touch the same file should look at fragment-based workflows (towncrier, changesets) — each PR adds its own file under changelog.d/ and merge conflicts disappear.

Automation

Generating entries from commit history saves effort but always needs a human review pass before publishing. Pick the tool that matches your repo shape:

ToolBest forHow it works
changesetsJS/TS monoreposPer-change Markdown fragments compiled at release
release-pleaseGitHub projectsOpens a release PR that accumulates entries from conventional commits
semantic-releaseFully automated pipelinesDetermines version + generates notes from commits, no human step
auto-changelogSimple projectsRenders git log output into changelog format

All of them rely on Conventional Commits (feat:, fix:, BREAKING CHANGE: footers) to classify entries. If commit messages are inconsistent, no tool will rescue the output — enforce the convention in CI or via pre-commit hooks before automating.

GitHub Releases can also auto-generate notes from merged PRs and labels (breaking, feature, bug). Treat its output as a draft: export to CHANGELOG.md and edit for clarity rather than pasting it raw.

A changesets fragment looks like this — one file per change, compiled into the changelog at release time:

---
"@myorg/payment-service": minor
---

Add partial refund endpoint `POST /payments/:id/refund-partial`

Changelog vs Release Notes

They answer different questions and shouldn’t be the same document:

ChangelogRelease notes
AudienceDevelopers consuming your projectUsers and stakeholders of a product
GranularityEvery notable change, one line eachHighlights of a single release
LifecycleAppend-only file that lives in the repoPublished per release (GitHub Releases, blog)
FormatKeep a Changelog categoriesNarrative, screenshots, upgrade guides

The changelog is the source of truth; release notes are the story you tell from it. The Release Notes Template covers the announcement format.

Migrating an Existing Project

Starting a changelog on a project that never had one:

  1. Don’t backfill history. Start from the next release with a note at the top: This changelog starts at v2.5.0. For earlier history see git log.
  2. Seed the current Unreleased section from what is already merged: git log --oneline v2.4.0..main gives you the raw list to curate.
  3. Keep it honest — only include what users can observe. Skip refactors, chores, and version bumps.
  4. Announce the convention in CONTRIBUTING.md so contributors know a changelog line is expected per PR — the Contributing Guide template has a spot for this.

What Works

  • Date every release using ISO 8601 format (YYYY-MM-DD)
  • Group changes by category within each release
  • Link to issues/PRs so users can trace the discussion behind a change
  • Mention breaking changes prominently — first under Deprecated/Removed, then in the release announcement
  • Keep an Unreleased section at the top for upcoming changes
  • Write entries for humans — a changelog isn’t a git commit log
  • One line per change — details belong in the linked issue or migration guide

Common Mistakes

  • Commit log dumps: a changelog is curated; raw git log output is unreadable to users
  • Missing dates: every released version needs its date
  • Forgetting Unreleased: without it, entries get written at release time from memory and things get lost
  • Mixing categories: security fixes go under Security, not Fixed — users scan that category first
  • Inventing categories: Breaking, Improved, Misc aren’t in the spec and fragment tooling that parses the file
  • Editing old entries: never rewrite a published version’s section — corrections go in a new entry
  • No issue references: (#123) is cheap context that users genuinely use

Writing a Good Entry

Every entry is a single line that answers “what changed for me?” Write it from the reader’s perspective, not the implementer’s.

Weak entryBetter entry
fix(auth): resolve token issueFixed: expired SSO sessions sent users into a login redirect loop (#220)
add exportAdded: CSV export for dashboard charts, up to 50k rows (#234)
update depsSecurity: updated jsonwebtoken to 9.0.2 to patch CVE-2026-0987 (#410)

Rules that keep entries readable:

  • Let the category heading carry the type — the entry text describes the effect.
  • Describe user-visible impact, not the code path: “rate limit headers missing from 429 responses”, not “fix header middleware”.
  • Always reference the issue or PR so the entry is traceable.
  • For breaking changes, say what to do instead: Removed: XML response format — use JSON (see migration guide).

Troubleshooting

  • Changelog drifts from git tags: a release shipped but CHANGELOG.md has no matching section. Add a CI check that extracts the tag version and greps for ## [X.Y.Z] — fail the pipeline when it’s missing.
  • Unreleased is empty at release time: entries were never written during development. Require a changelog line in the PR template, or use fragment files (changelog.d/ style, like towncrier or changesets) so each PR carries its own entry file.
  • Generated entries read like commit spam: tools reproduce feat(auth): thing verbatim. Review generated output before publishing — entries should describe user-visible impact, not implementation detail.
  • Merge conflicts on every release: everyone appends to the same ## [Unreleased] block. Fragment files compiled at release time eliminate the shared line.
  • Breaking changes get buried: users scroll past them under Fixed. Put deprecations and removals in their own categories and repeat them in the release notes and migration guide.

Variants

Library changelog

For libraries and SDKs, include migration code snippets next to breaking changes and link to a migration guide. Note the minimum language or runtime version each release requires — consumers need it to plan upgrades.

Internal service changelog

For internal services, add deployment context: which environments received the release, which feature flags were toggled, and links to dashboards. Operators need to trace what changed in their environment, not just in the code.

Monorepo changelog

Maintain one changelog per package (packages/foo/CHANGELOG.md) with tools like changesets that handle per-package versioning. A single root changelog for a monorepo mixes unrelated changes and confuses consumers of individual packages.

Key Takeaways

  • Follow the six Keep a Changelog categories exactly; date every release in ISO 8601.
  • Update Unreleased per PR — cutting a release then means renaming a heading, not archaeology.
  • Automate generation with conventional commits, but keep a human review pass.
  • Breaking changes belong under Deprecated/Removed and deserve a callout in release notes.

Further Reading

Frequently Asked Questions

What format should a changelog follow?

Use the Keep a Changelog format with the six standard categories: Added, Changed, Deprecated, Removed, Fixed, and Security. Date every release in ISO 8601 and link to issues or PRs.

Should I include every commit in the changelog?

No. A changelog is curated, not a raw git log. Include only changes users can see: new functionality, fixes, breaking changes, and security patches. Internal refactors and chores don't belong.

What is semantic versioning?

Semantic Versioning (SemVer) uses the MAJOR.MINOR.PATCH format: bump MAJOR for breaking changes, MINOR for new functionality, and PATCH for bug fixes. The semver.org spec defines the rules.

Should the Unreleased section be empty between releases?

No — that's the point. Entries accumulate under Unreleased as PRs merge, so at release time you only rename the heading to the version and date, then open a fresh empty one.

How do I handle pre-release versions?

Use semver pre-release labels: 1.0.0-alpha.1, 1.0.0-beta.2, 1.0.0-rc.1. Give each its own dated section and mark it clearly as pre-release so users know it may be unstable.

Should I keep old entries forever?

Yes. The changelog is a historical record — someone upgrading from v1.0 to v3.0 needs every intermediate change. If the file gets unwieldy, split by major version (CHANGELOG-v1.md, CHANGELOG-v2.md) with an index.

How do I keep the changelog consistent across contributors?

Put a changelog line in the PR template and validate it in CI. For larger teams, fragment files compiled at release time (towncrier, changesets) prevent merge conflicts and enforce the format mechanically instead of relying on convention.

Can the changelog be fully automated?

Partially. Tools like release-please or semantic-release generate solid drafts from conventional commits, but the output still needs a human pass — generated entries describe commits, not user impact. Automate the mechanics, keep the editorial review.