Build the release body from a per-release notes file - #50
Merged
chrisuthe merged 1 commit intoAug 30, 2026
Merged
Conversation
Every tag published the same body: one quoted heredoc of Install, Gatekeeper and SHA256SUMS prose that said nothing about the release, under `##` headings that render as banner text beneath the release page's own title. v0.1.6's notes had to be replaced by hand after publishing. The body is now two halves. The half that changes is `docs/release-notes/<version>.md`, keyed off the version preflight already resolves from CMakeLists.txt and written in the pull request that bumps it, so what a release says about itself is reviewed alongside the change it describes. The half that does not is a short tail in the workflow: what `sendspin-cli --version` prints, the unsigned-macOS disclosure, and links to the wiki, the tag-pinned README and the roadmap. Read in preflight, which is the job with a checkout -- the release job has none on purpose, and adding one would run an action under `contents: write` for a file another job already holds. Carried across as a job output beside `version`, fenced with a random delimiter, and consumed through the step's environment rather than interpolated into its script, so nothing written in a notes file is parsed as a workflow expression or as shell. The single `sed` pass now covers both halves, which is what lets a notes file write `@REPO@` and `@TAG@` into its compare link. Preflight refuses a tag whose notes file is missing, empty, leaves a ``` fence open, or carries a `#` or `##` heading outside a fence -- seconds, before the matrix burns three runners. The release job refuses to publish if the output did not reach it, rather than shipping a body of tail alone and calling it success. The download, completeness assertion, checksum, draft-create and publish steps are untouched.
chrisuthe
marked this pull request as ready for review
August 30, 2026 23:55
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
.github/workflows/release.ymlpublished the same body for every tag: one quoted heredoc of Install, Gatekeeper andSHA256SUMSprose that said nothing about the release, under##headings that render as banner text beneath the release page's own title. v0.1.6's notes had to be replaced by hand withgh release editafter publishing.The body is now assembled from two halves.
The half that changes is
docs/release-notes/<version>.md— keyed off the versionpreflightalready resolves fromCMakeLists.txt, written in the pull request that bumps it, so what a release says about itself is reviewed alongside the change it describes. As a side effectdocs/release-notes/becomes the changelog this repo has never had.The half that does not is a short tail in the workflow: what
sendspin-cli --versionprints, the unsigned-macOS disclosure, and links to the wiki, the tag-pinned README and the roadmap. Install, "macOS is unsigned" and Checksums are gone as sections — a release body is frozen at the tag and a wiki page is not, so a copy of one in the other drifts with no way for a reader to tell which is current.How the notes reach the release job
The
releasejob has no checkout on purpose, and none was added.preflightalready checks out and already readsCMakeLists.txt, so it reads the notes file too and hands the content over as a job output besideversion.releasejob asksdownload-artifactfor whatever the build produced with noname:and nopattern:on purpose, and the step after it diffs the whole ofdist/against the exact set the tag publishes. A seventh file arriving there would have to be excepted from the assertion that makes the release atomic. The other alternative — a checkout inrelease— is one more action running undercontents: writefor a file another job already holds.od -An -N16 -tx1 /dev/urandom), because a heredoc into$GITHUB_OUTPUTends at the first line equal to it and what is being fenced is exactly the text somebody wrote in the tree.opensslis not in every image; coreutils is.What
preflightrefusesAll four fire in seconds, before the matrix burns three runners, and each names the file and ends in the fix:
[ -s ]would pass a lone newline#or##outside a fenceThe heading scan is fence-aware so that a column-0
#inside a bash block — which this project's docs write,docs/wiki/Installation.mdamong them — is not refused as a heading it is not.releasethen refuses to publish if the output did not reach it (a renamed step id, a typo in the output name), rather than shipping a body of tail alone and calling it success.Two deliberate calls
1. The unsigned-macOS disclosure stayed, as two sentences in the tail. The brief said to drop the "macOS is unsigned" section, and the section is gone — the
##heading, thexattrwalkthrough, the Privacy & Security override, the notarization account. What remains is the disclosure itself, pointing at the tag-pinned README anchor.docs/ROADMAP.md:1306states as a shipped property of item 10: "The release notes say the same in short, which is where a reader deciding whether to download actually is." Dropping it would have falsified that claim in the same commit. This is a deviation from the acceptance criterion's list of what remains — flagging it rather than settling it silently. Easy to strike.2. A missing notes file fails
preflight, the brief's recommended option, rather than falling back to a tail-only body. A release with no notes is exactly the defect being fixed, and failing costs seconds.Owed, deliberately not here
Every refusal fires after the tag is pushed, and recovery means deleting and re-pushing it. A step in
ci.ymlasserting thatdocs/release-notes/<CMakeLists VERSION>.mdexists would move the failure into the bump PR where the file belongs and reducepreflight's guard to a backstop. Separate change — the brief scopes this one to the notes step.Verification
Every case below executed the actual
run:blocks extracted from the YAML, onubuntu:24.04in Docker — not a re-implementation.0.1.6## Installheading5: ## Install# 1.and## 2.$(id -u),$HOME,${VAR},!!NOTESempty inreleaseshellcheck scripts/*.shandactionlintare clean. The download, completeness-assertion, checksum, draft-create and publish steps are byte-identical toorigin/main, verified bydiffrather than by inspection.Dry run — the exact body
v0.1.6would publishAssembled from
docs/release-notes/0.1.6.mdplus the tail, after thesedpass. Every heading is###.