Skip to content

fix(mobile,ci): build releases from the lockfile, drop Expo, cache the native compiles - #551

Merged
thomasluizon merged 2 commits into
mainfrom
fix/android-release-pipeline
Jul 16, 2026
Merged

thomasluizon merged 2 commits into
mainfrom
fix/android-release-pipeline

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Rebuilds the Android release path around one idea: ship the tree the repo tests.

Closes #550. Supersedes #547 and #548 (both closed in favour of this).

The release did not build what we test

The job copied apps/mobile into an isolated workspace and installed with --package-lock=false, resolving every dependency fresh from the registry at release time. The AAB was compiled from a tree no local install or CI job ever had.

#545 pinned the 57 native modules after @expo/ui floated to 57.0.6 and failed R8 eleven minutes into a release — but that closed only half of it. 87 JS packages still drifted, including the tooling that generates the Android project:

package release resolved lockfile
@expo/cli 57.0.8 57.0.6
@expo/config-plugins 57.0.5 57.0.3
@expo/prebuild-config 57.0.7 57.0.5

npm ci at the root installs exactly the committed lockfile, so the release tree equals the tested tree by construction. No pins to maintain, no drift guard, no second lockfile.

The isolated workspace only ever existed to dodge npm hoisting. Its stated blocker — @expo/cli crashing on the tsconfig extends chain ("Failed to collapse") — does not reproduce outside the workspace it was written for: prebuild runs clean in the real monorepo with tsconfig.json untouched, which is exactly what scripts/android-release-apk.js has always done locally. So it goes, and with it prepare-release-workspace.js, verify-release-native-modules.js (tautological once npm ci guarantees the tree), and the .release-mobile indirection through every step.

Google Play, without the Expo account

eas-cli submit is not a local pass-through. At the v21.0.1 tag it uploads the AAB to Expo's GCS bucket and posts the Play service-account key to Expo's GraphQL backend, which makes the androidpublisher call for you — as npx eas-cli@latest, unpinned, fetched fresh each run, to reach an API the runner can call itself.

EAS Submit is free on every tier, so this is not about cost — it's credential custody and one less dependency.

r0adkll/upload-google-play calls that API directly, pinned to the v1.1.5 commit. The closed→alpha / open→beta mapping that lived only in eas.json becomes an explicit step that runs early, so an unmappable track fails in seconds. It also uploads mapping.txt, so R8-obfuscated Play crash reports deobfuscate. eas.json is then entirely dead (its build profiles were already vestigial), as are EXPO_TOKEN and the step that wrote the key to disk.

⚠️ EAS_BUILD_PROFILE stays despite the name — app.config.js uses it as the gate that throws on unresolved production AdMob IDs.

31m39s of a 37m13s run was Gradle

Measured on run 29517434610, the first green release since #545:

  • Stop building x86/x86_64. Nothing set reactNativeArchitectures, so all four ABIs were built and worklets/reanimated each ran CMake 4×. Play serves only the matching split per device; Google's 64-bit policy doesn't require x86. Tradeoff: ChromeOS/ARCVM loses native x86_64 → Houdini fallback ("OK" not "Best"). One line to revert.
  • ccache the NDK compiles. CMake initializes CMAKE_<LANG>_COMPILER_LAUNCHER from the environment, so no generated CMake needs editing. compiler_check=content is required on CI. Keyed on the lockfile.
  • Cache Gradle task outputs via gradle/actions/setup-gradle. setup-java's cache: gradle cached only dependencies and must not run alongside it, so it's removed.

Faults found in the same path, fixed not filed

  • Every release has minified under the wrong heap. The plugin sets -Xmx6144m/2048m and claims it forces them "authoritatively" — but configure-android-release-signing.js runs after prebuild and upserted it back to -Xmx4g/1024m. The plugin is now the sole writer. workers.max=2 left alone — correctly sized for this heap.
  • SENTRY_AUTH_TOKEN missing from the secrets guard while the sourcemap upload runs inside Gradle (surfaced ~15min in, not 1s).
  • set-app-version.js patched build.gradle behind an existsSync guard its only caller can never satisfy (runs before prebuild creates android/).
  • scripts/android-release-device.js: zero references repo-wide.

Net −384 lines.

Verified locally, against the real tree, with production AdMob env

  • expo config + the AdMob assertion pass
  • prebuild finishes clean in place
  • generated manifest carries the production AdMob app ID, no sample IDs
  • expo-modules-autolinking resolves orbit-widget (43 modules) from the real tree — the widget still reaches the manifest merge

Real prebuild output:

org.gradle.jvmargs=-Xmx6144m -XX:MaxMetaspaceSize=2048m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError
reactNativeArchitectures=armeabi-v7a,arm64-v8a

against armeabi-v7a,arm64-v8a,x86,x86_64 on the same command without buildArchs.

Plugin jvmargs covered by a new test that fails on a heap regression (confirmed by reintroducing it). tsc, expo lint, mobile suite (244 files / 1589 tests) pass.

Not provable without a release

The Gradle build, signing, and the Play upload itself. Dispatch to the internal track before an open-track release — the first run is a guaranteed ccache miss and will not be faster.

Parity: Android-native release machinery; apps/web has no counterpart.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DDueAWEMzgcryVDyG7yBq2

@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
orbit-ui-mobile-web Ignored Ignored Jul 16, 2026 6:30pm

Request Review

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review: PR #551 — fix(mobile,ci): build releases from the lockfile, drop Expo, cache the native compiles

Scope: PR #551 in thomasluizon/orbit-ui-mobile (.github/workflows/android-release.yml + apps/mobile/{app.json,eas.json,plugins,scripts,__tests__})
Recommendation: NEEDS WORK

Summary

This PR replaces the isolated-workspace Android release build with a root npm ci install, replaces eas-cli submit with a direct r0adkll/upload-google-play Play Developer API call, adds ccache + Gradle build-cache + reactNativeArchitectures restriction for build speed, and fixes a real bug where configure-android-release-signing.js was clobbering the JVM heap size the release plugin set. The dead workspace/verification/device scripts and eas.json are cleanly removed with no dangling references left anywhere in the repo. One High-severity issue: the new Google Play upload step's with: block passes tracks: (plural) to an action whose declared input is track (singular) in every version of r0adkll/upload-google-play this reviewer is aware of — which would mean the per-dispatch track selection (the whole point of the new "Resolve Google Play track" step) never reaches the upload action.

Findings

Critical

None.

High

[HIGH] tracks: is very likely the wrong input key for r0adkll/upload-google-play, so the resolved release track may never reach the upload step
· dimension: 1 — Correctness
· location: .github/workflows/android-release.yml:472-479 (the "Submit build to Google Play" step)
· issue: The step resolves the dispatched track into env.PLAY_TRACK (a new, well-designed step) and then passes it as tracks: ${{ env.PLAY_TRACK }} to r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 (v1.1.5). Every published version of this action I'm aware of declares its release-track input as track (singular) — releaseFiles is plural, track is not. If the action's action.yml at this pinned commit really only declares track, GitHub Actions logs an "Unexpected input(s) 'tracks'" warning and continues, and the action never receives the resolved value — it either falls back to its own default/required-input error, or a hardcoded track, independent of what inputs.track (internal/closed/open/production) the human actually dispatched.
· risk: A release dispatched to internal or closed could silently upload to whatever track the action defaults to instead — worst case, straight to production for real users, with the workflow reporting green. The PR's own body flags this exact step as "Not provable without a release" (unlike the rest of the diff, which was verified locally against the real tree), so this is precisely the part of the change most likely to hide a bug until a real release runs.
· fix: Confirm the exact input name in action.yml at commit e738b9dd8f2476ea806d921b64aacd24f34515a5 (https://github.com/r0adkll/upload-google-play/blob/e738b9dd8f2476ea806d921b64aacd24f34515a5/action.yml) before merging. If it is track (singular), rename the key. Given the PR's own recommendation to dispatch to internal before an open-track release, also consider a one-line assertion/log of the track the action itself reports using, so a future mismatch fails loudly instead of silently.
· reference: PR body, "Not provable without a release" section; CLAUDE.md rule 1 (root cause / correctness at the boundary this diff itself introduces).

Note on confidence: I could not reach the network from this review sandbox (no gh api/curl/WebFetch/WebSearch access was granted) to read the pinned action.yml directly, so this is reported at High rather than Critical per the rubric's "downgrade when uncertain" guidance — verify the single fact above and this either closes as a one-line fix or confirms a real release-safety bug.

Medium

None.

Low / Info

  • inputs.message's description ("Optional build message shown in Expo") is now stale — this PR removes every EAS/Expo submission step, so there is no longer anywhere for the message to be "shown in Expo." The field was already unused by any actual step before this PR (only echoed to the summary/re-run command), so this is a pre-existing dead field whose description this diff incidentally made more wrong. Not blocking; worth a follow-up line update or removal.

Subagents

Ran inline (this session had no access to spawn the specialist subagents or the network they'd typically use); folded the equivalent checks into the dimensions above.

Agent Verdict
parity-checker N/A — Android-native release/CI tooling has no apps/web counterpart (PR body states this explicitly; consistent with the precedent set by #545).
i18n-syncer N/A — no user-facing strings changed.
contract-aligner N/A — no packages/shared/src/types/* or endpoints.ts change; orbit-api not touched.
security-reviewer N/A — orbit-api not touched. Frontend-security note: this PR is a net security improvement (removes EXPO_TOKEN, stops writing the Play service-account key to disk, pins the new third-party action to a commit SHA rather than a floating tag).
design-reviewer N/A — no apps/* UI files changed.

Validation

Check Result
Lint N/A — no node_modules installed in this review sandbox and package install requires network approval not available here
Type check N/A — same reason
Tests N/A — same reason; PR body claims 244 files / 1589 tests pass, and the new with-android-release-build-fixes.test.ts was manually traced against the plugin's forceJvmFlag logic and is internally consistent (idempotency case included)
Build (api) N/A — orbit-api not touched

Deferred — N/A dimensions & files not verdicted

  • DESIGN.md/AI-slop (#8), i18n (#10), contract drift (#11), backend hard rules (#13), FEATURES.md parity (#14): all N/A — the diff never touches their surface (no UI files, no strings, no shared types, no orbit-api, no user-facing feature/gating change).
  • Live verification of the r0adkll/upload-google-play action's actual action.yml input schema at the pinned commit: not performed — this review sandbox had no approved path to the network (gh api, curl, WebFetch, and WebSearch all required a permission grant that wasn't available). This is the one open question behind the High finding above; the reviewer should close it before merge.
  • Full validation (lint/typecheck/tests) per Phase 7: not run — no installed dependencies and no approved install path in this sandbox. Relying on the PR body's stated local verification (tsc, expo lint, 1589 tests, plus the author's own real-prebuild-output evidence for reactNativeArchitectures and the AdMob/widget manifest checks).
  • Every other changed file (app.json, eas.json removal, with-android-release-build-fixes.js/.d.ts, configure-android-release-signing.js, set-app-version.js, the three deleted scripts and their test) was read in full and given a verdict: clean removals, no dangling references anywhere in the repo (grep-verified), and the JVM-heap ownership fix (plugin now sole writer of org.gradle.jvmargs) is correct and covered by a new, well-constructed test.

What's good

  • Root-causes a real, previously-invisible drift bug (13/57 native modules + 87 JS packages resolving off-lockfile at release time) by deleting the mechanism that caused it, rather than adding a verification script to guard around it — exactly the rule-1 "root cause over workaround" standard.
  • Finds and fixes a second real bug in the same path (configure-android-release-signing.js silently undoing the release JVM heap plugin's work on every CI release) and backs the fix with a genuinely useful regression test (with-android-release-build-fixes.test.ts), including an idempotency case.
  • Net −384 lines: three dead scripts and eas.json removed outright, with grep confirming zero remaining references anywhere in the repo.
  • Meaningful security hardening as a side effect: EXPO_TOKEN gone, the Play service-account key is never written to disk, and the new third-party action is pinned to a commit SHA (not a floating tag) with the version documented in a trailing comment, consistent with the file's existing convention for third-party actions.
  • Honest and specific about what wasn't (and couldn't be) verified locally — the PR body itself calls out the Gradle/signing/Play-upload path as unprovable without a real release, which is exactly where this review's own High finding lives.

Recommendation

Verify the r0adkll/upload-google-play input name (track vs tracks) against the pinned commit's action.yml before merging. If it needs to change, it's a one-line fix. Once confirmed (or corrected), this is a clean, well-verified, and well-explained release-pipeline rewrite — re-review is not expected to surface anything further; a quick internal-track dispatch to confirm the real upload behaves as intended (per the PR's own recommendation) is the remaining real-world proof point.

thomasluizon added a commit that referenced this pull request Jul 16, 2026
…review)

Review flagged that `tracks:` might be the wrong input for upload-google-play.
Checked action.yml at the pinned commit e738b9dd: `tracks` is correct, and `track`
is the deprecated one -- validateTracks warns "'track' is deprecated ... please
migrate fully to 'tracks'" -- so the suggested change would have moved this onto a
deprecated input.

The review's underlying concern is real, though, and worse than reported: when
validateTracks receives no track it does not fail, it returns ['production']. A
PLAY_TRACK that never reaches the action -- a reordered or skipped resolve step, a
GITHUB_ENV that did not carry -- would therefore publish to real users while the
workflow stayed green. The value is verified as correct today, but the failure
mode is silent and the blast radius is production, so assert it is non-empty
before the upload rather than let that default stand.

Also corrects the `message` input description, which still promised the note was
"shown in Expo" after this PR removed every Expo submission step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDueAWEMzgcryVDyG7yBq2
@thomasluizon

Copy link
Copy Markdown
Owner Author

Thanks — the High finding is resolved, and it was worth raising even though the premise turned out inverted.

tracks is correct; track is the deprecated one

You flagged you couldn't reach the network to read action.yml. Fetched at the pinned commit e738b9dd:

  track:
    description: "The track in which you want to assign the uploaded app."
    required: false
  tracks:
    description: "The track(s) in which you want to assign the uploaded app."
    required: false

Both exist. And src/input-validation.ts at the same commit shows which one is current:

export async function validateTracks(track: string | undefined, tracks: string[]): Promise<string[]> {
    if (track && tracks.length > 0) {
        return Promise.reject(new Error(`Cannot set both 'track' and 'tracks'. 'track' is deprecated — please migrate fully to 'tracks'.`))
    }
    if (track) {
        core.warning(`WARNING!! 'track' is deprecated and will be removed in a future release. Please migrate to 'tracks'`)
        return [track]
    }
    if (tracks.length > 0) {
        return tracks
    }
    return ['production']
}

So tracks: (plural) is the current input; renaming to track: would have moved this onto a deprecated one that the action itself warns about — against this repo's never-use-deprecated rule. No rename.

Your risk analysis was right, and the real default is worse than you described

Look at the last line: with no track supplied, validateTracks doesn't error — it returns ['production'].

So the catastrophic scenario you described is real, just reached by a different route than a wrong input name: any path where PLAY_TRACK fails to reach the action (a reordered or skipped resolve step, a GITHUB_ENV that doesn't carry) publishes to real users with the workflow green.

The value is verified correct today and the action is SHA-pinned so its behaviour can't drift — but the failure is silent and the blast radius is production, so that default shouldn't stand unguarded. Added in d5e23ea, immediately before the upload:

- name: Assert the Google Play track resolved
  ...
  if [ -z "${RESOLVED_PLAY_TRACK}" ]; then
    echo "::error::PLAY_TRACK is empty. upload-google-play defaults to the production track when no track is supplied, so this would publish to production."
    exit 1
  fi

Low finding

Fixed — the message input's description no longer claims the note is "shown in Expo", which this PR made untrue.

The internal-track dispatch you recommend as the real-world proof point stands; it's called out in the PR body as the remaining unprovable-without-a-release step.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review: PR #551 — fix(mobile,ci): build releases from the lockfile, drop Expo, cache the native compiles

Recommendation: APPROVE

No Critical or High findings survive this round. The one High finding from the prior review round (tracks: vs track: input key on the r0adkll/upload-google-play step) has been resolved: commit d5e23ea confirms against the pinned action.yml that tracks (plural) is the current input and track is deprecated, and additionally hardens the upload against the real underlying risk — a new "Assert the Google Play track resolved" step fails the run if PLAY_TRACK is empty, rather than letting the action silently default to the production track.

What the PR does

Rebuilds the Android release CI pipeline: builds from npm ci against the committed root lockfile instead of an isolated unpinned-install workspace, replaces eas-cli submit (which routed the AAB and the Play service-account key through Expo's backend) with the directly-pinned r0adkll/upload-google-play action, adds Gradle/ccache build caching, drops x86/x86_64 native targets, and fixes a real latent bug where configure-android-release-signing.js was silently clobbering the release JVM heap/metaspace that with-android-release-build-fixes.js had set. Pure CI/build infrastructure — no app runtime, UI, contract, or i18n surface touched.

Findings

Critical / High / Medium: none.

Info only:

  • The "Assert the Google Play track resolved" guard (.github/workflows/android-release.yml:469-481) is correctly ordered before "Submit build to Google Play" — verified the full step sequence, no gap.
  • The org.gradle.jvmargs single-writer split between the plugin and configure-android-release-signing.js:221-224 is a legitimate root-cause fix, backed by a new regression test (apps/mobile/__tests__/plugins/with-android-release-build-fixes.test.ts) that fails on the exact clobber that used to happen.
  • Two newly pinned action SHAs (gradle/actions/setup-gradle@3f131e8... and r0adkll/upload-google-play@e738b9d...) are correctly-shaped 40-char hex strings; the upload-google-play pin was independently verified against its action.yml per the author's commit message for the tracks input resolution.

Confirmed via repo-wide grep: zero leftover references to the deleted RELEASE_WORKDIR, prepare-release-workspace.js, verify-release-native-modules.js, android-release-device.js, eas.json, eas-cli, or EXPO_TOKEN.

Subagents

Agent Verdict
parity-checker N/A (exempt) — Android-native release/CI machinery, no apps/web equivalent surface
i18n-syncer N/A — no user-facing app strings changed
contract-aligner N/A — no packages/shared/src/types/*/endpoints.ts changes
security-reviewer N/A — no orbit-api changes; net security improvement (EXPO_TOKEN removed, Play service-account key no longer written to disk, action pinned to commit SHA)
design-reviewer N/A — no UI files changed

Validation

Skipped per CI adaptation — Build / Unit Tests / SonarCloud run as separate required checks on this PR. SonarCloud quality gate has already passed.

What's good

  • Fixes a real, previously-invisible bug (silent JVM heap downgrade on every CI release) with a proper regression test.
  • Removes a third-party credential hop (Play service-account key no longer travels to Expo's backend) and pins the replacement action to a commit SHA.
  • Root-cause fix (rule 1): npm ci against the lockfile replaces an unpinned isolated install and deletes the drift-verification script that install pattern required.
  • Responded to the prior review's High finding with real verification (checked the pinned action.yml) rather than a guess, and went further by hardening against the actual failure mode the finding was pointing at (silent default-to-production on an empty track).

thomasluizon and others added 2 commits July 16, 2026 15:30
…e native compiles

Rebuilds the Android release path around one idea: ship the tree the repo tests.
Closes #550.

## The release did not build what we test

The job copied apps/mobile into an isolated workspace and installed with
--package-lock=false, resolving every dependency fresh from the registry at
release time. The AAB was therefore compiled from a tree no local install or CI
job ever had. #545 pinned the 57 native modules after @expo/ui floated to 57.0.6
and failed R8 eleven minutes into run 29514193887, but that closed only half of
it: 87 JS packages still drifted, including @expo/cli, @expo/config-plugins and
@expo/prebuild-config -- the tooling that generates the Android project.

npm ci at the repo root installs exactly the committed lockfile, so the release
tree equals the tested tree by construction. No pins to maintain, no drift guard,
no second lockfile.

The isolated workspace only ever existed to dodge npm hoisting. Its stated
blocker -- @expo/cli crashing on the tsconfig extends chain ("Failed to collapse")
-- does not reproduce outside the workspace it was written for: prebuild runs
clean in the real monorepo with tsconfig.json untouched, which is what
scripts/android-release-apk.js has always done locally. So it goes, and with it
prepare-release-workspace.js, verify-release-native-modules.js (tautological once
npm ci guarantees the tree), and the .release-mobile indirection through every
step.

## Google Play, without the Expo account

eas-cli submit is not a local pass-through: at the v21.0.1 tag it uploads the AAB
to Expo's GCS bucket and posts the Play service account key to Expo's GraphQL
backend, which then makes the androidpublisher call. Every release sent the
artifact and the signing credential through a third party, as `npx eas-cli@latest`
-- unpinned, fetched fresh each run -- to reach an API the runner can call itself.
EAS Submit is free on every tier, so this is not about cost.

r0adkll/upload-google-play calls that API directly, pinned to the v1.1.5 commit
because this step handles the signing key. The closed->alpha / open->beta mapping
that lived only in eas.json becomes an explicit step that runs early, so an
unmappable track fails in seconds rather than after the build. It also uploads
mapping.txt, so R8-obfuscated Play crash reports deobfuscate. eas.json is then
entirely dead -- its build profiles were already vestigial -- as are EXPO_TOKEN and
the step that wrote the key to disk. EAS_BUILD_PROFILE stays despite the name:
app.config.js uses it as the gate that throws on unresolved production AdMob IDs.

## 31m39s of a 37m13s run was Gradle

Measured on run 29517434610, the first green release since #545. Nothing was
reused between releases, because android/ is regenerated every run.

- Stop building x86/x86_64. Nothing set reactNativeArchitectures, so all four ABIs
  were built and worklets and reanimated each ran CMake four times. Play serves
  only the matching split per device and Google's 64-bit policy does not require
  x86. ChromeOS/ARCVM loses native x86_64 and falls back to Houdini ("OK" rather
  than "Best"), accepted deliberately; one line to revert.
- ccache the NDK compiles. CMake initializes CMAKE_<LANG>_COMPILER_LAUNCHER from
  the environment, so no generated CMake needs editing. compiler_check=content is
  required on CI. Keyed on the lockfile, so a dependency change misses.
- Cache Gradle task outputs via gradle/actions/setup-gradle. setup-java's
  `cache: gradle` cached only dependencies and must not run alongside it, so it is
  removed rather than kept.

## Faults found in the same path

- with-android-release-build-fixes.js sets -Xmx6144m/2048m and says it forces them
  "authoritatively". It did not: configure-android-release-signing.js runs after
  prebuild and upserted the property back to -Xmx4g/1024m, so every release has
  minified under a heap the plugin's own comment calls insufficient. The plugin is
  now the only writer. workers.max=2 is left alone -- it is sized for this heap.
- SENTRY_AUTH_TOKEN was missing from the secrets guard while the sourcemap upload
  runs inside the Gradle task, surfacing ~15 minutes in instead of in one second.
- set-app-version.js patched android/app/build.gradle behind an existsSync guard
  that its only caller can never satisfy: the workflow runs it before prebuild
  creates android/. Redundant even if reached, since prebuild regenerates those
  values from the app.json the script already wrote.
- scripts/android-release-device.js had zero references repo-wide.

## Verified

Locally, against the real tree, with the production AdMob env:
`expo config` + the AdMob assertion pass; prebuild finishes clean in place;
the generated manifest carries the production AdMob app ID and no sample IDs;
expo-modules-autolinking resolves orbit-widget (43 modules) from the real tree,
so the widget still reaches the manifest merge.

Generated gradle.properties reads, from a real prebuild:
  org.gradle.jvmargs=-Xmx6144m -XX:MaxMetaspaceSize=2048m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError
  reactNativeArchitectures=armeabi-v7a,arm64-v8a
against armeabi-v7a,arm64-v8a,x86,x86_64 on the same command without buildArchs.

The plugin's jvmargs mod is covered by a new test that fails on a heap regression.
tsc, expo lint and the mobile suite (244 files / 1589 tests) pass.

Not provable without a release: the Gradle build, signing and the Play upload
itself. Dispatch to the internal track before an open-track release; the first run
is a guaranteed ccache miss and will not be faster.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDueAWEMzgcryVDyG7yBq2
…review)

Review flagged that `tracks:` might be the wrong input for upload-google-play.
Checked action.yml at the pinned commit e738b9dd: `tracks` is correct, and `track`
is the deprecated one -- validateTracks warns "'track' is deprecated ... please
migrate fully to 'tracks'" -- so the suggested change would have moved this onto a
deprecated input.

The review's underlying concern is real, though, and worse than reported: when
validateTracks receives no track it does not fail, it returns ['production']. A
PLAY_TRACK that never reaches the action -- a reordered or skipped resolve step, a
GITHUB_ENV that did not carry -- would therefore publish to real users while the
workflow stayed green. The value is verified as correct today, but the failure
mode is silent and the blast radius is production, so assert it is non-empty
before the upload rather than let that default stand.

Also corrects the `message` input description, which still promised the note was
"shown in Expo" after this PR removed every Expo submission step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDueAWEMzgcryVDyG7yBq2
@thomasluizon
thomasluizon force-pushed the fix/android-release-pipeline branch from d5e23ea to 023f065 Compare July 16, 2026 18:30
@sonarqubecloud

Copy link
Copy Markdown

@thomasluizon
thomasluizon merged commit 63fc278 into main Jul 16, 2026
21 checks passed
@thomasluizon
thomasluizon deleted the fix/android-release-pipeline branch July 16, 2026 18:33

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review: PR #551 — fix(mobile,ci): build releases from the lockfile, drop Expo, cache the native compiles

Recommendation: APPROVE

Summary

Rebuilds the Android release GitHub Actions pipeline around one idea — install with npm ci at the repo root so the release tree is exactly the tree the repo tests, instead of resolving dependencies fresh in an isolated, lockfile-less workspace. Also swaps eas-cli submit for a direct r0adkll/upload-google-play call (removing Expo/EXPO_TOKEN from the release path and the plaintext service-account-key file), adds ccache + Gradle build-cache caching, restricts release APK architectures to armeabi-v7a/arm64-v8a, fixes a JVM-heap-size regression where a later script silently clobbered the config plugin's -Xmx6144m, and deletes ~384 lines of now-dead workspace/verification scripts. The diff is CI/build infrastructure only — no app UI, no shared contract, no user-facing feature surface — and every changed file traces to a stated, verified reason in the PR body.

Findings

Critical / High / Medium: None.

Info: The "Assert the Google Play track resolved" guard (refusing to upload if PLAY_TRACK failed to resolve, since r0adkll/upload-google-play's validateTracks falls back to the production track on empty input) is already present in this diff — noting only so it isn't mistakenly re-flagged as missing later.

Subagents

Agent Verdict
parity-checker N/A-EXEMPT — Android-native release machinery has no apps/web counterpart (confirmed for all 12 changed files)
i18n-syncer N/A — no user-facing strings changed
contract-aligner N/A — no shared types / DTOs touched
security-reviewer N/A — orbit-api not touched
design-reviewer N/A — no apps/* UI files touched

Validation

Not independently re-executed in this review environment (no network egress for npm ci). Relying on the PR body's self-reported local verification: real expo prebuild output confirming reactNativeArchitectures=armeabi-v7a,arm64-v8a and the raised heap, tsc/expo lint clean, mobile suite (244 files / 1589 tests) green including the new plugin regression test that fails when the heap regression is reintroduced.

What's good

  • Root-cause fix, not a patch. The isolated release workspace existed only to dodge a tsconfig.json extends-chain crash the PR proves (with a real prebuild run) does not reproduce outside the workspace invented for it — the whole indirection (RELEASE_WORKDIR, prepare-release-workspace.js, verify-release-native-modules.js) is deleted, not patched around.
  • Real regression coverage for the exact heap-clobbering failure mode this PR fixes, confirmed to fail on a reintroduced regression.
  • Credential custody improved, not just moved — drops EXPO_TOKEN and the plaintext service-account-key file; the new upload action is pinned to a commit SHA with the rationale spelled out.
  • Clean dead-code removal — grepped the tree for every deleted symbol/file (EXPO_TOKEN, eas.json, eas-cli, prepare-release-workspace, verify-release-native-modules, android-release-device); no dangling references anywhere.
  • Comment discipline — every comment is a WHY note with a URL or a concrete prior CI run number, no narration.

Recommendation

Merge as-is. Per the PR's own note, dispatch to the internal track first — the first run is a guaranteed ccache miss and won't demonstrate the speedup.


🤖 Reviewed via /pr-review

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.

Release install is unpinned: the isolated workspace's stated reason does not reproduce

1 participant