Skip to content

Commit 2a3c908

Browse files
committed
Split production native build and submit into per-platform, per-phase jobs
Build (build stage) runs eas build per platform and passes the build id to the submit job via a dotenv artifact; submit (deploy stage) ships that build and records the fingerprint marker only on success. Submit is gated to develop only, so the validation branch builds without pushing to the stores. Also set changesNotSentForReview on the production Android submit profile.
1 parent f24546d commit 2a3c908

4 files changed

Lines changed: 142 additions & 45 deletions

File tree

app/.gitlab-ci.yml

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,42 +1059,94 @@ deploy:mobile-ota-staging:
10591059
# native counterpart to the OTA publish below. A signed OTA only applies to an
10601060
# installed build whose runtimeVersion (Expo fingerprint) matches, so a native dep
10611061
# / app.config.js / Expo SDK change (the ~5% case OTA can't serve) needs a fresh
1062-
# store build carrying the new fingerprint. scripts/production-build.sh builds on
1063-
# EAS and auto-submits:
1062+
# store build carrying the new fingerprint; pure JS/TS changes ship over the air.
1063+
#
1064+
# Split per platform and per phase: the `build` phase builds on EAS
1065+
# (scripts/production-build.sh, fingerprint-gated, emitting the build id to a
1066+
# dotenv artifact), and the `deploy` phase submits that build
1067+
# (scripts/production-submit.sh):
10641068
# iOS -> App Store Connect (lands in TestFlight + as an available build; it
10651069
# is NOT submitted for App Store review and no release notes change —
10661070
# do that by hand in App Store Connect).
10671071
# Android -> the Play *internal* testing track (submit profile `production`,
10681072
# track: internal); promote to public production by hand in the Play
10691073
# Console.
1070-
# Neither path makes anything public on its own. It no-ops when the freshly
1071-
# computed fingerprint matches the last-built app, recorded per platform under
1072-
# s3://$AWS_PREVIEW_BUCKET/production-builds/. Requires the EXPO_TOKEN CI variable.
1073-
build:production-native:
1074-
needs: ["protos"]
1075-
stage: build
1074+
# Neither path makes anything public on its own. The build no-ops (and the submit
1075+
# in turn) when the freshly computed fingerprint matches the last-submitted app,
1076+
# recorded per platform under s3://$AWS_PREVIEW_BUCKET/production-builds/. Requires
1077+
# the EXPO_TOKEN CI variable.
1078+
.production-native:
10761079
image: node:22
10771080
inherit:
10781081
default: false
1082+
before_script:
1083+
- apt-get update -qq && apt-get install -y --no-install-recommends awscli curl
1084+
rules:
1085+
- if: ($BUILD_MOBILE == "true") && ($CI_COMMIT_BRANCH == $RELEASE_BRANCH)
1086+
# TEMP: run on this branch to validate the pipeline; revert before merge.
1087+
- if: $CI_COMMIT_BRANCH == "mobile/v1.1.20"
1088+
1089+
build:production-native-ios:
1090+
extends: .production-native
1091+
needs: ["protos"]
1092+
stage: build
10791093
variables:
10801094
# Compute the fingerprint as the production variant so it matches the EAS build
10811095
# (app.config.js branches on APP_VARIANT) and the OTA publish job's fingerprint.
10821096
APP_VARIANT: production
1083-
before_script:
1084-
- apt-get update -qq && apt-get install -y --no-install-recommends awscli curl
10851097
script:
10861098
- cd app/mobile
10871099
- npm ci
10881100
- npm run build:protos
10891101
- npm install -g eas-cli
1090-
# Separate lines (not a loop) so an iOS failure fails the job instead of being
1091-
# masked by a later Android success.
10921102
- bash scripts/production-build.sh ios
1103+
artifacts:
1104+
reports:
1105+
dotenv: app/mobile/native-build-ios.env
1106+
1107+
build:production-native-android:
1108+
extends: .production-native
1109+
needs: ["protos"]
1110+
stage: build
1111+
variables:
1112+
APP_VARIANT: production
1113+
script:
1114+
- cd app/mobile
1115+
- npm ci
1116+
- npm run build:protos
1117+
- npm install -g eas-cli
10931118
- bash scripts/production-build.sh android
1119+
artifacts:
1120+
reports:
1121+
dotenv: app/mobile/native-build-android.env
1122+
1123+
deploy:production-native-ios:
1124+
extends: .production-native
1125+
needs: ["build:production-native-ios"]
1126+
stage: deploy
1127+
script:
1128+
- cd app/mobile
1129+
- npm ci
1130+
- npm install -g eas-cli
1131+
- bash scripts/production-submit.sh ios
1132+
# Submit only on the real release branch — disabled on the temp validation branch
1133+
# so we build (above) without pushing anything to the stores yet.
1134+
rules:
1135+
- if: ($BUILD_MOBILE == "true") && ($CI_COMMIT_BRANCH == $RELEASE_BRANCH)
1136+
1137+
deploy:production-native-android:
1138+
extends: .production-native
1139+
needs: ["build:production-native-android"]
1140+
stage: deploy
1141+
script:
1142+
- cd app/mobile
1143+
- npm ci
1144+
- npm install -g eas-cli
1145+
- bash scripts/production-submit.sh android
1146+
# Submit only on the real release branch — disabled on the temp validation branch
1147+
# so we build (above) without pushing anything to the stores yet.
10941148
rules:
10951149
- if: ($BUILD_MOBILE == "true") && ($CI_COMMIT_BRANCH == $RELEASE_BRANCH)
1096-
# TEMP: run on this branch to validate the pipeline; revert before merge.
1097-
- if: $CI_COMMIT_BRANCH == "mobile/v1.1.20"
10981150

10991151
# ─── Production native OTA publish (decoupled from the backend/web deploy) ───
11001152
#

app/mobile/eas.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@
7070
},
7171
"android": {
7272
"track": "internal",
73-
"releaseStatus": "completed"
73+
"releaseStatus": "completed",
74+
"changesNotSentForReview": true
7475
}
7576
},
7677
"devtool": {
Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,20 @@
11
#!/usr/bin/env bash
2-
# Rebuild and submit the production store app on EAS when its fingerprint changes.
2+
# Build the production store app on EAS when its fingerprint changes (build phase).
3+
# Submitting the build to the stores happens separately in production-submit.sh
4+
# (deploy phase). We emit the EAS build id and the fingerprint to a dotenv artifact
5+
# so the submit job knows what to ship and which fingerprint to record on success.
36
#
47
# A signed OTA only applies to an installed build whose runtimeVersion (the Expo
5-
# fingerprint) matches it. So when a native change moves the production
6-
# fingerprint — a new native module, an app.config.js change, or an Expo SDK bump
7-
# — we need a fresh store build carrying it before an OTA on that fingerprint can
8-
# land; pure JS/TS changes ship over the air. We record the last-built fingerprint
9-
# per platform in S3 and compare; on a change we build and auto-submit, recording
10-
# the marker only once that succeeds so a failed build is retried.
11-
#
12-
# iOS: eas build --auto-submit -> App Store Connect. The build lands in
13-
# TestFlight and as an available build; it is NOT submitted for App
14-
# Store review and no release notes are changed — do that by hand.
15-
# Android: eas build --auto-submit -> the Play *internal* testing track (the
16-
# `production` submit profile sets track: internal). Promote to the
17-
# public production track by hand in the Play Console.
18-
#
19-
# Neither platform makes anything public on its own.
8+
# fingerprint) matches it, so when a native change moves the production fingerprint
9+
# we cut a fresh store build; pure JS/TS changes ship over the air. We compare the
10+
# fingerprint against the last-submitted marker in S3 and skip the build (leaving
11+
# EAS_BUILD_ID empty) when it is unchanged.
2012
#
2113
# Usage: production-build.sh <ios|android>
22-
# Env: EXPO_TOKEN EAS auth (build + submit scope)
14+
# Env: EXPO_TOKEN EAS auth (build scope)
2315
# AWS_PREVIEW_BUCKET the couchers-dev-assets bucket (holds the markers)
2416
# plus the AWS creds the aws CLI reads from the environment
25-
# Run from app/mobile (eas.json, the project, and node_modules must be present).
17+
# Run from app/mobile. Writes native-build-<platform>.env (a dotenv artifact).
2618
set -euo pipefail
2719

2820
PLATFORM="${1:?usage: production-build.sh <ios|android>}"
@@ -35,6 +27,7 @@ case "$PLATFORM" in
3527
esac
3628

3729
MARKER="s3://${AWS_PREVIEW_BUCKET}/production-builds/${PLATFORM}.fingerprint"
30+
DOTENV="native-build-${PLATFORM}.env"
3831

3932
# Read a single JSON field from stdin with node (no jq in the node:22 image).
4033
json_field() { node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const j=JSON.parse(s);process.stdout.write(String(eval("j"+process.argv[1])??""))})' "$1"; }
@@ -43,22 +36,26 @@ json_field() { node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{
4336
# and the OTA publish job bakes into the manifest. APP_VARIANT=production (set by
4437
# the job) selects the production bundle id / scheme / signed updates config.
4538
CURRENT="$(npx expo-updates fingerprint:generate --platform "$PLATFORM" 2>/dev/null | json_field '.hash')"
46-
echo "current $PLATFORM fingerprint: $CURRENT"
39+
echo "current $PLATFORM fingerprint: $CURRENT"
40+
41+
# The submit job records the marker once it succeeds, so hand it the fingerprint.
42+
echo "EAS_FINGERPRINT=$CURRENT" > "$DOTENV"
4743

4844
PREVIOUS="$(aws s3 cp "$MARKER" - 2>/dev/null || true)"
49-
echo "last-built $PLATFORM fingerprint: ${PREVIOUS:-<none>}"
45+
echo "last-submitted $PLATFORM fingerprint: ${PREVIOUS:-<none>}"
5046

5147
if [ -n "$PREVIOUS" ] && [ "$PREVIOUS" = "$CURRENT" ]; then
52-
echo "Fingerprint unchanged for $PLATFORM — the live production build still matches, skipping."
48+
echo "Fingerprint unchanged for $PLATFORM — the live production build still matches, skipping build."
49+
echo "EAS_BUILD_ID=" >> "$DOTENV"
5350
exit 0
5451
fi
5552

56-
echo "Fingerprint changed for $PLATFORM — building and submitting a new production app."
57-
# --auto-submit uses the submit profile named like the build profile (production):
58-
# iOS -> App Store Connect, Android -> the internal track.
59-
eas build --platform "$PLATFORM" --profile production --auto-submit --non-interactive
60-
61-
# Record the new fingerprint only after a successful build+submit, so a failure is
62-
# retried on the next pipeline instead of being marked done.
63-
printf '%s' "$CURRENT" | aws s3 cp - "$MARKER"
64-
echo "Recorded $PLATFORM fingerprint $CURRENTproduction build is up to date."
53+
echo "Fingerprint changed for $PLATFORM — building a new production app on EAS."
54+
BUILD_JSON="$(eas build --platform "$PLATFORM" --profile production --non-interactive --json)"
55+
BUILD_ID="$(printf '%s' "$BUILD_JSON" | json_field '[0].id')"
56+
[ -n "$BUILD_ID" ] || {
57+
echo "could not determine the EAS build id from the build output" >&2
58+
exit 1
59+
}
60+
echo "EAS_BUILD_ID=$BUILD_ID" >> "$DOTENV"
61+
echo "Built $PLATFORM production app: $BUILD_ID"
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env bash
2+
# Submit a production store app build to the stores (deploy phase). Reads the EAS
3+
# build id and fingerprint that production-build.sh emitted into the dotenv
4+
# artifact; an empty build id means the build was skipped (fingerprint unchanged)
5+
# and there is nothing to submit. The fingerprint marker is recorded only after a
6+
# successful submit, so a failed submit is retried (rebuilt) on the next pipeline.
7+
#
8+
# iOS: eas submit -> App Store Connect. The build lands in TestFlight and as
9+
# an available build; it is NOT submitted for App Store review and no
10+
# release notes change — do that by hand in App Store Connect.
11+
# Android: eas submit -> the Play *internal* testing track (the `production`
12+
# submit profile sets track: internal). Promote to the public production
13+
# track by hand in the Play Console.
14+
#
15+
# Neither platform makes anything public on its own.
16+
#
17+
# Usage: production-submit.sh <ios|android>
18+
# Env: EXPO_TOKEN EAS auth (submit scope)
19+
# EAS_BUILD_ID from the build job's dotenv (empty => nothing to submit)
20+
# EAS_FINGERPRINT from the build job's dotenv (recorded on success)
21+
# AWS_PREVIEW_BUCKET the couchers-dev-assets bucket (holds the markers)
22+
# plus the AWS creds the aws CLI reads from the environment
23+
# Run from app/mobile (eas.json must be present).
24+
set -euo pipefail
25+
26+
PLATFORM="${1:?usage: production-submit.sh <ios|android>}"
27+
case "$PLATFORM" in
28+
ios | android) ;;
29+
*)
30+
echo "platform must be ios or android (got $PLATFORM)" >&2
31+
exit 1
32+
;;
33+
esac
34+
35+
if [ -z "${EAS_BUILD_ID:-}" ]; then
36+
echo "No new $PLATFORM build (fingerprint unchanged) — nothing to submit."
37+
exit 0
38+
fi
39+
40+
echo "Submitting $PLATFORM build $EAS_BUILD_ID."
41+
eas submit --platform "$PLATFORM" --profile production --id "$EAS_BUILD_ID" --non-interactive
42+
43+
# Record the fingerprint only after a successful submit, so a failure is retried on
44+
# the next pipeline instead of being marked done.
45+
MARKER="s3://${AWS_PREVIEW_BUCKET}/production-builds/${PLATFORM}.fingerprint"
46+
printf '%s' "$EAS_FINGERPRINT" | aws s3 cp - "$MARKER"
47+
echo "Recorded $PLATFORM fingerprint $EAS_FINGERPRINT — production build is up to date."

0 commit comments

Comments
 (0)