Skip to content

Changelog

Package: pkg/changelog

Parses and generates conventional commit-based release notes into structured Go types. Includes a pure-Go changelog generator (cmd/changelog) that replaces external tools like git-cliff. Breaking changes are highlighted prominently so consumers can assess upgrade risk.


Types

Category

const (
    CategoryBreaking    Category = iota // breaking change requiring user action
    CategoryFeature                     // new feature
    CategoryFix                         // bug fix
    CategoryPerformance                 // performance improvement
    CategoryOther                       // refactor, docs, chore, etc.
)

Entry

A single change within a release:

type Entry struct {
    Category    Category
    Scope       string // e.g., "http", "chat" โ€” may be empty
    Description string
    Raw         string // original unparsed line
}

Release

Parsed changelog for a single version:

type Release struct {
    Version string   // e.g., "v1.5.0"
    Entries []Entry
}

Changelog

Parsed changelog across multiple releases:

type Changelog struct {
    FromVersion string
    ToVersion   string
    Releases    []Release
}

Parsing

cl := changelog.Parse(rawMarkdown)

Parse processes raw release notes markdown line-by-line:

  1. Splits into per-release sections by detecting # vX.Y.Z headers.
  2. Within each release, detects section headers (### Features, ### Bug Fixes, ### BREAKING CHANGES, etc.) to determine the current category.
  3. Parses bullet points (* **scope:** description or * description) into Entry structs.
  4. Detects BREAKING CHANGE: footers within entry descriptions and reclassifies them as CategoryBreaking.

Releases are returned oldest-first.

Supported Section Headers

Header Category
### BREAKING CHANGES CategoryBreaking
### Features CategoryFeature
### Bug Fixes CategoryFix
### Performance Improvements CategoryPerformance
Any other section CategoryOther

Querying

// Check for breaking changes
if cl.HasBreakingChanges() {
    for _, e := range cl.BreakingChanges() {
        fmt.Printf("BREAKING: %s: %s\n", e.Scope, e.Description)
    }
}

// Filter by category
features := cl.EntriesByCategory(changelog.CategoryFeature)

Formatting

output := changelog.FormatSummary(cl)
fmt.Print(output)

FormatSummary produces human-readable terminal output. Breaking changes appear first with a WARNING prefix, followed by features, bug fixes, performance improvements, and other changes.

Example output:

WARNING: Breaking changes detected!

  BREAKING: config: rename ConfigPath to ConfigDir

Features:
  - http: add middleware chaining
  - chat: add streaming support

Bug Fixes:
  - fix startup race condition

Integration with SelfUpdater

The SelfUpdater in pkg/setup provides a convenience method that fetches and parses release notes in one call:

cl, err := updater.GetStructuredReleaseNotes(ctx, "v1.0.0", "v1.3.0")
if err != nil {
    return err
}

if cl.HasBreakingChanges() {
    fmt.Print(changelog.FormatSummary(cl))
}

The existing GetReleaseNotes method continues to return raw markdown and is unchanged.


Archive-Bundled Changelog

Release archives can include a CHANGELOG.md file alongside the binary (generated by go tool changelog generate in the CI release workflow). When present, the changelog is parsed directly from the archive โ€” zero extra API calls.

Integrated Fallback via SelfUpdater

GetStructuredReleaseNotes accepts an optional archive buffer. When provided, it tries the bundled changelog first and falls back to per-release API calls automatically:

archive, _ := updater.DownloadAsset(ctx, asset)
cl, err := updater.GetStructuredReleaseNotes(ctx, "v1.0.0", "v1.3.0", archive)

Direct ParseFromArchive

For lower-level control, ParseFromArchive can be called directly. It scans tar entries for CHANGELOG.md, parses it via Parse, and returns nil (not an error) if no changelog file is found:

cl, err := changelog.ParseFromArchive(archiveReader)
if err != nil {
    return err
}
if cl == nil {
    // No CHANGELOG.md in archive โ€” fall back to API-based retrieval
    cl, err = updater.GetStructuredReleaseNotes(ctx, from, to)
}

The file is matched case-insensitively and at any nesting depth within the archive.


Generation

The GenerateFromRepo function reads git history directly using go-git and produces markdown compatible with Parse():

result, err := changelog.GenerateFromRepo(".", changelog.WithMaxReleases(10))

Options

Option Description
WithSinceTag(tag) Only include releases after the given tag
WithMaxReleases(n) Limit to the N most recent releases
WithIncludeAll() Include non-conventional commits under "Other"

CLI Tool

The cmd/changelog tool wraps GenerateFromRepo as a Go tool directive:

# Generate to stdout
go tool changelog generate

# Generate to file
go tool changelog generate --output CHANGELOG.md

# Limit to releases after v1.5.0
go tool changelog generate --since v1.5.0

# Include non-conventional commits
go tool changelog generate --include-all

Add the tool directive to go.mod:

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

Use with go:generate:

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

Testing

The parser is pure logic with no I/O dependencies. Tests use inline markdown strings and testdata fixtures:

cl := changelog.Parse(rawNotes)
assert.True(t, cl.HasBreakingChanges())
assert.Len(t, cl.EntriesByCategory(changelog.CategoryFeature), 3)

  • Setup / Update โ€” self-update lifecycle that consumes the changelog parser
  • Version โ€” version comparison utilities used alongside changelog diffing