Build once, upload per flavor, and record what actually shipped - #568
Open
andiwand wants to merge 2 commits into
Open
Build once, upload per flavor, and record what actually shipped#568andiwand wants to merge 2 commits into
andiwand wants to merge 2 commits into
Conversation
A tag had to be pushed before the run it named, which is a promise the run can fail to keep. v4.9.0 is the case in point: its tag push run failed and the upload came from a dispatched run instead. The same commit that time, but nothing made it so - a dispatch off main builds whatever main is at, and a release uploaded from the fix-play-store-upload branch has no tag naming it anywhere. So the tag stops being the trigger and becomes the record. release.yml is dispatch-only, and writes build/<flavor>/<version> at the commit it uploaded, as each lane lands rather than once at the end: the half uploaded release is the case the record exists for, and a lite failure must not take pro's tag down with it. Per flavor for the same reason - the two halves get finished from different commits. The plain v* tag stays a human decision, taken once the release is actually live, and no longer builds anything. What it does run is attach-apk.yml, which is the old attach job moved out: it fetches the pro apk from the run that uploaded it and puts it on the github release. Attaching had to move anyway, since it keyed off a trigger tag that no longer exists, and it is better placed here - it now attaches the apk that went to play rather than one built alongside it, and it refuses a version tag that does not sit on the commit build/pro/<version> names. resolve-version.py loses its tag arm; the input is the only source. CHANGELOG.md gets the matching rule: the heading is cut when the version is dispatched, not when it is tagged, because a version code can only be uploaded once and no later commit can ever ship under it. Untested end to end, since a real run uploads to the play store. Both workflows parse, the run blocks are shellcheck clean, the tag round trip and the flavor loop were exercised in a scratch repo, and resolve-version.py was run across all its cases. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RsdTta67FZ4KeNfPPw5BVL
Follows the same change in OpenDocument.ios. The release workflow was one job that built both flavors and uploaded them in a loop, with an `uploads` input to pick a flavor when one half failed. That input is gone, and so is the loop. Both flavors always go out together now. Lite and pro are the same app with ads and tracking switched off, so anything worth rebuilding one for is worth rebuilding the other for - and once one run builds both from one checkout, a version's build tag names a single commit. That is what the v* tag and f-droid build from, so it is worth more than the flexibility it costs. Build, upload and record are three jobs. A half uploaded release is repaired with "Re-run failed jobs", which retries only the failed upload against the bundle already built, signed and verified - no rebuild, so the retry cannot come out different from what its other half uploaded. The v* tag is no longer written by hand. `record` drafts a github release at the built commit carrying the pro apk; a draft creates no tag, and publishing it creates one there. That wait matters: f-droid tracks this repository with UpdateCheckMode: Tags, so a v* tag is what makes it build and ship, and one written when the bundle merely reached the internal track would push a version to f-droid users that google may never release. attach-apk.yml is deleted - every check it made is now structural. No cross-run artifact fetch, no 90 day expiry, no version-tag-vs-build-tag drift check. The release body is the version's CHANGELOG.md section with the generated pull request list below it, and a version with no section is refused before anything is built rather than after both flavors are on the store. resolve-version.py takes --dry-run instead of --uploads, so both repositories now run the same script with the same arguments. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nhfz8qVwtcDrJ6wdkrrcJr
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.
Aligned with the same change in OpenDocument.ios#135 — the two apps now describe one release design.
A version tag had to be pushed before the run it named, which is a promise the run can fail to keep.
v4.9.0is the case in point: its tag push run failed, and the upload that shipped came from a dispatched run instead. So the tag stops being the trigger and becomes the record.That much was the original scope. What changed since is everything downstream of one observation: Lite and Pro are the same app. The flavor switches ads and tracking off and nothing else, so any change worth shipping to one is worth shipping to the other.
What changes
No input chooses a flavor. The
uploadschoice is gone; inputs areversionanddry_run, the same two iOS takes. One run builds both flavors from one checkout, so a version's build tag names a single commit — which is what thev*tag and F-Droid then build from.Three jobs:
build→upload(per flavor) →record. The split is what makes a half uploaded release repairable. If Lite's upload fails alone, "Re-run failed jobs" retries just that job against the bundle already built, signed andverify-signed.sh'd. No rebuild, so the retry cannot come out different from what its other half uploaded. Re-running all jobs is the wrong button and fails fast on the build tag rather than six minutes in at Play.That also separates the credentials: the keystore never enters an upload job, and the Play service account key never enters the build job.
One build tag,
build/<version>, written byrecordonce both flavors are up. Not per flavor — there is only one commit to name. A half uploaded release gets no tag at all, which is the honest answer: nothing yet could be published from it.The
v*tag is written neither by hand nor by the workflow.recorddrafts a GitHub release at the built commit carrying the Pro APK. A draft creates no tag; publishing it creates one there:That wait is not cosmetic. F-Droid tracks this repository with
UpdateCheckMode: TagsandAutoUpdateMode: Version, so av*tag is what makes F-Droid build and ship. One written when the bundle merely reached the internal track would push a version to F-Droid users that Google may never release.--targetis the SHA rather than a branch for the same family of reason: a branch name resolves when the draft is published, days later.attach-apk.ymlis deleted. Every check it made is now structural — no cross-run artifact fetch, no 90-day expiry check, no "the release must exist already", and no version-tag-vs-build-tag drift check, because publishing the draft is the tag.The release body is the
CHANGELOG.mdsection for the version, with GitHub's generated pull request list appended below it (--generate-notesappends to--notes-file). A version with no section is refused in the run's first seconds, before anything is built — new.github/scripts/changelog-section.py. That gives the changelog a second consumer, so it cannot silently rot.resolve-version.pytakes--dry-runinstead of--uploads, so both repositories now run the same script with the same arguments.The trade
A half uploaded release leaves no tag, so the pre-flight check cannot catch a fresh re-dispatch of a half-uploaded version — that fails at Play instead. The supported repair is a re-run; past the ~30 days GitHub offers re-runs for, the answer is a new patch version for both flavors. Accepted deliberately: it buys one tag family instead of two in a repository F-Droid scans by tag.
Testing
No end-to-end run is possible — a real dispatch uploads to Play.
actionlintis clean (shellcheck included),resolve-version.pyandchangelog-section.pywere run across all their cases including missing and empty sections, and adry_rundispatch exercises the build, the signing, both pre-flight checks and the archiving — everything except the upload and record jobs, which are skipped by design.Load-bearing GitHub behaviours worth confirming before the first real release: that "Re-run failed jobs" re-runs a dependent job skipped because its
needsfailed, that artifacts from attempt 1 download in attempt 2, and that F-Droid's tag scanner ignores the newbuild/…tags — if not, that is a one-lineUpdateCheckMode: Tags ^v[0-9.]+$change in fdroiddata, not here.Separately and not addressed here: F-Droid's metadata currently reads
CurrentVersion: '4.3'/CurrentVersionCode: 199against an app at 4.13 / 41300, so it looks badly behind.🤖 Generated with Claude Code