open atlas
↑ Back to track
CI/CD pipelines CICD · 12 · 02

Changelogs and releases: generated notes, PR labels, and the human-facing record

A changelog is the human-readable diff between two tags. Generate it from conventional commits or PR labels so it cannot drift from what shipped, attach it to an immutable GitHub Release tied to the tag and artifact, and the record becomes the auditable answer to what changed.

CICD Senior ◷ 15 min
Level
FoundationsJuniorMiddleSenior

The audit request was one line: “show us every change that touched the payment-rounding logic in the last quarter, and who approved each.” The team had a CHANGELOG.md — hand-maintained, which meant it was a work of fiction. Half the entries said “various fixes and improvements.” Three releases had no entry at all because someone forgot. One entry described a feature that was reverted before it shipped. The actual changes lived in 1,400 commits and a wall of merged PRs, and reconstructing the audit trail by hand took two engineers three days. The postmortem action item was not “write better changelogs.” It was “stop writing them”: derive the release notes mechanically from the same commits and PRs that produced the build, so the record can never disagree with what shipped — and attach that record to an immutable release the auditor can read directly.

The changelog is a derived artifact, not a hand-written one

Ask yourself how many release notes you have written under pressure — and how many of them were accurate three months later. A changelog answers two questions for a human: what changed between the last release and this one, and do I need to do anything before upgrading. The Hook’s failure was treating it as prose someone writes — which guarantees it drifts from reality, because the writer is a separate, fallible step from the shipping. The fix is to make it a function of the same inputs that produced the build. Two sources dominate:

  • Conventional commits: a release tool groups commits since the last tag by type — feat: under Features, fix: under Bug Fixes, anything with BREAKING CHANGE: under a prominent Breaking Changes section — and emits a Markdown section per release. Because the commits are the same ones that built the artifact, the notes cannot describe something that did not ship.
  • PR labels / titles: GitHub’s automatic release notes (and tools like release-drafter) categorize merged PRs by label — type: feature, type: fix, breaking — into configured sections, and link each line to its PR and author. This is the model when you squash-merge and the PR title, not individual commits, is the unit of change.

Either way the changelog is generated in CI at tag time, appended to CHANGELOG.md, and used as the body of the release. The discipline shifts from “write good entries” to “label PRs and write good commit/PR subjects” — work that already happens on every change, enforced by a label/commit-lint check, so the record is a byproduct, not a chore.

Quiz

Why is a changelog generated from conventional commits or PR labels more trustworthy for an audit than a hand-maintained CHANGELOG.md?

The GitHub Release is the record, not the file

CHANGELOG.md is convenient for browsing in the repo, but the load-bearing artifact is the GitHub Release: an object bound to a specific tag, carrying the generated notes as its body, the build artifacts (or links to the published image/package), and — through the tag — the immutable ref and digest the release shipped. A release is the join point of everything in this unit: the semver name, the immutable tag, the digest of what ran, the human-readable changelog, and (optionally) the provenance attestation that proves how it was built. That join is exactly what the auditor wanted and the hand-maintained file could not give — a single, per-version record tying what changed to what shipped to who approved it. Treat the published release as immutable in spirit: you edit notes for clarity, but the version, tag, and artifact it points at do not change; a follow-up fix is a new release, not an edit of the old one. Pre-releases (v2.5.0-rc.1 marked “pre-release”) let release candidates carry generated notes and artifacts without appearing as the “Latest” release, so consumers who pin to latest are not pulled onto an unfinished build.

Quiz

A team wants the single most useful place to answer 'what shipped in v2.5.0, and how do I get exactly those bytes back?' Which artifact ties it all together, and why?

Why this works

Why bind the changelog to the release object instead of just keeping the Markdown file? Because the file is detached from the artifact: nothing stops CHANGELOG.md from saying one thing while the deployed image is another, which is how the Hook’s file drifted into fiction. The release object is anchored to a tag, and the tag resolves to a digest, so the notes, the version, and the exact shipped bytes are one indivisible record. When the auditor — or the on-call deciding what to roll back to — opens the release, they see the change description and the artifact identity in the same place, with no chance the two disagree.

Recall before you leave
  1. 01
    Contrast generating release notes from conventional commits versus from PR labels — when does each fit, and what do they share?
  2. 02
    Why is the GitHub Release object, not CHANGELOG.md, the artifact that makes auditing and rollback a single lookup?
Recap

A changelog answers two human questions — what changed since the last release and what you must do before upgrading — and it is trustworthy only when it stops being prose someone writes and becomes a function of the build’s inputs. Generate it from conventional commits, grouping feat: under Features, fix: under Bug Fixes, and BREAKING CHANGE: under a prominent section, or from labeled merged PRs via GitHub’s auto-generated notes or release-drafter, linking each line to its PR and author; either way it is emitted in CI at tag time and cannot describe a change that did not ship, because it is derived from the same commits/PRs that produced the artifact. The discipline moves from writing entries to enforcing honest commit types and PR labels with a lint check — work that already happens on every change. The load-bearing record is not CHANGELOG.md but the GitHub Release: an immutable object bound to a tag, carrying the generated notes, the artifacts or links, and through the tag the digest of what ran, plus optionally the provenance attestation. That release is the join point of the whole unit — semver name, immutable tag, artifact digest, human changelog, and approval in one per-version record — which turns “show every change and who approved it” and “restore exactly these bytes” from multi-day archaeology into a single lookup. Edit notes for clarity, but never the version or artifact a published release points at; a follow-up is a new release, and a release candidate ships as a marked pre-release so latest-pinned consumers are never pulled onto an unfinished build. Now when you see a hand-maintained changelog in a repo, you know exactly how far it has drifted from reality — and what to replace it with.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 6 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.