Skip to content

fix(release): monotonic Android versionCode, defer the iOS Wallet extensions - #2975

Merged
innolope-dev merged 3 commits into
devfrom
fix/release-native-blockers
Sep 4, 2026
Merged

fix(release): monotonic Android versionCode, defer the iOS Wallet extensions#2975
innolope-dev merged 3 commits into
devfrom
fix/release-native-blockers

Conversation

@innolope-dev

Copy link
Copy Markdown
Collaborator

Unblocks the v1.2.0 store release. The first Release Native run (33862755029) failed on both platforms; nothing reached either store and the tag job was skipped, so v1.2.0 is not burned.

Android — versionCode went backwards

Play refused the upload:

You cannot rollout this release because it does not allow any existing users to upgrade to the newly added APKs.

The build shipped versionCode 10001 against the live 10048. The cause:

export ANDROID_VERSION_CODE=$((10000 + GITHUB_RUN_NUMBER))

Its comment asserts run_number is monotonic. That held while android-release.yml was dispatched directly (run #48 → 10048), but inside a workflow_call, github.run_number is the caller's counter. release-native.yml is 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:

version versionCode
1.1.0 1001000
1.2.0 1002000
1.3.0 1003000
2.1.0 2001000

iOS — Wallet extensions break the archive

error: Multiple commands produce '…/UninstalledProducts/iphoneos/.appex'
error: Provisioning profile "Peanut Wallet Provisioning Extension UI App Store"
       doesn't include the Associated Domains capability

The four extension build configurations set INFOPLIST_FILE but no PRODUCT_NAME, while the App target has PRODUCT_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.wallet app group, and the extension-profile plumbing in the release workflow. Reverted to 216e89f84 (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:

Binaries built without the MeaWallet SDK — and OTA'd JS on older binaries — don't have it, so every caller treats "unavailable" as false and falls back to the manual add-to-wallet carousel.

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.

…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.
@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
peanut-wallet Ready Ready Preview Sep 4, 2026 1:48pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 92bbf5b7-3aec-4ad9-a074-68b0cda44367

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7136.05 → 7136.05 (0)
Findings: 0 net (+0 new, -0 resolved)

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 5745 ran, 0 failed, 0 skipped, 1.8m

📊 Coverage (unit)

metric %
statements 74.5%
branches 59.8%
functions 68.0%
lines 75.4%
⏱ 10 slowest test cases
time test
🐢 9.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Network failure keeps loading while retries remain, then shows the generic error
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_SOURCE_OVER_MONTHLY_CAP fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_MERCHANT_RECENT_REFUND fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › User KYC not approved fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › routes the KYC rejection on its wire code, and does not retry it
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › a refused idempotency key tells the user to scan again, not to contact support
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_MERCHANT_VOLUME_NEAR_CAP fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_USER_NOT_PROVISIONED fails fast with copy that names the real cause
3.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Going offline blames the connection, and reconnecting clears it for the recovered scan
3.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Scan that recovers on the retry lands on the payment screen, not an error
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.yml leaves 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

Comment thread .github/workflows/android-release.yml Outdated
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.

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.
@innolope-dev

Copy link
Copy Markdown
Collaborator Author

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: run_number was the caller's counter inside a workflow_call; run_number*10 + run_attempt held within one run lineage but not across an out-of-order re-run, which is exactly the sequence you describe.

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:

dispatch 1  10:00Z -> 21290400
dispatch 2  11:00Z -> 21294000
re-run of 1 12:00Z -> 21297600   (highest, because it uploads last)

Moved into scripts/android-version-code.mjs so the arithmetic is testable at all — inline shell could not have covered that ordering. 8 tests, including the pre-epoch clock and the Play ceiling, both of which fail closed rather than emitting a code the upload would reject.

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 dev).

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@innolope-dev
innolope-dev merged commit 3a3b543 into dev Sep 4, 2026
23 checks passed
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