-
-
Notifications
You must be signed in to change notification settings - Fork 268
236 lines (219 loc) · 10.1 KB
/
Copy pathtest-app-build-cache.yml
File metadata and controls
236 lines (219 loc) · 10.1 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
name: Test App Build Cache
# Builds a Release binary of examples/test-app and publishes it as a workflow
# artifact keyed by the Expo fingerprint, so CI e2e jobs install a ready binary
# (via .github/actions/setup-fixture-app) instead of compiling from scratch.
#
# Release, not dev-client: the JS bundle is embedded, so a consuming job needs no
# Metro. Its JS is refreshed on retrieval with @expo/repack-app, so keying on the
# native-only fingerprint is correct -- a JS-only change reuses the same native
# binary. Local development does not use this; it caches on disk via the
# expo-build-disk-cache provider in app.config.js.
#
# Only builds when the fingerprint has no trusted artifact yet. Runs on every
# push to main and every PR so a same-head consumer can wait for a scheduled
# producer (workflow_dispatch cannot reach a workflow that is not yet on main).
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
permissions:
contents: read
# Look up whether this fingerprint's artifact already exists across runs.
actions: read
jobs:
fingerprint:
name: Resolve native fingerprint
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
matrix: ${{ steps.fingerprint.outputs.matrix }}
has-work: ${{ steps.fingerprint.outputs.has-work }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup toolchain
uses: ./.github/actions/setup-node-pnpm
with:
cache-dependency-path: examples/test-app/pnpm-lock.yaml
- name: Setup test app dependencies
uses: ./.github/actions/setup-test-app-dependencies
- name: Typecheck test app
uses: ./.github/actions/run-gate
with: { gate: test-app-typecheck }
- name: Resolve native fingerprint
id: fingerprint
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
ARTIFACT_NAME_RESOLVER=".github/actions/setup-fixture-app/resolve-artifact-name.sh"
IOS_NAME="$(sh "$ARTIFACT_NAME_RESOLVER" ios)"
ANDROID_NAME="$(sh "$ARTIFACT_NAME_RESOLVER" android)"
EXPECTED_HEAD_SHA="${{ github.event.pull_request.head.sha || github.sha }}"
TRUSTED_ARTIFACT=".github/actions/setup-fixture-app/trusted-artifact.mjs"
IOS_ARTIFACT="$(node "$TRUSTED_ARTIFACT" find "${{ github.repository }}" "$IOS_NAME" "$EXPECTED_HEAD_SHA")"
ANDROID_ARTIFACT="$(node "$TRUSTED_ARTIFACT" find "${{ github.repository }}" "$ANDROID_NAME" "$EXPECTED_HEAD_SHA")"
IOS_BUILD=true
if [ -n "$IOS_ARTIFACT" ] ||
{ [ "${{ github.event_name }}" = "pull_request" ] &&
[ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; }; then
IOS_BUILD=false
fi
ANDROID_BUILD=true
if [ -n "$ANDROID_ARTIFACT" ]; then
ANDROID_BUILD=false
fi
MATRIX="$(jq -cn \
--arg iosName "$IOS_NAME" \
--argjson iosBuild "$IOS_BUILD" \
--arg androidName "$ANDROID_NAME" \
--argjson androidBuild "$ANDROID_BUILD" \
'[
{
name: "iOS Release",
platform: "ios",
runsOn: (if $iosBuild then "macos-26" else "ubuntu-latest" end),
artifactName: $iosName,
build: $iosBuild
},
{
name: "Android Release",
platform: "android",
runsOn: "ubuntu-latest",
artifactName: $androidName,
build: $androidBuild
}
] | { include: map(select(.build)) }')"
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
echo "has-work=$(jq -r '.include | length > 0' <<<"$MATRIX")" >> "$GITHUB_OUTPUT"
release:
name: ${{ matrix.name }}
needs: fingerprint
if: needs.fingerprint.outputs.has-work == 'true'
runs-on: ${{ matrix.runsOn }}
timeout-minutes: 60
concurrency:
group: test-app-${{ matrix.artifactName }}-${{ github.event.pull_request.number || github.ref_name }}
cancel-in-progress: false
strategy:
# Independent caches; one platform failing must not withhold the other's.
fail-fast: false
matrix: ${{ fromJSON(needs.fingerprint.outputs.matrix) }}
steps:
- name: Checkout
if: matrix.build
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup toolchain
if: matrix.build
uses: ./.github/actions/setup-node-pnpm
with:
cache-dependency-path: |
pnpm-lock.yaml
examples/test-app/pnpm-lock.yaml
- name: Setup test app dependencies
if: matrix.build
uses: ./.github/actions/setup-test-app-dependencies
# The generated Android project is safe to assemble without an emulator.
# Keep Gradle's heavy dependency/transforms cache, but never let an
# untrusted fork PR write it.
- name: Setup Gradle cache
if: matrix.platform == 'android' && matrix.build
uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3
with:
cache-read-only: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
# `--device generic` builds for the simulator without booting one or
# installing. Release embeds the bundle (no Metro) and links a universal
# x86_64+arm64 slice, so the artifact runs on any developer's simulator.
# Fork PRs cannot safely publish this binary; their iOS workflow builds the
# same Release app inline on a cache miss, so compiling it here would double
# the macOS cost without adding signal.
- name: Build the iOS Release app
if: |
matrix.platform == 'ios' &&
matrix.build &&
(github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository)
run: |
set -euo pipefail
pnpm --dir examples/test-app exec expo run:ios \
--configuration Release \
--device generic \
--no-bundler \
--output "${{ github.workspace }}/.tmp/test-app-build"
# Deliberately no `set -o pipefail`: `yes` is killed by SIGPIPE once
# sdkmanager stops reading, and pipefail would report that 141 as the
# step's own failure.
- name: Install Android SDK packages
if: matrix.platform == 'android' && matrix.build
run: |
SDK_ROOT="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}}"
SDKMANAGER="$SDK_ROOT/cmdline-tools/latest/bin/sdkmanager"
if [ ! -x "$SDKMANAGER" ]; then
SDKMANAGER="$SDK_ROOT/cmdline-tools/bin/sdkmanager"
fi
if [ ! -x "$SDKMANAGER" ]; then
echo "sdkmanager not found under $SDK_ROOT" >&2
exit 1
fi
yes | "$SDKMANAGER" --licenses >/dev/null
"$SDKMANAGER" "platforms;android-36" "build-tools;36.0.0"
# CNG's generated Android project exposes the ordinary Gradle Release task;
# it builds and packages every ABI without device resolution. Installation
# and launch are deliberately proven by the Android consumer instead.
- name: Build the Android Release apk
if: matrix.platform == 'android' && matrix.build
run: |
set -euo pipefail
pnpm --dir examples/test-app exec expo prebuild --platform android --no-install
(
cd examples/test-app/android
./gradlew :app:assembleRelease
)
mkdir -p "${{ github.workspace }}/.tmp/test-app-build"
cp examples/test-app/android/app/build/outputs/apk/release/app-release.apk \
"${{ github.workspace }}/.tmp/test-app-build/app-release.apk"
- name: Stage the binary for upload
id: stage
if: |
matrix.build &&
(matrix.platform == 'android' ||
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository)
run: |
set -euo pipefail
SRC="${{ github.workspace }}/.tmp/test-app-build"
BINARY="$(find "$SRC" -maxdepth 1 \( -name '*.app' -o -name '*.apk' \) | head -1)"
if [ -z "$BINARY" ]; then
echo "::error::Build produced no .app or .apk under $SRC."
exit 1
fi
# An APK must span both the maintainer's arm64 and CI's x86_64; a
# regression here (e.g. an unexpected abiFilter) is otherwise silent.
if [ "${{ matrix.platform }}" = "android" ] && ! unzip -l "$BINARY" | grep -q "lib/arm64-v8a/"; then
echo "::error::Release APK has no arm64-v8a libraries."
exit 1
fi
# Tar the binary: artifact zips drop the executable bit, which would
# leave an installed .app unable to launch.
mkdir -p .tmp/test-app-artifact
tar -czf .tmp/test-app-artifact/binary.tar.gz -C "$SRC" "$(basename "$BINARY")"
echo "publishing ${{ matrix.artifactName }} ($(du -h .tmp/test-app-artifact/binary.tar.gz | cut -f1))"
# Fork PRs run with a read-only token but can still upload artifacts, and a
# consumer serves whatever it finds by name -- so a fork could publish a
# tampered binary that CI then executes. Only branches in this repo publish.
# Android still builds on forks; iOS gets equivalent build/install signal
# from the smoke workflow's inline cache-miss fallback.
- name: Publish to the build cache
if: |
matrix.build &&
(github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository)
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ matrix.artifactName }}
include-hidden-files: true
path: .tmp/test-app-artifact/binary.tar.gz
if-no-files-found: error
compression-level: 0 # already gzipped