Skip to content

ci(release): couple native releases to the production OTA lane - #2806

Merged
abalinda merged 1 commit into
devfrom
ci/couple-native-ota-lanes
Aug 24, 2026
Merged

ci(release): couple native releases to the production OTA lane#2806
abalinda merged 1 commit into
devfrom
ci/couple-native-ota-lanes

Conversation

@innolope-dev

Copy link
Copy Markdown
Collaborator

TASK-21793. Follow-up to #2795 (which fixed the silent-no-op bug class for the dev→staging lane and predicted this incident in its own PR body).

The incident

Internal store builds shipped with versionName 1.0.53 while the newest production OTA bundle was 1.0.51. Capgo's disable_auto_update_under_native rule rejects any bundle sorting below the installed native version, so every device on the new binaries refused all existing bundles — OTA silently dead for the whole beta fleet (102 devices / 1,002 refusal events in 30 days; 26 devices stuck on July-29 JS), with green CI throughout. The immediate remediation (ota-1.0.54 on the 08-21 build commit) already shipped; this PR makes the failure class impossible to repeat silently.

Changes

  • android-release.yml / ios-release.yml — three new pieces, kept in lockstep across the two platforms:
    • Resolve release versionName up front (input > tag > package.json, the same precedence both workflows already used inline), so every later step sees the exact version the binary ships with.
    • Check production OTA floor before the long build: read channel currentBundle production from Capgo and compare against the versionName with scripts/semver-newer.mjs. Unreadable channel or unparseable version fails the job — a binary that may strand the fleet does not ship on a guess.
    • Publish matching production OTA bundle after the store upload, only when the binary would leapfrog the newest bundle: upload the release's own out/ (the exact JS baked into the binary) under the binary's versionName, then assert the channel actually serves it. The lanes can no longer drift apart; a failed publish turns the job red instead of the fleet going dark. Both platforms publishing the same version from the same tag is a verified no-op via --version-exists-ok.
  • scripts/semver-newer.mjs — prerelease-aware "sorts strictly above" comparison. sort -V gets the case that matters here backwards: 1.0.54-hotfix1 sorts below 1.0.54 in semver (and in Capgo's on-device check), above it in GNU version sort. Fails loudly on unusable input so a bash -e step dies rather than misreading an error as "false".
  • capgo-deploy.yml / docs/NATIVE-RELEASE.md — the "production OTA fires on an ota-* tag and nothing else" design notes now document this one deliberate exception.

Task items covered elsewhere

  • Post-upload assertion on the production ota-* lane: already live — fix(capgo): give every OTA upload a unique bundle version #2795's "Verify channel serves the new bundle" step merged unconditional (no channel guard), so it ran on the ota-1.0.54 remediation deploy. No change needed.
  • Sentry alert on disable_auto_update_under_native and the Capgo channel update-policy check are ops-side, tracked in TASK-21793.

Verification

  • 16 new jest tests for semver-newer.mjs (plain/prerelease ordering incl. numeric identifiers, loud failure on unusable input); full scripts/__tests__ suite green (35 tests).
  • The floor-check step simulated under bash -e for all four paths: ahead → publish, covered → skip, unreadable channel → red, unparseable version → red.
  • All three workflows parse; prettier --check clean on every touched file.

A store build whose versionName outruns the newest production Capgo
bundle strands its whole fleet: disable_auto_update_under_native makes
every device on the new binary refuse all existing bundles, silently
(TASK-21793 — 102 devices refused OTA for a month while CI stayed
green, because internal builds shipped 1.0.53 over bundle 1.0.51).

android-release.yml / ios-release.yml now resolve the versionName up
front, read the production channel's current bundle before the build,
and — when the binary would leapfrog it — publish the release's own
static export under the binary's versionName after the store upload,
then assert the channel actually serves it. A native release can no
longer outrun the OTA lane; if the publish fails, the job goes red
instead of the fleet going dark.

scripts/semver-newer.mjs does the prerelease-aware comparison (sort -V
gets 1.0.54 vs 1.0.54-hotfix1 backwards, which is exactly the case
that matters here).
@innolope-dev innolope-dev self-assigned this Aug 24, 2026
@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview Aug 24, 2026 2:09pm

Request Review

@innolope-dev

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 27 days. After that, they cost $0.25 per reviewed file.

Or wait 53 minutes for your next included review.

View limit details

Limit details: You’ve used all 2 included reviews currently available. Your 53 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 50377b9a-ce09-4471-8e83-d95797dfb02c

📥 Commits

Reviewing files that changed from the base of the PR and between f5dfbc5 and f49807c.

📒 Files selected for processing (6)
  • .github/workflows/android-release.yml
  • .github/workflows/capgo-deploy.yml
  • .github/workflows/ios-release.yml
  • docs/NATIVE-RELEASE.md
  • scripts/__tests__/semver-newer.test.js
  • scripts/semver-newer.mjs
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/couple-native-ota-lanes

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

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor
Action performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 3488 ran, 0 failed, 0 skipped, 1.1m

📊 Coverage (unit)

metric %
statements 68.3%
branches 53.6%
functions 59.4%
lines 69.1%
⏱ 10 slowest test cases
time test
4.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.0s src/hooks/query/__tests__/user.test.tsx › does NOT clear a token that rotated mid-request (stale 401 racing a fresh login)
1.0s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.8s src/utils/__tests__/crisp.test.ts › resets the native session on logout once support has been opened
0.5s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.5s src/utils/__tests__/crisp.test.ts › settles, and hands back a usable plugin, against a real-shaped plugin proxy
0.5s src/utils/__tests__/crisp.test.ts › configures once across repeated support opens
0.4s src/utils/__tests__/crisp.test.ts › retries configuration on the next open after a failure
0.4s src/utils/__tests__/auth-token.test.ts › is none — never guarded — when only the guarded marker is present
0.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

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

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@abalinda

Copy link
Copy Markdown
Contributor

Decision: merge this PR. The mechanism is right — a native release that would leapfrog the production bundle now publishes its own out/ under the same version and asserts the channel serves it, so the incident class cannot repeat silently on releases cut from this workflow.

One thing this PR does not cover that we need before the next native build: bump package.json to ≥ 1.0.55 (on dev, main, and mobile-release).

Why: package.json is 1.0.53 and the production channel now serves ota-1.0.54 (the 08-24 remediation, JS from the 08-21 commit). A native build cut today without a bump gets versionName 1.0.53, the floor check reports it as "covered", no bundle is published — and on first launch those devices download bundle 1.0.54, which is older JS than what the binary bakes in. Silent downgrade. The "equal-or-below the current bundle = error" rule that closes this properly can be a follow-up; the bump is what prevents it right now.

Follow-ups, not merge gates: gate the auto-publish on track == production + a v* tag (a manual dispatch on a feature branch currently publishes that branch's JS to every device), re-read currentBundle right before publishing, anchor the version parse to the channel line, and pin @capgo/cli. Also note this guard only runs on releases cut from commits that contain it — it needs to reach main and mobile-release too.

@abalinda
abalinda merged commit 9d47eea into dev Aug 24, 2026
20 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.

2 participants