Skip to content

Hold the version in one place and give the release run a tag to build [#53] - #150

Merged
iderex merged 1 commit into
mainfrom
the-version-and-the-release-run
Aug 12, 2026
Merged

Hold the version in one place and give the release run a tag to build [#53]#150
iderex merged 1 commit into
mainfrom
the-version-and-the-release-run

Conversation

@iderex

@iderex iderex commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Closes nothing on its own. It lands the version and the release run that #53
asks for, and the one clause of that issue it cannot meet is named at the
bottom rather than left for a reader to notice.

What was wrong

Nothing in this tree named the version it releases under, so a tag had nothing
to be built from and the bundle and the bill of materials had nothing to agree
with. There is no tag and no release behind this repository today:

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

Run 2026-08-12.

What the change does

The version is a constant in internal/version, and everything that states it
reads it from there. The build names it in its report, the bill of materials
states it on the component the document is about, and the release run builds
the tag out of the verb that prints it. The prefix that turns the version into
a tag sits beside the constant, so two files cannot spell one release
differently.

go run . version
0.1.0
go run . build | head -1
version 0.1.0
go run . sbom 2>/dev/null | grep -m1 '"version": "0'
      "version": "0.1.0",

All run at 87f032d.

decisions/0013-the-version-scheme.md carries what each part of the number
moves for, why the words on the pages do not move it, and what the alternatives
cost.

The release run is three jobs. The first runs the whole gate verb. The second
calls the same packing file a pull request already runs, so a release carries an
archive packed by a definition that has been exercised on every change rather
than by a second one whose first real run is release day. The third is the only
job granted a write, and it creates the tag with the bundle and the bill of
materials attached. It creates no deployment: tagging and publishing are
separate acts, and joining them leaves a bad publish undoable without deleting a
tag.

Write access appears once in the file:

grep -n 'contents:' .github/workflows/release.yml
43:      contents: read # checkout only; nothing here writes
67:      contents: read
75:      contents: write # the tag and the release; the only write in this file

Run at 87f032d.

The guard, and the proof that it bites

version-lives-in-exactly-one-file refuses the version written into a tracked
file outside the register that holds it. With one sentence appended to
README.md:

go run . invariants
  version-lives-in-exactly-one-file: REFUSED, 1 violation(s)
    it refuses the version this repository releases under, written into a tracked file outside the register that holds it
    README.md: line 63 writes the version 0.1.0, and internal/version/version.go is the one file it is read from
exit status 1

and with the sentence removed again:

go run . invariants
  version-lives-in-exactly-one-file: ok, 99 file(s) of every tracked text file, outside the register that holds the version

Removing the row from the table reds the suite rather than passing quietly:

go test ./internal/invariant
--- FAIL: TestEveryRowRefusesItsOwnViolationAndPassesTheNeighbour (0.00s)
    invariant_test.go:178: the table holds 23 row(s) and this test carries 24 violation(s); a row without one proves nothing
FAIL

All three run at 87f032d.

The row compares whole version-shaped runs rather than searching for a
substring, so a longer version spelled around this one and a pinned tool are
both left alone, and a version at the end of a sentence is still found with the
full stop after it. TestTheReleaseVersionRowTellsAnotherVersionApart and
TestTheReleaseVersionRowNamesWhereTheVersionBelongs hold both halves.

What the row reaches is the copy at the moment it is written. A copy made before
a version moved carries the old number and reads to the row as a different
version, so the row does not find a sentence that went stale yesterday. That
bound is written at the operator rather than implied here.

The means

Go, in a package of its own. Both readers of the value reach it through code
that is already compiled here, and the release run gets it from a verb rather
than from a second parser: a bare file at the root would be read once by the
build and once more by a shell step, which is where a trailing newline becomes a
tag nobody can resolve. The three rules survive it - the row refuses a second
copy, the suite proves the row bites, and every number above carries the command
that produced it.

The gate

go run . ci
gate: 6 legs, in order: format, vet, test, build, links, invariants
  format: ok, 35 file(s)
  vet: ok
  test: ok, 17 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.

go run . reproduce
reproduce: two builds of ., compared byte for byte
  3 file(s), identical in both builds

Both run at 87f032d. The browser-backed and network-backed sets were not
asked for and the run says so; needs-network was not run here.

What this does not do, and what #53 still waits on

The release run has never been dispatched, so no tag exists and nothing has been
attached to one. That is deliberate. Every other issue in the same milestone is
open, the output this would package is still the placeholder page, and a first
tag on a public repository is not a thing to create in order to tick a clause.
So #53's clause that the release run produces a tag with the bundle and the bill
of materials attached is unobserved, and the issue stays open holding exactly
that.

The other three clauses are readable off this branch and are quoted above: the
version appears in one file and a row refuses a second, the publish job needs
the gate job so a failing leg stops the tag existing, and write permission
appears on one job.

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.

…#53]

Nothing in this tree named the version it releases under, so the tag, the
bundle and the bill of materials had nothing to agree with and no run
existed to make any of them. A version copied into a second file is right
on the day it is typed, and the copy that goes stale is the one a reader
takes the version from rather than the one a release reads, which is a
document announcing a release nobody tagged with nothing about the tag to
say otherwise.

The version is a constant in internal/version and is read from there by
everything that states it: the build names it in its report, the bill of
materials states it on the component the document is about, and the
release run builds the tag from the verb that prints it. The prefix that
makes a tag out of it sits beside the constant so two files cannot spell
one release differently.

The row version-lives-in-exactly-one-file refuses the version written into
any tracked file outside the register that holds it. It compares whole
version-shaped runs rather than searching for a substring, so a longer
version spelled around this one stays a different version and a pinned
tool is left alone. What it reaches is the copy at the moment it is
written, which is stated at the operator rather than implied.

The release run gates, packs and tags in three jobs, and the only job
granted a write is the last one. It calls the same packing file a pull
request runs, so the archive a release carries is packed by a definition
that has already been exercised. It creates no deployment: tagging and
publishing are separate acts, and joining them leaves a bad publish
undoable without deleting a tag.

The means is Go, in a package of its own, because both readers reach the
value through compiled code already here and a bare file at the root would
be parsed once by the build and once more by a shell step.

decisions/0013-the-version-scheme.md carries what each part of the number
moves for and what the alternatives cost.

Signed-off-by: Nils Lehnen <[email protected]>
@iderex iderex added the ci 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 8117129 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant