forked from pingdotgg/t3code
-
Notifications
You must be signed in to change notification settings - Fork 0
290 lines (271 loc) · 12.8 KB
/
Copy pathmobile-eas-production.yml
File metadata and controls
290 lines (271 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
name: Mobile EAS Production
# Production builds and OTA updates run from CI (Linux) — never from a laptop.
# Under the fingerprint runtime-version policy the fingerprint must be computed
# in the same OS/pnpm as the EAS build; a macOS `eas build` computes a different
# fingerprint (platform-specific deps + pnpm version) and errors. On this Linux
# runner, with corepack pinning pnpm 10.24 in eas.json, local == build.
#
# Every merge to main that touches the mobile app reconciles, per platform:
# 1. Store builds: if the latest production build's version differs from
# app.config.ts, cut a new build and submit it (TestFlight + Play internal
# track). Bumping `version` is therefore all it takes to
# start the next release train — the first build of a version enters
# external-TestFlight beta review immediately, and later builds of the
# same version auto-approve until that version is released. After App
# Store approval, Apple closes the release train and `version` must be
# bumped before another iOS build can be submitted. Releasing to the App
# Store stays a manual App Store Connect step.
# 2. OTA: publish a production-channel update for each platform where at
# least one finished production build matches the current native
# fingerprint. Old-version binaries with a matching fingerprint receive
# it too. When native drift means no binary could install the update,
# it is skipped and flagged in the job summary instead of published
# into the void.
# workflow_dispatch remains as a manual override for both modes (e.g. to
# retry an errored build or force an OTA).
on:
workflow_dispatch:
inputs:
mode:
description: "build (+ auto-submit to TestFlight) or update (OTA)"
required: true
type: choice
default: build
options:
- build
- update
platform:
description: "Target platform"
required: true
type: choice
default: ios
options:
- ios
- android
- all
version:
description: "Optional build version override (blank uses app.config.ts; an override is committed before building)"
required: false
type: string
message:
description: "OTA update message (mode=update only)"
required: false
type: string
push:
branches: [main]
paths:
- apps/mobile/**
- packages/client-runtime/**
- packages/contracts/**
- packages/shared/**
- assets/**
- scripts/**
- patches/**
- pnpm-lock.yaml
- pnpm-workspace.yaml
- .github/workflows/mobile-eas-production.yml
# Serialize runs so OTAs publish in merge order. GitHub keeps at most one
# queued run per group, so a burst of merges collapses into one run of the
# newest commit — intermediate commits don't need their own OTA.
concurrency:
group: mobile-eas-production
cancel-in-progress: false
jobs:
production:
name: EAS Production ${{ github.event_name == 'push' && 'auto' || inputs.mode }}
runs-on: blacksmith-8vcpu-ubuntu-2404
permissions:
contents: read
env:
APP_VARIANT: production
NODE_OPTIONS: --max-old-space-size=8192
steps:
- id: expo-token
name: Check for EXPO_TOKEN
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: |
if [ -n "$EXPO_TOKEN" ]; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
echo "EXPO_TOKEN is not available; skipping EAS production job."
fi
- id: version_app_token
name: Mint release app token for version override
if: steps.expo-token.outputs.present == 'true' && github.event_name == 'workflow_dispatch' && inputs.mode == 'build' && inputs.version != ''
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Checkout
if: steps.expo-token.outputs.present == 'true'
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ steps.version_app_token.outputs.token || github.token }}
# No sparse-checkout here: it makes actions/checkout fetch with
# --filter=blob:none, and eas-cli archives the project via
# `git clone --depth 1 file://<workspace>`, which fails (exit 128)
# when the partial clone can't serve the unfetched blobs.
- name: Setup Vite+
if: steps.expo-token.outputs.present == 'true'
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: |
args:
- --filter=@t3tools/mobile...
- name: Expose pnpm
if: steps.expo-token.outputs.present == 'true'
run: |
pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")"
vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin"
echo "$vp_pnpm_bin" >> "$GITHUB_PATH"
"$vp_pnpm_bin/pnpm" --version
- name: Setup EAS
if: steps.expo-token.outputs.present == 'true'
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
# npm, not pnpm: this only installs eas-cli into the action's own
# tool dir, and pnpm 11 hard-fails that install on dtrace-provider's
# ignored build script (no allowBuilds config outside the repo).
packager: npm
- name: Pull production environment variables
if: steps.expo-token.outputs.present == 'true'
working-directory: apps/mobile
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: eas env:pull production --non-interactive
- name: Apply manual version override
if: steps.version_app_token.outcome == 'success'
env:
GH_TOKEN: ${{ steps.version_app_token.outputs.token }}
APP_SLUG: ${{ steps.version_app_token.outputs.app-slug }}
RELEASE_VERSION: ${{ inputs.version }}
run: |
if [ "$GITHUB_REF_TYPE" != "branch" ]; then
echo "Version overrides require dispatching this workflow from a branch; received $GITHUB_REF_TYPE '$GITHUB_REF_NAME'." >&2
exit 1
fi
if ! [[ "$RELEASE_VERSION" =~ ^[0-9]+(\.[0-9]+){1,2}$ ]]; then
echo "Version override must contain two or three dot-separated integers; received '$RELEASE_VERSION'." >&2
exit 1
fi
node --input-type=module -e '
import fs from "node:fs";
const path = "apps/mobile/app.config.ts";
const source = fs.readFileSync(path, "utf8");
const next = source.replace(
/^( version: ")[^"]+(".*)$/m,
`$1${process.env.RELEASE_VERSION}$2`,
);
if (next === source && !source.includes(` version: "${process.env.RELEASE_VERSION}"`)) {
throw new Error("Could not update app version");
}
fs.writeFileSync(path, next);
'
vp fmt apps/mobile/app.config.ts
if git diff --quiet -- apps/mobile/app.config.ts; then
echo "app.config.ts is already at $RELEASE_VERSION; no version commit needed."
exit 0
fi
user_id="$(gh api "/users/${APP_SLUG}[bot]" --jq .id)"
git config user.name "${APP_SLUG}[bot]"
git config user.email "${user_id}+${APP_SLUG}[bot]@users.noreply.github.com"
git add apps/mobile/app.config.ts
git commit \
-m "chore(mobile): bump app version to $RELEASE_VERSION" \
-m "Co-authored-by: codex <codex@users.noreply.github.com>"
git push origin "HEAD:refs/heads/${GITHUB_REF_NAME}"
- name: Summarize manual build version
if: steps.expo-token.outputs.present == 'true' && github.event_name == 'workflow_dispatch' && inputs.mode == 'build'
working-directory: apps/mobile
run: |
version="$(npx expo config --json --type public | jq -r '.version')"
{
echo "## Manual production build"
echo
echo "- App version: \`$version\`"
echo "- Platform: \`${{ inputs.platform }}\`"
echo
echo "> Apple closes an iOS release train after App Store approval. Before building iOS, confirm \`$version\` is newer than the approved App Store version."
} >> "$GITHUB_STEP_SUMMARY"
- name: Build and submit (manual)
if: steps.expo-token.outputs.present == 'true' && github.event_name == 'workflow_dispatch' && inputs.mode == 'build'
working-directory: apps/mobile
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: eas build --platform ${{ inputs.platform }} --profile production --auto-submit --non-interactive --no-wait
- name: Publish OTA update (manual)
if: steps.expo-token.outputs.present == 'true' && github.event_name == 'workflow_dispatch' && inputs.mode == 'update'
working-directory: apps/mobile
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: |
eas update \
--channel production \
--environment production \
--platform ${{ inputs.platform }} \
--message "${{ inputs.message || format('Production OTA ({0})', github.sha) }}" \
--non-interactive
# No --status filter on build:list: an in-queue/in-progress build must
# count as existing, or every merge during the build window would cut a
# duplicate. After an errored build, retry via workflow_dispatch
# mode=build — pushes won't re-trigger it until the app version changes.
- id: store_builds
name: Ensure store builds exist for the current app version
if: steps.expo-token.outputs.present == 'true' && github.event_name == 'push'
continue-on-error: true
working-directory: apps/mobile
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: |
failed=0
version="$(npx expo config --json --type public | jq -r '.version')"
for platform in ios android; do
latest="$(eas build:list --platform "$platform" --build-profile production --limit 1 --json --non-interactive | jq -r '.[0].appVersion // "none"')"
if [ "$latest" = "$version" ]; then
echo "$platform: production build for $version already exists (or is in progress)"
continue
fi
echo "$platform: latest production build is $latest, app.config.ts says $version — building"
if eas build --platform "$platform" --profile production --auto-submit --non-interactive --no-wait; then
echo ":building_construction: $platform: scheduled production build and submission for $version" >> "$GITHUB_STEP_SUMMARY"
else
failed=1
echo ":x: $platform: production build or submission failed for $version" >> "$GITHUB_STEP_SUMMARY"
fi
done
exit "$failed"
- name: Publish fingerprint-gated OTA
if: steps.expo-token.outputs.present == 'true' && github.event_name == 'push'
working-directory: apps/mobile
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: |
message="$(git log -1 --pretty=%s | head -c 120) ($(git rev-parse --short=9 HEAD))"
for platform in ios android; do
# eas-cli prints an environment-loaded notice to stdout before the
# JSON even with --json, so discard everything before the document.
hash="$(eas fingerprint:generate --platform "$platform" --environment production --json --non-interactive | sed -n '/^{/,$p' | jq -er '.hash | select(type == "string" and length > 0)')"
matching="$(eas build:list --platform "$platform" --build-profile production --status finished --fingerprint-hash "$hash" --limit 1 --json --non-interactive | jq 'length')"
if [ "$matching" -gt 0 ]; then
eas update \
--channel production \
--environment production \
--platform "$platform" \
--message "$message" \
--non-interactive
echo ":white_check_mark: $platform: OTA published to production (fingerprint \`$hash\`)" >> "$GITHUB_STEP_SUMMARY"
else
echo ":warning: $platform: no finished production build matches fingerprint \`$hash\` — OTA skipped; JS changes reach $platform only once a matching build ships" >> "$GITHUB_STEP_SUMMARY"
fi
done
- name: Propagate store build failure
if: steps.store_builds.outcome == 'failure'
run: exit 1