Files
yellowjacket/.planning/plans/completed/017-release-automation.md
logan 6e563f3846
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m27s
CI / e2e (pull_request) Successful in 6m8s
docs: record what shipping the release pipeline taught us
Moves plan 017 to completed with a recap, and lifts the three findings
that generalise into NOTES.md: a preset major that renders empty notes
with everything green, a 403 that looks like branch protection and is a
token scope, and tag-triggered workflows running the tagged commit's
own definitions.
2026-08-17 23:35:59 -04:00

4.4 KiB

017 — Releases that happen by themselves

Shipped as v0.0.1. A merge to main now reads the Conventional Commits since the last tag, cuts the tag and the Gitea release whose body is the generated changelog, and the four publishing workflows build that tag and attach their artifacts. Nothing is released by hand.

What it looks like now

release.yml on push to main → semantic-release → tag → four v* workflows in parallel (serialised in practice by the capacity-1 runner):

workflow publishes attaches
arch-package pacman registry …-x86_64.pkg.tar.zst
android-apk generic registry (Obtainium) …-android-arm64.apk
desktop-assets …-linux-amd64.tar.gz
homebrew-formula the public tap — (builds from source)

Verified on the real thing: all five green, three assets on the release, the tap at 0.0.1, and the Obtainium latest URL serving 200.

The five decisions, and what they cost

  1. semantic-release, not a shell script. The first draft of this plan proposed hand-rolling it and the argument did not survive checking: @semantic-release/exec is first-party and current, and the Gitea-shaped part is one curl. What I would have hand-rolled — commit parsing, semver ordering, note rendering — is the part with the edge cases and none of it is Gitea-shaped.
  2. @saithodev/semantic-release-gitea is a dead end and was offered before it was checked: last published 2022, got@10, and no peer dependency on semantic-release at all.
  3. No @semantic-release/git. main is protected, so a changelog commit-back is rejected by the pre-receive hook — and would be rejected after the tag was pushed, leaving a tagged release the run reports as failed. The release page is the changelog; .release-notes.md is a gitignored carrier and CHANGELOG.md is a signpost.
  4. Versions restart at 0.0.1, a downgrade on every channel. No epoch, no versionCode offset: both are permanent, a reinstall is once. Documented in packaging/homebrew/README.md and docs/android-release.md.
  5. No macOS and no Windows. GOOS=darwin CGO_ENABLED=0 fails at wails/v3/pkg/mac and there is no macOS runner, so Homebrew-from-source stays that channel. Windows cross-compiles in ~2.5 s and is withheld because no build of it has ever been run.

Four things that only showed up by running it

  • conventional-changelog-conventionalcommits@10 renders empty notes. Silently: right version, right tag, every step green, and a release body that is a bare ## 0.0.1 (date) heading with nothing beneath it. Held at 9, in release.yml and make release-dry, with the reason beside both. Check the rendered notes, never the exit code.
  • semantic-release core dry-run-pushes to the release branch as a permission check, independently of any plugin. PACKAGE_TOKEN had package-write and repo-read — enough to clone, not enough for this — and it failed with a flat 403 Forbidden that reads exactly like branch protection. It is not: a --dry-run push never reaches the pre-receive hook, which a one-line experiment settled. The token needed write:repository.
  • The floor tag must go on HEAD^, not HEAD. Seeded on the merge commit itself it leaves nothing between the floor and HEAD, and semantic-release correctly reports there is nothing to release. The first run did exactly that and cut nothing.
  • A tag-triggered workflow runs from the tagged commit's tree. Moving v0.0.0 back to 6fb7b5e ran the pre-merge homebrew workflow, which predates the v0.0.0 skip guard, and pushed a 0.0.0 formula to the public tap. Self-corrected at 0.0.1. The corollary is general: a guard added today does not protect a tag pointing at yesterday.

Two mechanisms confirmed, having been assumptions

  • A tag pushed with a user PAT does start the v* workflows; one pushed with the Actions token does not (go-gitea#33123). Both halves are load-bearing and both were observed: the floor seed triggered nothing, and the release tag triggered all four.
  • Tags are not protected on this repo, only main — which is what lets semantic-release tag at all.

Left behind deliberately

v0.0.0 stays on origin as the floor. It carries no release, and all four publishers skip it by name.