Skip to content

Take the version from the git tag instead of the manifest - #538

Merged
andiwand merged 4 commits into
mainfrom
version-from-tag
Jul 27, 2026
Merged

Take the version from the git tag instead of the manifest#538
andiwand merged 4 commits into
mainfrom
version-from-tag

Conversation

@andiwand

@andiwand andiwand commented Jul 27, 2026

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

versionCode and versionName lived in AndroidManifest.xml and had to be raised by hand before tagging. Between releases that number is wrong: it describes a release that already went out, or guesses the next one. Forgetting it only surfaces at the very end of a release run, as Version code 203 has already been used — six minutes in, and unrepeatable, since the upload step failing takes the attach job with it.

The tag is the one place a version cannot be stale: it does not exist until the version is true. So the repository now carries no version number at all.

What changes

  • app/build.gradle derives both halves from -Podr.version (or ODR_VERSION): v4.8.0 → name 4.8.0, code 40800, two digits per part. v4.8 and 4.8.0 are accepted too.
  • A part above 99 is a build error, not a silently folded code — 4.100.0 and 5.0.0 would both be 50000.
  • The two attributes come out of AndroidManifest.xml. Nothing replaces them there: both spellings reach the merged manifest and the gradle one wins, so a second copy could only ever disagree with the tag.
  • The release workflow passes the tag it ran on. A dispatched run has no tag, so it takes a version input — which is also how a half-uploaded release gets finished off the branch it was cut from. Without either, it can only be a dry run, and says so before the build rather than after it.
  • fastlane android deployPro version:v4.8.0 for the laptop path, as a gradle property rather than an env var, since the daemon captures its environment at startup.

Version codes jump to five digits

Codes up to 204 were counted by hand, so the first derived one is 40800. That is one way — Play only ever accepts a code above the last one it saw.

Unversioned builds

A build handed no version is 0.0.0. Nothing reads it: no code in the app looks at its own version, and only what the release workflow builds is ever uploaded. Its code is spelled out as 1 rather than derived, because AGP rejects 0 (versionCode is set to 0, but it should be a positive integer).

F-Droid

This drops the versionCode that F-Droid's UpdateCheckData greps out of the manifest, so their auto-update stops finding releases. Their metadata needs rewriting regardless: the build recipe seds .java files under at/tomtasche/reader/, which the kotlin migration and the package rename removed, and the listing has been stuck at 4.3 since December. Worth a follow-up MR to fdroiddata either way.

Verified locally

  • merged manifest for -Podr.version=v4.8.0versionCode="40800", versionName="4.8.0-pro"; v4.9.1 via ODR_VERSION40901; no version → 1 / 0.0.0-pro
  • rejected as expected: v4.100.0, lol, v4.8.0.1, 4..8
  • spotlessCheck, testProDebugUnitTest, lintProDebug all pass (no MissingVersion finding)

Does not touch the other half of the problem — the upload step is still not idempotent, so a duplicate code still fails the run and skips attach. That is a separate change.

versionCode and versionName were two numbers in AndroidManifest.xml that had to
be walked forward by hand before tagging, and that were wrong the rest of the
time: between releases they describe one that already went out, and a bump
commit is a guess at the next. Forgetting one of them is only discoverable at
the very end of a release run, when the store refuses a code it has already
accepted.

The tag is the one place a version cannot be stale, since it does not exist
until the version is true. So the repository now holds no version at all. The
release workflow hands gradle the tag it was triggered by as -Podr.version, and
app/build.gradle derives both halves: v4.8.0 becomes name 4.8.0 and code 40800,
two digits per part. A part above 99 is an error rather than a code that
collides with the next release - 4.100.0 and 5.0.0 would otherwise both be
50000.

Codes up to 204 were counted by hand, so the first derived one is a five digit
jump, and that is one way: play only ever accepts a code above the last.

A build handed no version is 0.0.0. Nothing reads it - no code in the app looks
at its own version, and only what the release workflow builds is ever uploaded.
Its code is spelled out as 1 rather than derived, because AGP rejects 0.

A dispatched run has no tag to read, so it takes a version input, which is also
how a release whose upload half failed gets finished off the branch it was cut
from. Without either it can only be a dry run. The laptop path keeps working
the same way: fastlane android deployPro version:v4.8.0, or ODR_VERSION in the
environment.

This drops the versionCode out of AndroidManifest.xml, which f-droid's
UpdateCheckData greps for update detection. That metadata needs rewriting
regardless - its build recipe seds the java files the kotlin migration and the
package rename removed, and it has been stuck at 4.3 since December.

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

@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: 2c00097b44

ℹ️ 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 .github/workflows/release.yml Outdated
andiwand and others added 3 commits July 27, 2026 20:52
Two things the review caught in the release workflow:

A versionless dry run put 0.0.0 in -Podr.version rather than leaving the
property off, and that name derives to version code 0, which AGP rejects. The
fallback only applies when the property is absent, so the one path that is meant
to run without a version was the one path that could not build. The step now
leaves the output empty and the gradle line omits the property with it.

Dispatched against a tag with a different version input, the input won, but the
attach job still puts the apk on the release of the tag the run was started
from - so the release would end up carrying an apk of a version it is not. On a
tag the tag now decides, and an input that disagrees with it fails the run
instead of being quietly overridden.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgR3TTDbVm7HLybtCMDT15
The step had grown three error paths, a normalisation and a regex, and none of
it could be run without pushing it - checking the behaviour meant copying the
shell into a scratch file and testing that instead, which is a copy that can
disagree with what actually runs.

It is now .github/scripts/resolve-version.py, next to the service account key
script and written for the same reason: fail before the six minutes of setup and
building, with a message that names what is wrong. It takes the tag, the version
input and what the run publishes, and writes the result to GITHUB_OUTPUT - empty
when there is none, so the gradle line keeps leaving the property off entirely
rather than passing 0.0.0. Run it by hand to see what a dispatch would build.

The step moves behind the checkout it now needs, and takes the tag name and the
version input through the environment rather than interpolating them into the
run: line.

The version code is deliberately not computed there. app/build.gradle derives it
and is the one that counts; the script only checks the shape, so that a typo in
a dispatched version does not cost the conan setup first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgR3TTDbVm7HLybtCMDT15
The two remaining run: blocks that were more than a command: the keytool checks
and the "is everything signed" pass. Both are about running a tool and reading
its exit code, so both stay shell rather than becoming python.

verify-keystore.sh takes the same ODR_* variables the build takes, so the check
the release runs can be run against a keystore by hand, which is the whole point
of it - it exists to say which password is wrong before six minutes of building.

verify-signed.sh takes the outputs directory, so it can be pointed at a local
build. Being able to run it turned up two things the inline version had:

An unmatched glob left the loops iterating over nothing and passing, so a wrong
path verified nothing and said so with a "signed:" for every file it did not
look at. It now counts what it found first.

apksigner exits non-zero both for an unsigned apk and for not being able to run,
and only the first was reported - "not signed" is a poor way to learn that the
runner has no java. What it said is now part of the message.

And one the extraction introduced and the tests caught: grep -q stops at the
first match, and the SIGPIPE unzip is left with fails the pipeline under
pipefail, so a signed bundle came out unsigned whenever unzip had not finished
writing. The listing is read once into a variable now.

Left inline: everything that is a fixed sequence without branches - decoding the
keystore, the gradle invocation, the fastlane lanes, the udev rules in
build_test. A script would only hide what those runs do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgR3TTDbVm7HLybtCMDT15
@andiwand
andiwand enabled auto-merge (squash) July 27, 2026 19:05
@andiwand
andiwand merged commit 3212213 into main Jul 27, 2026
7 checks passed
@andiwand
andiwand deleted the version-from-tag branch July 27, 2026 19:10
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