Skip to content

ci(mobile): stop OTA workflow from wiping CodePush release history#14339

Merged
dylanjeffers merged 1 commit into
mainfrom
fix/mobile-ota-create-history
May 15, 2026
Merged

ci(mobile): stop OTA workflow from wiping CodePush release history#14339
dylanjeffers merged 1 commit into
mainfrom
fix/mobile-ota-create-history

Conversation

@dylanjeffers
Copy link
Copy Markdown
Contributor

@dylanjeffers dylanjeffers commented May 15, 2026

Summary

The OTA jobs in .github/workflows/mobile.yml call code-push create-history on every push and every workflow_dispatch. That command writes a fresh history JSON containing only the binary-version placeholder and uploads it to s3://.../mobile-ota/histories/<platform>/<channel>/<binary>.json, overwriting every prior entry. The subsequent code-push release step then reads that wiped file and writes it back with just [placeholder, latest_OTA], so each run silently drops every previously-published OTA bundle for that binary version.

User-facing symptom: cycling

On RC, every push to main triggers a new OTA run. While each run is in flight, the history file sits in one of two bad states:

  1. Wipe window (~3–5 minutes between create-history upload and release upload): the file holds only the binary placeholder. A device opening the app and running CodePush.sync() during this window fetches a history whose "latest enabled release" is the placeholder (empty downloadUrl). That's treated as "no update available," and CodePush has no record of the OTA the device already has pending — so the pending-update banner (packages/mobile/src/components/ota-update-banner/OtaUpdateBanner.tsx:75) goes away.

  2. Concurrent runs racing on the same JSON: when two pushes land close together, both call getReleaseHistory → setReleaseHistory on the same S3 path. With the wipe gone there's still a read-modify-write race; with the wipe present it's worse because a later create-history truncates back to [placeholder] before the earlier run's release step has finished its read-modify-write.

Together those two flows produce the symptom the user described — banner appears, disappears a few minutes later when the next run's create-history lands, then comes back when that run's release step writes a new bundle.

Live evidence

All four history files currently hold exactly one real OTA each, with every earlier release gone:

  • download.audius.co/mobile-ota/histories/ios/rc/1.5.179.json[1.5.179, 1.5.100776]
  • download.audius.co/mobile-ota/histories/ios/production/1.5.179.json[1.5.179, 1.5.100775]
  • download.audius.co/mobile-ota/histories/android/rc/1.5.179.json[1.5.179, 1.5.100776]
  • download.audius.co/mobile-ota/histories/android/production/1.5.179.json[1.5.179, 1.5.100775]

Why removing the call is safe

packages/mobile/OTA_UPDATES.md:55 describes create-history as a one-time init when shipping a new native binary, not part of the OTA release loop. And the client (packages/mobile/src/app/ota-updates.ts:94) already substitutes a no-update placeholder when the history JSON is missing or empty, so the call isn't needed to keep CodePush from throwing "There is no latest release." On a brand-new binary version code-push release will see a 404 from getReleaseHistory, return {}, append its entry, and create the file from scratch with just the new OTA — which the client treats correctly.

Concurrency group

Adds a per-platform concurrency group on both OTA jobs (mobile-ota-release-rc-<platform> and mobile-ota-release-production-<platform>). With the wipe removed, each run still does a read-modify-write on the history JSON; an unserialized race between two pushes landing close together would let a later writer clobber an earlier release's entry. cancel-in-progress: false queues subsequent runs rather than killing a publish mid-upload.

Out of scope

Test plan

  • Workflow YAML parses (validated locally with python3 -c "import yaml; yaml.safe_load(...)").
  • Land this, then push a no-op commit (or workflow_dispatch on rc) and confirm code-push release no longer prints a wipe step — only the release/upload — and the resulting histories/ios/rc/1.5.179.json keeps the previous OTA entry and appends the new one.
  • Repeat with workflow_dispatch ota_channel=production and confirm the production history file grows instead of resetting.
  • Trigger two pushes in quick succession and confirm the second OTA waits (concurrency queue) instead of overlapping the first.
  • Observe RC devices over a few OTA cycles and confirm the pending-update banner stays sticky once shown (no more disappear-then-reappear).

🤖 Generated with Claude Code

The OTA jobs called `code-push create-history` on every push and every
workflow_dispatch. That command writes a fresh history JSON containing
only the binary-version placeholder and uploads it to
s3://.../mobile-ota/histories/<platform>/<channel>/<binary>.json,
overwriting every prior entry. The subsequent `code-push release` step
then read that wiped file and wrote it back with just
`[placeholder, latest_OTA]`, so each run silently dropped every
previously-published OTA bundle for that binary version.

Live state today across all four files (verified via
download.audius.co/mobile-ota/histories/{ios,android}/{rc,production}/1.5.179.json):

  iOS rc        -> [1.5.179, 1.5.100776]
  iOS prod      -> [1.5.179, 1.5.100775]
  Android rc    -> [1.5.179, 1.5.100776]
  Android prod  -> [1.5.179, 1.5.100775]

Per packages/mobile/OTA_UPDATES.md, `create-history` is meant to be a
one-time init when shipping a new native binary, not part of the OTA
release loop. The client (packages/mobile/src/app/ota-updates.ts)
already substitutes a no-update placeholder if the history JSON is
missing or empty, so the call isn't needed to keep CodePush from
throwing "There is no latest release."

Also adds a per-platform concurrency group so two pushes landing close
together can't race on the same S3 history file: with the wipe gone,
each run still does a read-modify-write on the history JSON, and an
unserialized race would let a later writer clobber an earlier release's
entry. `cancel-in-progress: false` queues subsequent runs instead of
killing an in-flight publish mid-upload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 15, 2026

⚠️ No Changeset found

Latest commit: a6ef73b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@dylanjeffers dylanjeffers merged commit 28293fd into main May 15, 2026
3 checks passed
@dylanjeffers dylanjeffers deleted the fix/mobile-ota-create-history branch May 15, 2026 22:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant