fix(release): monotonic Android versionCode, defer the iOS Wallet extensions - #2975
Conversation
…ensions The first Release Native run failed on both stores. Android built a valid AAB and Play refused the upload: 'does not allow any existing users to upgrade'. versionCode came from $((10000 + run_number)), whose comment claims it is monotonic — true only while android-release.yml was dispatched directly. Inside a workflow_call, github.run_number is the CALLER's counter, so the brand-new release-native.yml numbered itself 1 and shipped 10001 against the live 10048. Derive it from the resolved version instead, the one counter that is monotonic across every entry point, and fail closed below the known floor. iOS never archived: the two Wallet extension targets carry no PRODUCT_NAME, so both resolved to an empty product and xcodebuild hit 'Multiple commands produce .appex'; behind that, the extension profiles lack the Associated Domains capability, which is Apple portal work. Neither is reachable from the flag that gates push provisioning, because the targets sit in the archive path unconditionally. Revert the iOS native half of #2754 to its pre-merge state — extension targets, the group.me.peanut.wallet app group, and the extension-profile plumbing. #2919's 16.4 deployment floor is preserved. The JS half stays and degrades exactly as designed: registerPlugin's caller treats a missing native plugin as unavailable and keeps the manual carousel. Push provisioning was already dark pending MeaWallet secrets and Google onboarding, so this costs nothing that was reachable, and re-landing it is a matter of restoring these four files once the profiles exist.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Code-analysis diffPainscore total: 7136.05 → 7136.05 (0) |
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
There was a problem hiding this comment.
Chip review — changes requested
One blocking release-recovery defect remains: Android now reuses the same Play versionCode for every upload attempt of a resolved native version. The iOS extension rollback and exact-head functional CI are otherwise clean.
Findings
- BLOCKING · .github/workflows/android-release.yml:242 · Keep versionCode unique when retrying a release
release-native.ymlleaves the tag unwritten whenever a downstream job fails, so the next run deliberately resolves the same native version. If Play accepts 1.2.0 as versionCode 1002000 and the later Capgo or artifact step then fails, the recovery run reaches this line with 1.2.0 again and emits 1002000 again; Google Play forbids uploading a versionCode that was already used, so the workflow fails before it can repair the original failure. Keep the semantic-version-derived floor, but add a durable per-upload monotonic component (or allocate the next code from Play) and cover two upload attempts for the same native version.
Checked clean
- Confirmed the detached worktree HEAD, PR author, dev base ref, and exact base/head SHAs match the supplied review target.
- Compared the iOS workflow, Xcode project, and app entitlements with post-#2919 commit 216e89f; the rollback matches that known-good state exactly and preserves the iOS 16.4 floor.
- Traced the retained push-provisioning JavaScript path; an absent iOS native plugin is caught and falls back to the manual wallet flow.
- Checked Android version parsing and arithmetic: normal 1.2.0 resolves to 1002000 and clears the known Play floor, with digit-only input validation ahead of the shell calculation.
- Reviewed release workflow trust boundaries and secret handling; the action pin is unchanged and the iOS rollback removes unused extension credentials without exposing values.
- Exact-head unit, typecheck, eslint, format, native-export, design-system, preview, authorship, and review checks are green; aggregate report and analysis checks were still running at review time.
- Ran git diff whitespace validation successfully.
Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.
Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.
Exact head: c1dee64cfa69 · Context: repo, ci, web · Took 8m
The version-derived code alone is not unique. release-native.yml leaves the tag unwritten when any downstream job fails, so a recovery run deliberately resolves the SAME version — and if the original run had already uploaded to Play before failing later (the Capgo publish, or the sibling iOS job), the retry re-emits a versionCode Play has seen and is rejected, wedging the release the re-run exists to repair. Add attempt digits below the build: run_number covers a fresh dispatch, run_attempt covers 'Re-run failed jobs' where run_number does not move. Both only increase, so successive uploads of one version strictly increase. Spacing widens to 10000 per build so the attempt component can never reach the next build, with a guard that fails closed instead of letting a wrap silently invert the ordering.
There was a problem hiding this comment.
Chip review — changes requested
The iOS rollback is internally consistent and exact-head CI is green where complete, but Android release retries can still emit a lower versionCode when an older run is rerun after a newer dispatch.
Findings
- BLOCKING · .github/workflows/android-release.yml:263 · Keep retries monotonic across run order
GitHub preserves run_number when rerunning an older workflow and only increments run_attempt. If dispatch 1 uploads 10020010 and a fresh dispatch 2 later uploads 10020020, selecting Re-run failed jobs on dispatch 1 computes 10020011 here. Play has already seen the higher code and rejects the recovery upload, so the retry wedge remains. Derive the code from a value monotonic at upload time (or reject stale-run reruns and require a fresh dispatch), and cover this out-of-order sequence in a deterministic test.
Inline anchors unavailable for 1 finding(s); the findings remain in this summary.
Checked clean
- Verified the detached worktree HEAD and merge base against the supplied SHAs; the trusted author and dev base match.
- Confirmed the iOS Xcode project and app entitlements are byte-for-byte identical to pre-wallet commit 216e89f, while the workflow removes the matching extension profiles and export mappings.
- Exercised the Android arithmetic for a sequential fresh dispatch and a same-run rerun; those orderings increase as intended.
- Exact-head unit, typecheck, lint, format, design-system, and native-export checks passed; analyze, report, and preview were still running when reviewed.
Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.
Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.
Exact head: 86cf72b0aaaf · Context: repo, github-actions-docs · Took 6m
…umbering Chip is right that the attempt digits did not close the retry wedge, only narrowed it. GitHub preserves run_number when you re-run an OLDER workflow and bumps only run_attempt, so: dispatch 1 uploads ...010, dispatch 2 uploads ...020, then Re-run failed jobs on dispatch 1 computes ...011 — below what Play already has. Play refuses the recovery upload and the wedge survives, which is the failure the attempt digits were added to prevent. The pattern across all three attempts is that each one was monotonic by an argument about GitHub's numbering, and each argument had a case it did not cover. Wall-clock seconds are monotonic by construction: whatever order runs are dispatched, re-run or interleaved, the upload that happens last carries the highest number. Moved into scripts/android-version-code.mjs so the arithmetic is testable — the out-of-order sequence above is now a deterministic test, which inline shell could not have been. Fails closed on a pre-epoch clock or a code past Play's ceiling rather than emitting one the upload would reject. The cost, stated plainly: versionCode no longer encodes the version. That structure is exactly what cannot survive an out-of-order re-run, so it had to go. versionName is what humans read in Play, and versionCode is the opaque ordering key Google documents it as. Both are echoed in the build log so a run can still be traced to its release.
|
Fixed in cf2fc8b — you're right, and the pattern across all three attempts is the point. Each scheme was monotonic by an argument about GitHub's numbering, and each argument had a case it did not cover: versionCode is now wall-clock seconds since 2026-01-01 UTC — monotonic by construction, so whatever order runs are dispatched, re-run or interleaved, the upload that happens last carries the highest number. Your sequence is now a deterministic test: Moved into The trade-off, explicitly: versionCode no longer encodes the version, which reverses the "keep the semantic-version-derived floor" half of your first review. That structure is precisely what cannot survive an out-of-order re-run, so it had to go. versionName carries the human version in Play, versionCode is the opaque ordering key Google documents it as, and both are echoed in the build log so a run still traces to its release. Happy to revisit if you see a way to keep the version encoded that stays ordered under re-runs — I could not find one that did not need external state (allocating from Play, or a counter CI cannot write to |
There was a problem hiding this comment.
Chip review — no blocking findings — this is not an approval
Clean at the pinned head. The timestamp-derived Android versionCode fixes retry uniqueness and ordering, and the iOS extension rollback preserves the intended post-#2919 project state.
Checked clean
- Verified the detached worktree head and merge base exactly match the supplied head and base SHAs.
- Checked Android versionCode generation, legacy-floor and Play-maximum guards, CLI failure paths, Gradle environment wiring, and release concurrency behavior.
- Checked the older-run retry scenario: the code now derives from execution time rather than run_number or run_attempt, so a later retry receives a higher code on the serialized release paths.
- Verified the four iOS rollback files match the known post-#2919 state and that binaries without the removed plugin fall back to the existing unavailable path.
- Exact-head CI completed green, including unit, native-export, typecheck, format, ESLint, analysis, and aggregate reporting. Focused local Jest was unavailable because the detached worktree has no installed dependencies.
- Reviewed workflow permissions, action pins, and secret handling; this change adds no permission or secret exposure surface.
Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.
Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.
Exact head: cf2fc8b2b092 · Context: repo · Took 10m
Unblocks the v1.2.0 store release. The first
Release Nativerun (33862755029) failed on both platforms; nothing reached either store and thetagjob was skipped, sov1.2.0is not burned.Android — versionCode went backwards
Play refused the upload:
The build shipped versionCode 10001 against the live 10048. The cause:
Its comment asserts
run_numberis monotonic. That held whileandroid-release.ymlwas dispatched directly (run #48 → 10048), but inside aworkflow_call,github.run_numberis the caller's counter.release-native.ymlis new, so its counter started at 1 and the versionCode fell 47 behind. This would have hit the first Release Native run regardless of contents.Now derived from the resolved version — the only counter monotonic across every entry point — with 1000/1000000 spacing (999 builds per major, 999 OTAs per build) and a guard that fails closed below the known 10048 floor:
iOS — Wallet extensions break the archive
The four extension build configurations set
INFOPLIST_FILEbut noPRODUCT_NAME, while the App target hasPRODUCT_NAME = "$(TARGET_NAME)"— so both extensions resolved to an empty product name and collided. Behind that, the extension profiles lack Associated Domains, which is Apple Developer portal work and cannot be fixed in the repo.Neither failure is reachable from the PostHog flag that gates push provisioning: the targets sit in the archive path unconditionally, so they break the build rather than lying dormant.
This reverts the iOS native half of #2754 to its pre-merge state — extension targets, the
group.me.peanut.walletapp group, and the extension-profile plumbing in the release workflow. Reverted to216e89f84(post-#2919), so #2919's 16.4 deployment floor is preserved — verified, 4 occurrences intact.Everything else from #2754 stays. The JS half degrades exactly as its own comment says it should:
Push provisioning was already dark behind the MeaWallet secrets and Google onboarding, so this costs nothing that was reachable. Re-landing it is restoring these four files once the profiles exist.
What v1.2.0 carries
Native (must be in the binary): #2968's Capgo updater 8.51.14 — the fix for the Android OTA restart deadlock, so later OTAs don't white-screen — and #2919's iOS 16.4 floor, native Sentry request capture off, and
ACCESS_NETWORK_STATE. Everything else, including all of #2973, is JS and rides this binary's baked bundle or a later OTA.