Module: ci-mobile-release
The EAS release DAG. Tag-triggered (v*): validate floor → EAS production build
(both stores) → artifact download from the build JSON → size budgets from
tools/perf-baseline.json → SHA-256 checksums → attach to the GitHub release →
gated eas submit. Plus release-please automation that maintains the release PR
and bumps every version surface in lockstep (the version-sync gate requires it),
and a PR preview lane: a credential-free native-fingerprint diff on every PR, and
a label-gated EAS preview build.
What it adds
Section titled “What it adds”| File | Purpose |
|---|---|
.github/workflows/release-mobile.yml |
the tag-triggered gates → build → verify/attach → submit pipeline |
.github/workflows/release-please.yml |
conventional-commit release automation |
.github/workflows/preview-mobile.yml |
PR fingerprint diff (every PR) + EAS preview build (preview-build label) |
release-please-config.json / release-please-manifest.json |
version-bump config: root + apps/mobile + apps/web package.json move together |
The version story (why extra-files is so short)
Section titled “The version story (why extra-files is so short)”The source harness bumped four files per release; this one bumps three
package.jsons and no native config file, because there is none to bump:
app.config.ts derives every other version surface from
apps/mobile/package.json — version, ios.buildNumber, android.versionCode
(maj·1e6 + min·1e3 + pat), and the runtimeVersion OTA boundary
(policy: 'appVersion'). The version-sync gate recomputes those derivations
through the resolved Expo config on every run, so a literal smuggled into a
native surface goes red at the next bump, not at store review. What release-please
must keep in lockstep is exactly what the gate asserts as equal: the root,
apps/mobile, and apps/web package.json versions — the root is bumped
natively by the node release type, the other two by extra-files.
Prerequisites
Section titled “Prerequisites”- An EAS-initialized project: the
EAS_PROJECT_ID,ASC_APP_ID, andAPPLE_TEAM_IDplaceholders answered with real values (doctorwarns while any isTBD). - A GitHub environment named
release(recommended: protect it with required reviewers) carrying theEXPO_TOKENsecret — a robot-account access token from expo.dev. The build and submit jobs run in that environment; the PR preview lane runs outside any environment, so previews additionally need a repository-levelEXPO_TOKEN(fork PRs never see either — they degrade green). - Store credentials in the EAS credentials service, not in GitHub: the Google
service-account key (
eas credentials --platform android) and the ASC API key (--platform ios). Submission executes on EAS servers; no store key material ever touches a runner. - The
AUTO_SUBMITrepository variable set totrue— or theauto-submitinput on a manual dispatch — before the submit job does anything but writeNOT-SUBMITTED.txt. - Conventional commits on
{{DEFAULT_BRANCH}}(commitlint already enforces this). - A
preview-buildlabel in the repo for the label-gated preview lane.
Honest degrade: with no EXPO_TOKEN the tag pipeline still runs end to end —
it builds an unsigned Android bundle on the runner, writes
UNSIGNED-BUILD.txt beside it, loud-skips iOS (an .ipa cannot exist without
EAS/Apple credentials — the absence is stated, never faked), verifies budgets and
checksums, attaches everything to the release, and attaches NOT-SUBMITTED.txt
in place of a submission. A skipped submit with a token present (no
AUTO_SUBMIT) also attaches NOT-SUBMITTED.txt. Nothing on this path is
silently green.
How enabling works
Section titled “How enabling works”npx next-expo-supabase-agent-harness@latest enable ci-mobile-releasecopies the files above and records them in .harness/manifest.json. The release
pipeline is live on the next v* tag — no gate-config change needed (the
workflow IS the gate). First release: merge the release-please PR it opens after
your first conventional commit, or push a v0.1.0 tag manually.
One GitHub quirk to know: a tag created by release-please with the default
GITHUB_TOKEN does not trigger release-mobile.yml (workflow-to-workflow
recursion guard). Start it via Run workflow on the tag ref, or push tags
manually / with a PAT if you want full automation.
SOURCE: https://docs.github.com/en/actions/concepts/security/github_token
The PR preview lane
Section titled “The PR preview lane”- fingerprint runs on every PR (and every
{{DEFAULT_BRANCH}}push, which updates its database): theexpo/expo-github-actionfingerprint sub-action diffs the project's native fingerprint and comments the PR. An empty diff means the change is OTA-shaped; a non-empty diff means the native app changes and a store build is implied. Credential-free by design — noEXPO_TOKENnear a PR. - preview-build runs only when the PR carries the
preview-buildlabel: a fire-and-forgeteas build --profile preview --no-wait(CI is not billed while EAS builds; install links appear on the EAS dashboard). Without a token the job stays green with a warning naming exactly what is missing.
How this gate can FAIL (anti-vacuity)
Section titled “How this gate can FAIL (anti-vacuity)”A release gate you have never seen fail is a decoration. Each check has a cheap injection:
- degrade honesty: remove
EXPO_TOKENfrom the environment → the run must produceUNSIGNED-BUILD.txt+NOT-SUBMITTED.txtand a warning, never a quiet green that looks like a real release. - artifact parse: point the pipeline at a profile with no artifacts (or
truncate the build JSON by hand in a fork) → the parse step errors on the
missing
applicationArchiveUrl, it does not shrug. - size budget: set
installerBudgetBytesintools/perf-baseline.jsonto1→ verify-and-attach fails with the measured size (restore it after — the file is write-guard-protected for exactly this reason). - checksums/attach: delete the downloaded artifacts before the checksum step
(or break the artifact name) →
if-no-files-found: errorand the empty-glob guards trip. - submit gating: with a token and
AUTO_SUBMITunset, the release must gainNOT-SUBMITTED.txt— if it doesn't, the gate is lying about what shipped. - fingerprint: touch
apps/mobile/app.config.ts(add a harmless permission) in a PR → the fingerprint diff must be non-empty; revert and it must be[].
- Two facts in this pipeline carry recorded MEDIUM confidence in the design
record (RELEASE-FACTS): whether artifact downloads require the
Authorization: Bearerheader (it is sent regardless — harmless when not required), and the exacteas submitstored-credentials interplay. Dry-run the pipeline once on a throwaway tag before trusting a real release to it. eas.jsonpinsappVersionSource: "local"andautoIncrement: false— the repo is the version source of truth, which is what makes the release PR the complete release record. Remote/auto-increment variants were considered and rejected (design record: PORT-SPEC).- The identity in
tools/identity.lock.jsonpins{{APP_IDENTIFIER}}as both the iOS bundle id and the Android package; changing it after the first release is a new app in both stores. Theexpo-policygate guards this. - Pair with the
ci-provenancemodule for build attestation + SBOM of what this pipeline ships, and theeas-updatemodule for the OTA half of the release story.