Skip to content

Tag what a release built instead of what it was meant to build - #135

Open
andiwand wants to merge 1 commit into
mainfrom
release-workflow-records-what-it-built
Open

Tag what a release built instead of what it was meant to build#135
andiwand wants to merge 1 commit into
mainfrom
release-workflow-records-what-it-built

Conversation

@andiwand

@andiwand andiwand commented Aug 3, 2026

Copy link
Copy Markdown
Member

A version tag had to be pushed before the build it named, which is a promise the release cannot keep: a version often takes more than one build to clear review, and the second one gets built from a different commit. Tag 1.37 is the case in point — it points at 6e97420, three commits behind what was actually submitted, and the build that shipped has no name in git at all.

So the tag stops being the trigger and becomes the record.

What changes

release.yml is dispatch-only. The push: tags: trigger is gone; the version input is now the only source. Same one-liner as before:

gh workflow run release.yml -f version=1.38 -f flavor=both

The workflow tags what it built. After each successful upload it writes build/<flavor>/<version>/<build> at the built commit, annotated with the run it came from. Per flavor, because Pro and Lite count their builds separately — one commit uploaded to both apps is two builds with two different numbers. Needs contents: write, and the success() in the step's if is spelled out because naming an if drops the implicit one.

The plain <version> tag stays manual, created once the release is live, pointing at the accepted build rather than the tip of main:

git tag 1.38 build/pro/1.38/4^{} && git push origin 1.38

Dropping the tag trigger isn't optional here, incidentally: 1.38 matches the old '[0-9]*' filter, so the receipt tag would have kicked off another release.

Fastfile hands the build number back through GITHUB_OUTPUT — it's computed from latest_testflight_build_number inside the lane, and the workflow can't name the tag without it. A local run has no GITHUB_OUTPUT and is unaffected.

resolve-version.py loses its --tag arm and the tag/input agreement check. The "only a dry run may go without a version" guard is untouched.

CHANGELOG.md gets the matching rule: cut the heading at submission rather than at a tag, on main, in the same PR that writes the store copy — and if a second build of the same version is needed, the fix goes under the heading already cut rather than back under Unreleased. That is exactly the case #134 fell into.

Also __pycache__/ in .gitignore, since running the version script by hand leaves one.

Notes

Nobody bumps a version in the tree. A commit on main is not a release and the 0.0.0 in project.pbxproj says so — a stale-but-plausible 1.37 sitting there would be worse.

Two things deliberately left alone:

  • The existing 1.37 tag. It's wrong but it's published; moving it is worse than leaving it. The README says what it points at.
  • The 1.36/1.37 changelog attribution. changelogs/1.37.txt describes the work CHANGELOG.md files under 1.36, while 1.36's own entry reads as a shipped release. If 1.36 never reached users, that entry should say so — but that's a factual question about what shipped, not something to guess at in this PR.

Untested end to end, since a real run uploads to App Store Connect. The YAML parses, the tag message dedents correctly out of the block scalar, resolve-version.py was exercised across all its cases, and ruby -c passes on the Fastfile. A dry_run dispatch exercises everything except the tag step itself, which is skipped on a dry run by design.

🤖 Generated with Claude Code

A version tag had to be pushed before the build it named, which is a
promise the release cannot keep: a version often takes more than one
build to clear review, and the second one is built from a different
commit. Tag 1.37 is the case in point - it points at 6e97420, three
commits behind what was actually submitted, and the build that shipped
has no name in git at all.

So the tag stops being the trigger and becomes the record. The workflow
is dispatched by hand with the version it should build, and writes
build/<flavor>/<version>/<build> at the commit it built once the upload
goes through. Per flavor, because Pro and Lite count their builds
separately: one commit uploaded to both apps is two builds with two
different numbers. The plain version tag is left to a human, once the
release is actually live.

The build number was only ever known inside the lane, so the Fastfile
hands it back through GITHUB_OUTPUT. Dropping the tag trigger also
matters on its own: the receipt tag would have matched '[0-9]*' and
started another release.

resolve-version.py loses its tag arm, and the version input is now the
only source. Nobody bumps a version in the tree - a commit on main is
not a release, and the 0.0.0 in project.pbxproj says so.

CHANGELOG.md gets the matching rule: the heading is cut at submission
rather than at a tag, on main, in the same pull request that writes the
store copy, and a fix that goes up as a second build of the same version
belongs under the heading already cut rather than back under Unreleased.
That is the case #134 fell into.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcHeDGMHcFChp6oHiaiEDV
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant