Skip to content

Describe a release before tagging it, and refuse a tag for one nobody described [#83] - #151

Merged
iderex merged 1 commit into
mainfrom
the-changelog-and-what-it-records
Aug 12, 2026
Merged

Describe a release before tagging it, and refuse a tag for one nobody described [#83]#151
iderex merged 1 commit into
mainfrom
the-changelog-and-what-it-records

Conversation

@iderex

@iderex iderex commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Closes #83.

What was wrong

Nothing in this repository told a reader what is different between two releases.
The repository whose gate this plan is measured against publishes such a file
and this one did not:

gh api repos/Flowfin/jellyfin-plugin-sso/contents/CHANGELOG.md --jq '.name,.size'
CHANGELOG.md
17755
gh api repos/Flowfin/site/contents/CHANGELOG.md
gh: Not Found (HTTP 404)

Run 2026-08-12 against main at 8117129, before this branch.

What the change does

CHANGELOG.md states what gets an entry and what does not, with the reason for
the omission. It records what changes what an operator serves or what a reader
can reach: a page added or removed, an address that moves, a budget number, a
check that starts refusing something it used to allow, and anything that moves
the bytes the build produces from unchanged input. It leaves out the words on
the pages, because prose is most of what this site is made of and a file listing
every corrected sentence buries the entries somebody is actually deciding from.
That is the version's own reasoning, in
decisions/0013-the-version-scheme.md, applied to the file that describes what
the number moves for.

It is kept by hand, and the file says so and why. A file generated from commit
subjects is a second copy of a history that already exists.

There is one section for what is unreleased and none for a released version,
because nothing has been released:

git ls-remote --tags origin | wc -l
0
gh api repos/Flowfin/site/releases --jq 'length'
0

Run 2026-08-12.

The refusal, and the proof that it bites

go run . changelog refuses a version the file carries no section for. On this
branch as it stands, which is the state a tree is in for most of its life:

go run . changelog
changelog: CHANGELOG.md holds 4 section(s): What gets an entry, What gets no entry, and why, How it is kept, Unreleased
CHANGELOG.md carries no section for 0.1.0, so this release would be one nobody described. Write the section, moving what belongs to it out of Unreleased, before the run that creates the tag
exit status 1

With a section for that version written above Unreleased, and nothing else
changed:

go run . changelog
changelog: CHANGELOG.md holds 5 section(s): What gets an entry, What gets no entry, and why, How it is kept, 0.1.0, Unreleased
  0.1.0 is described in 1 line(s) under its own heading
exit=0

Both run at 42d0239. The release run asks it before anything is packed or
tagged:

grep -n 'go run \.' .github/workflows/release.yml
59:        run: go run . ci
67:        run: go run . changelog
108:          version="$(go run . version)"

Run at 42d0239. So a version with nothing describing it stops the run in its
first job, which is before the job that is granted a write ever starts.

The suite holds the shapes the verb has to tell apart, and every fixture is
assembled from the version constant rather than written out, so a version bump
moves the fixtures with it:

go test ./internal/changelog -v
--- PASS: TestAVersionWithASectionPasses
--- PASS: TestTheDateBesideTheNumberIsNotPartOfTheName
--- PASS: TestOnlyUnreleasedIsRefusedAndSaysWhatToDo
--- PASS: TestAHeadingWithNothingUnderItIsRefused
--- PASS: TestALongerVersionIsADifferentSection
--- PASS: TestAMissingFileIsARefusalRatherThanASilence
--- PASS: TestTheRunSaysWhatItRead
ok

Run at 42d0239. A heading with nothing under it is refused for its own reason,
because a heading is where a description goes and is not one. A missing file
fails closed rather than reading as a version nobody had to describe.

Why it is not a leg of the gate

The version in the tree is the one the next release carries, and the section
describing it is written when somebody decides to release rather than when they
bump a constant. A gate leg would refuse every ordinary change for a description
nobody owes yet. The release run is the moment it is owed, and that is where it
runs. The verb exists so the same rule can be decided on the machine where the
mistake was made rather than only inside a workflow step.

The register, and what taking it out found

A file describing each release carries a heading per version because that is
what it is for, so it joins internal/version in the set the row
version-lives-in-exactly-one-file excludes. That entry is load-bearing rather
than decorative. With CHANGELOG.md tracked, carrying a section for the version,
and the entry removed:

go run . invariants
  version-lives-in-exactly-one-file: REFUSED, 3 violation(s)
    CHANGELOG.md: line 54 writes the version 0.1.0, and internal/version/version.go is the one file it is read from
    internal/changelog/changelog.go: line 88 writes the version 0.1.0, and internal/version/version.go is the one file it is read from

Run at 42d0239 with those two changes applied and then reverted. The second
line is not the register: it is a comment in the new package that illustrated
the two spellings of a heading with the number they are spellings of, which is a
second copy of the version. The row caught it in this change rather than after
it landed, and the comment now says what it means without writing the number.

The means

Go, in a package with a verb of its own. The rule has to be decidable where the
mistake is made rather than only inside a workflow step, and it has to be
provable by a suite, which is the position this repository already took for the
commit message rules. A shell block in the release file would satisfy neither.

The gate

go run . ci
gate: 6 legs, in order: format, vet, test, build, links, invariants
  format: ok, 37 file(s)
  vet: ok
  test: ok, 18 test file(s)
  build: ok, 3 file(s)
  links: every reference that stays inside this site resolves to a file the build wrote
  invariants: ok, 24 rule(s) decided, 1 owed and not decided
6 of 6 legs ran. None was skipped.

Run at 42d0239. The network-backed set was not asked for and the run says so.

This change carries no second reader. The ruleset on the branch requires no
approving review, so the evidence above stands in place of one, and the merge is
made by the same account that opened this.

… described [#83]

An operator holding a bundle has to decide whether the next one is worth
taking, and a version number does not answer that. This repository
published nothing that did: there was no changelog in the tree, while the
repository whose gate this plan takes as its target publishes one.

CHANGELOG.md carries what gets an entry and what does not, with the reason
for the omission. It records what changes what an operator serves or what
a reader can reach: a page added or removed, an address that moves, a
budget number, a check that starts refusing something it used to allow,
and anything that moves the bytes the build produces from unchanged input.
It leaves out the words on the pages, because prose is most of what this
site is made of and a file listing every corrected sentence buries the
entries somebody is deciding from. That is the same reasoning the version
follows in decisions/0013.

It is kept by hand. A file generated from commit subjects is a second copy
of a history that already exists, and what an operator needs is the
sentence saying whether this release moves something they depend on, which
a subject line does not say.

The refusal is what stops it being a courtesy. go run . changelog refuses
a version the file carries no section for, and refuses a heading with
nothing under it, because a heading is where a description goes and is not
one. The release run asks it before anything is packed or tagged. It is
deliberately not a leg of the gate: the version in the tree is the one the
next release carries and its section is written when somebody decides to
release, so a leg would refuse every ordinary change for a description
nobody owes yet.

The file is a register of versions rather than a copy of one, so it joins
internal/version in the set the row about a second copy excludes. That
exclusion is load-bearing rather than decorative, and taking it out was
what caught a comment in the new package spelling the version twice.

The means is Go, in a package with a verb, because the rule has to be
decidable on the machine where the mistake was made rather than only
inside a workflow step, and a suite has to be able to prove it bites.

Signed-off-by: Nils Lehnen <[email protected]>
@iderex iderex added the documentation Improvements or additions to documentation label Aug 12, 2026
@iderex iderex self-assigned this Aug 12, 2026
@iderex iderex added this to the First release milestone Aug 12, 2026
@iderex
iderex merged commit 39ab67a into main Aug 12, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Say what changed between two releases

1 participant