Skip to content

Take the version from the git tag instead of project.pbxproj - #127

Open
andiwand wants to merge 2 commits into
mainfrom
release-version-from-tag
Open

Take the version from the git tag instead of project.pbxproj#127
andiwand wants to merge 2 commits into
mainfrom
release-version-from-tag

Conversation

@andiwand

@andiwand andiwand commented Jul 27, 2026

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

The marketing version was a number committed in project.pbxproj — which describes either a release that already went out or a guess at the next one, so it was wrong almost all of the time and had to be bumped by hand before every release. The build number already stopped being committed when the release moved into CI; this does the same for the other half.

Both halves of the version now come from outside the tree:

where it comes from what is checked in
MARKETING_VERSION (CFBundleShortVersionString) the git tag 0.0.0
CURRENT_PROJECT_VERSION (CFBundleVersion) latest TestFlight build + 1 1

Cutting a release is git tag 1.36 && git push --tags, and needs no commit before or after it. This is the same move OpenDocument.droid made, minus the version-code arithmetic — iOS has no equivalent of Play's monotonic integer, so the tag is just handed through.

What changed

  • release.yml runs on a version tag (1.36, or v1.36), and keeps the workflow_dispatch path with a version input for runs off a branch — how a release whose upload failed gets finished from the branch it was cut from.
  • .github/scripts/resolve-version.py decides which run may build which version and says why. It sits right behind the checkout, because a run that cannot name a version should end before twenty minutes of conan and xcodebuild, not after. Being a script rather than a run: block, it can be run by hand to see what a dispatch would do.
  • A dry_run input builds, signs and archives the .ipa without uploading — the only way to exercise the signing path without putting a build on TestFlight. It is also the only kind of run allowed to go without a version, since uploading 0.0.0 would be rejected by App Store Connect at the very end.
  • MARKETING_VERSION rides the same path CURRENT_PROJECT_VERSION already did: an xcarg on the command line, so a release never leaves the working tree dirty. Both reach fastlane through the environment rather than as lane options, which fastlane passes through as strings — dry_run:false would have arrived as the string "false" and read as true.

Note before merging

1.35 is live, so the first tag has to be 1.36 or higher — App Store Connect rejects anything not above what is published.

Verified

  • xcodebuild -showBuildSettings with the override resolves MARKETING_VERSION = 1.36, and without it 0.0.0 / 1 — so the command-line setting reaches the bundle, which takes both from $(...) in Info.plist.
  • resolve-version.py exercised across tag push, v-prefixed tag, dispatch with and without a version, an input disagreeing with the tag, a malformed version, and a shell-injection-shaped one.
  • Ruby and YAML both parse.

Not verified: a full release run, which needs the secrets and a real upload. The first tag push is the real test — dry_run is there to take it first if you'd rather.

The marketing version was a number committed in project.pbxproj, which
describes either a release that already went out or a guess at the next
one, so it was wrong almost all of the time and had to be bumped by hand
before every release. The build number stopped being committed when the
release moved into CI - it comes from what TestFlight already has - and
this does the same for the other half: project.pbxproj holds 0.0.0, the
release workflow runs on a version tag, and the tag reaches xcodebuild
as MARKETING_VERSION by way of fastlane's ODR_VERSION. Cutting a release
is now a tag, and needs no commit before or after it.

resolve-version.py decides which run may build which version, and says
why: a tag push builds the tag, a dispatched run builds its version
input, and neither is only allowed for a dry run. It sits right behind
the checkout, because a run that cannot name a version should end before
twenty minutes of conan and xcodebuild, not after. Being a script rather
than a run: block, it can be run by hand to see what a dispatch would do.

The dry run is the second half: it builds, signs and archives the ipa
without uploading it, which is the only way to exercise the signing path
without putting a build on TestFlight. It is also the only kind of run
allowed to go without a version, since uploading 0.0.0 would be rejected
by App Store Connect at the very end.

Both settings reach fastlane through the environment rather than as lane
options, which fastlane passes through as strings - dry_run:false would
have arrived as the string "false" and read as true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5UtM8wFTKh9dGjnx2X2cs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 562c6d09e3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread .github/workflows/release.yml Outdated
Comment thread fastlane/Fastfile
Two things the tag change got wrong, both from copying the android side
where they are safe.

The concurrency key was narrowed to the ref. Droid can do that because
its version code is derived from the tag, so two runs cannot collide on
one. Here the build number is a live query of what TestFlight has, and
two overlapping runs read the same answer - the second upload then
carries a version and build pair App Store Connect has already taken.
Back to one release at a time, whatever ref it is on.

The lane trusted the workflow to have checked for a version, but these
lanes are documented as runnable by hand, where nothing had checked:
an unversioned local deploy built 0.0.0 and only found out at the upload,
twenty minutes later, that it is below what is live. It now says so
before the build, the way resolve-version.py does in CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5UtM8wFTKh9dGjnx2X2cs
@andiwand
andiwand enabled auto-merge (squash) July 27, 2026 20:13
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