Skip to content

Go-Native Changelog Generator Tool

Authors
Matt Cockayne
Date
1 April 2026
Status
IMPLEMENTED

Overview

GTB currently depends on git-cliff (Rust) to generate CHANGELOG.md at build time. This creates an external dependency that must be installed in CI and doesn't integrate with Go's tool directive. The changelog is a build artifact generated from the full git history โ€” it is not committed to the repository.

This spec replaces git-cliff with a pure-Go changelog generator that:

  • Uses go-git to read commit history directly (no shell commands)
  • Parses conventional commits and groups by version tag
  • Outputs markdown compatible with pkg/changelog.Parse()
  • Lives in cmd/changelog/ and is declared as a Go tool directive
  • Runs via go generate โ€” self-contained with zero external dependencies
  • Replaces git-cliff in both the GTB CLI and generated project templates

Performance

Benchmarked on this repository (276 commits, 49 tags): - Tag resolution: 6ms - Commit iteration and bucketing: 9ms - Total: 14ms

Extrapolated: ~40ยตs per commit. A 100,000-commit repo would complete in ~4s โ€” acceptable for a build-time tool.


Design

Command

cmd/changelog/
โ”œโ”€โ”€ main.go         โ† Cobra root command
โ””โ”€โ”€ generate.go     โ† generate subcommand
# Generate full changelog to stdout
changelog generate

# Generate to file (creates or updates existing)
changelog generate --output CHANGELOG.md

# Generate since a specific tag
changelog generate --since v1.5.0 --output CHANGELOG.md

# Limit to N most recent releases
changelog generate --releases 10 --output CHANGELOG.md

# Include non-conventional commits
changelog generate --include-all --output CHANGELOG.md

The generate subcommand structure allows future additions (e.g. validate, diff) without breaking the CLI contract.

Tool Directive

In go.mod:

tool gitlab.com/phpboyscout/go-tool-base/cmd/changelog

In go:generate:

//go:generate go tool changelog generate --output assets/CHANGELOG.md

Conventional Commit Parsing

The tool parses commit messages following the conventional commits spec:

type(scope): description

BREAKING CHANGE: explanation

Groups: - feat โ†’ Features - fix โ†’ Bug Fixes - perf โ†’ Performance Improvements - refactor, docs, style, chore โ†’ Other - test, ci โ†’ Skipped (not included in changelog) - BREAKING CHANGE footer โ†’ Breaking Changes section

Output Format

Matches the format pkg/changelog.Parse() expects:

# v1.2.3

### Features
* **chat:** add conversation persistence
* **output:** add spinner and progress helpers

### Bug Fixes
* **generator:** fix GitLab CI file selection

### Breaking Changes
* BREAKING CHANGE: remove deprecated GetRepo method

Implementation

The core logic:

  1. Open the repository via git.PlainOpen(".")
  2. Resolve all version tags (semver pattern v*.*.*)
  3. Iterate commits in committer-time order via repo.Log()
  4. Bucket each commit into its release (the tag whose commit it precedes)
  5. Parse each commit message for type, scope, description, and breaking changes
  6. Sort releases by semver (newest first)
  7. Format as markdown and write to output

Library Package

The core logic lives in pkg/changelog/generate.go (not in cmd/) so it can be reused:

// GenerateFromRepo reads git history and produces a full CHANGELOG.md string.
func GenerateFromRepo(repoPath string, opts ...GenerateOption) (string, error)

type GenerateOption func(*generateConfig)
func WithSinceTag(tag string) GenerateOption
func WithMaxReleases(n int) GenerateOption

The cmd/changelog/ is a thin CLI wrapper around this function.


Integration

GTB CLI

Replace the git-cliff copy directive with:

//go:generate go tool changelog --output assets/CHANGELOG.md

Generated Projects

The skeleton generator emits the same go:generate directive in pkg/cmd/root/generate.go. The tool directive is added to the generated go.mod.

CI Workflows

Remove the git-cliff action from generated GitHub Actions and GitLab CI. The changelog is generated by go generate which runs as part of the normal build.


Files

File Action
cmd/changelog/ CREATE โ€” CLI tool entry point
pkg/changelog/generate.go CREATE โ€” core generation logic
pkg/changelog/generate_test.go CREATE โ€” tests
go.mod MODIFY โ€” add tool directive
internal/cmd/root/generate.go MODIFY โ€” replace copy with go tool
internal/generator/assets/skeleton/ MODIFY โ€” remove cliff.toml, update CI templates
internal/generator/skeleton.go MODIFY โ€” update generate.go template

Resolved Questions

  1. Unreleased commits: Yes โ€” commits after the latest tag appear under an "Unreleased" heading.
  2. Config file: Flags and env vars only for v1. No config file.
  3. Merge commits: Filtered out by default. Only conventional commits are parsed by default. --include-all flag includes non-conventional commits under "Other".
  4. Existing file handling: If CHANGELOG.md already exists, parse it, determine which releases are already documented, and only generate missing releases. Preserves manually edited content. If the file doesn't exist, generate from scratch.