fix(examples): number the pre-release tag from the tags, and cover the publish-time invocation #13

Closed
mikluko wants to merge 4 commits from mikluko/octant/1718-examples-live into mikluko/octant/1721-unreadable-version
mikluko commented 2026-09-05 15:47:56 +00:00 (Migrated from github.com)

What this does and why

A repository adopted examples/release-branch/ and it broke twice within an hour. Both failures were in the tag composition step, and neither was findable by reading. This fixes them and folds back three other things the adoption surfaced.

Two defects

github.run_number counts a workflow file's runs, not a version's pre-releases. Renaming the workflow reset the counter to 1 and the next run died on fatal: tag 'v1.3.0-pre.1' already exists. Splitting the stage into two files gives each its own counter. A file rename should not be able to reach a tag sequence.

The ordinal now reads the tags, which are the thing being numbered:

prev=$(git tag --list "v$VERSION-$STAGE.*" \
       | sed "s|^v$VERSION-$STAGE\.||" \
       | grep -E '^[0-9]+$' | sort -n | tail -1)
tag="v$VERSION-$STAGE.$(( ${prev:-0} + 1 ))"

It costs nothing, because fetch-depth: 0 is already required for the tag-dependent checks. The grep -E '^[0-9]+$' is load-bearing and carries its own comment: a repository migrating from a two-segment scheme has tags like v1.3.0-pre.12.480, the glob matches them, and without the filter the sequence resumes at 13 rather than continuing the one-segment count.

${GITHUB_REF_NAME##*-} returns the whole ref when the branch carries no hyphen.

release/v1.3-pre  ->  pre
release/v1.3-rc   ->  rc
release/v1.3      ->  release/v1.3
main              ->  main

release/v1.3 composes a tag with a slash and fails at push, which is loud. main composes v1.3.0-main.7, a legal tag, which is the dangerous one.

The fix is not a guard in the script. The stage list moves into the trigger, branches: ['release/*-pre', 'release/*-rc'], where a branch filter's * does not span /. The derivation is then safe by construction: a branch naming no stage does not fail the run, it does not start one. The list also lands where a reader already looks to find which branches a workflow serves.

Four things the example was missing

Each carries a comment saying why, because none is guessable:

  • A guard stopping when the changelog's prerelease output is true. This step appends the only pre-release identifier the branch uses, so an entry carrying one of its own composes into nonsense.
  • already-tagged named as the branch's end signal rather than used as a bare if:. The final tag existing is how the branch knows it is spent.
  • concurrency with cancel-in-progress: false. Cancelling a run part-way through cutting and pushing a tag is worse than queueing.
  • The token, with the actual reason: a tag pushed with GITHUB_TOKEN triggers no workflow, so the publish workflow below never runs.

A fourth invocation

A tag-triggered workflow that reads the changelog for release notes runs on the pre-release tags the branch cuts, where the newest entry is legitimately open. Without off: undated-entry the build fails on a correct document, and the workflow that breaks is the one that never mentions the stabilization branch.

undated-release stays on there, and that is the invocation where it earns its place most plainly: a tag already names the version, so an undated newest entry is a release that shipped and nobody dated.

The example also shows the guard beside the relaxation:

if: format('v{0}', steps.changelog.outputs.version) == github.ref_name

1.3.0 never equals 1.3.0-pre.3, so a pre-release tag files no release. The same comparison refuses a tag pushed by hand at a commit the newest entry does not describe, which is why a repository that publishes from a changelog usually carries it already — such a repository needs one input, not a redesign.

Choosing stage names

pre sorting below rc is a property of the vocabulary, not an accident, and the README now says so with the evidence:

1.0.0-alpha.1  <  1.0.0-beta.1  <  1.0.0-candidate.1  <  1.0.0-early.1
               <  1.0.0-pre.1   <  1.0.0-rc.1         <  1.0.0-snapshot.1  <  1.0.0

The names that stuck sort in the order their stages run. The plausible alternatives are where it stops: candidate sorts below early and pre, and snapshot sorts above rc, so a nightly would outrank the candidate it precedes. Maven's SNAPSHOT is the one widely used convention that does not fit, and Maven does not compare qualifiers lexically at all.

The rule: pick stage names that sort in the order the stages run, and check them — worth doing when a third stage is added, which is the one moment nobody is reading the workflow that already works.

Two decisions worth a reviewer's eye

The branch workflow now cuts and pushes the tag rather than composing it into an output and stopping. Three of the four additions above have no subject otherwise, and the new tag.yaml is triggered by a tag the example would never produce. If the example is meant to stop at composition, the concurrency and token comments come out with it.

secrets.RELEASE_TOKEN is an invented generic name with no fallback to github.token. The fallback would degrade silently into exactly the failure its comment warns about. It is the one value in this diff a reader must replace.

Verification

go test ./... green, go vet ./... and gofmt -l clean, go run ./cmd/gen-readme idempotent. All four workflows name @v1, held by the existing test. The broken document's finding lists are exact per invocation, and CHANGELOG.md passes under three invocations and raises exactly undated-entry under the trunk one, which remains the strategy in a single assertion.

examples/release-trunk/, CHANGELOG.md, action.yaml, CONTEXT.md and internal/ are untouched. No version bump: this is documentation, and an entry would arm a release shipping no behaviour.

AI assistance

Written by Claude Code (Opus 5) under an octant worker contract, audited independently before opening.

Octant B1718 and T1719.

## What this does and why A repository adopted `examples/release-branch/` and it broke twice within an hour. Both failures were in the tag composition step, and neither was findable by reading. This fixes them and folds back three other things the adoption surfaced. ## Two defects **`github.run_number` counts a workflow file's runs, not a version's pre-releases.** Renaming the workflow reset the counter to 1 and the next run died on `fatal: tag 'v1.3.0-pre.1' already exists`. Splitting the stage into two files gives each its own counter. A file rename should not be able to reach a tag sequence. The ordinal now reads the tags, which are the thing being numbered: ```sh prev=$(git tag --list "v$VERSION-$STAGE.*" \ | sed "s|^v$VERSION-$STAGE\.||" \ | grep -E '^[0-9]+$' | sort -n | tail -1) tag="v$VERSION-$STAGE.$(( ${prev:-0} + 1 ))" ``` It costs nothing, because `fetch-depth: 0` is already required for the tag-dependent checks. The `grep -E '^[0-9]+$'` is load-bearing and carries its own comment: a repository migrating from a two-segment scheme has tags like `v1.3.0-pre.12.480`, the glob matches them, and without the filter the sequence resumes at 13 rather than continuing the one-segment count. **`${GITHUB_REF_NAME##*-}` returns the whole ref when the branch carries no hyphen.** ``` release/v1.3-pre -> pre release/v1.3-rc -> rc release/v1.3 -> release/v1.3 main -> main ``` `release/v1.3` composes a tag with a slash and fails at push, which is loud. **`main` composes `v1.3.0-main.7`, a legal tag**, which is the dangerous one. The fix is not a guard in the script. The stage list moves into the trigger, `branches: ['release/*-pre', 'release/*-rc']`, where a branch filter's `*` does not span `/`. The derivation is then safe **by construction**: a branch naming no stage does not fail the run, it does not start one. The list also lands where a reader already looks to find which branches a workflow serves. ## Four things the example was missing Each carries a comment saying why, because none is guessable: - A guard stopping when the changelog's `prerelease` output is true. This step appends the only pre-release identifier the branch uses, so an entry carrying one of its own composes into nonsense. - `already-tagged` named as the branch's **end signal** rather than used as a bare `if:`. The final tag existing is how the branch knows it is spent. - `concurrency` with `cancel-in-progress: false`. Cancelling a run part-way through cutting and pushing a tag is worse than queueing. - The token, with the actual reason: a tag pushed with `GITHUB_TOKEN` **triggers no workflow**, so the publish workflow below never runs. ## A fourth invocation A tag-triggered workflow that reads the changelog for release notes runs on the pre-release tags the branch cuts, where the newest entry is legitimately open. Without `off: undated-entry` the **build** fails on a correct document, and the workflow that breaks is the one that never mentions the stabilization branch. `undated-release` stays on there, and that is the invocation where it earns its place most plainly: a tag already names the version, so an undated newest entry is a release that shipped and nobody dated. The example also shows the guard beside the relaxation: ```yaml if: format('v{0}', steps.changelog.outputs.version) == github.ref_name ``` `1.3.0` never equals `1.3.0-pre.3`, so a pre-release tag files no release. The same comparison refuses a tag pushed by hand at a commit the newest entry does not describe, **which is why a repository that publishes from a changelog usually carries it already** — such a repository needs one input, not a redesign. ## Choosing stage names `pre` sorting below `rc` is a property of the vocabulary, not an accident, and the README now says so with the evidence: ``` 1.0.0-alpha.1 < 1.0.0-beta.1 < 1.0.0-candidate.1 < 1.0.0-early.1 < 1.0.0-pre.1 < 1.0.0-rc.1 < 1.0.0-snapshot.1 < 1.0.0 ``` The names that stuck sort in the order their stages run. The plausible alternatives are where it stops: `candidate` sorts below `early` and `pre`, and `snapshot` sorts **above** `rc`, so a nightly would outrank the candidate it precedes. Maven's `SNAPSHOT` is the one widely used convention that does not fit, and Maven does not compare qualifiers lexically at all. **The rule: pick stage names that sort in the order the stages run, and check them** — worth doing when a third stage is added, which is the one moment nobody is reading the workflow that already works. ## Two decisions worth a reviewer's eye **The branch workflow now cuts and pushes the tag** rather than composing it into an output and stopping. Three of the four additions above have no subject otherwise, and the new `tag.yaml` is triggered by a tag the example would never produce. If the example is meant to stop at composition, the concurrency and token comments come out with it. **`secrets.RELEASE_TOKEN` is an invented generic name with no fallback to `github.token`.** The fallback would degrade silently into exactly the failure its comment warns about. It is the one value in this diff a reader must replace. ## Verification `go test ./...` green, `go vet ./...` and `gofmt -l` clean, `go run ./cmd/gen-readme` idempotent. All four workflows name `@v1`, held by the existing test. The broken document's finding lists are exact per invocation, and `CHANGELOG.md` passes under three invocations and raises exactly `undated-entry` under the trunk one, which remains the strategy in a single assertion. `examples/release-trunk/`, `CHANGELOG.md`, `action.yaml`, `CONTEXT.md` and `internal/` are untouched. No version bump: this is documentation, and an entry would arm a release shipping no behaviour. ## AI assistance Written by Claude Code (Opus 5) under an octant worker contract, audited independently before opening. Octant B1718 and T1719.

Pull request closed

Sign in to join this conversation.
No description provided.