expo-release 2.3.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -124
- package/action.yml +63 -12
- package/dist/args.d.ts +17 -15
- package/dist/cli.js +865 -143
- package/dist/decide.d.ts +26 -0
- package/dist/doctor.d.ts +30 -0
- package/dist/eas-json.d.ts +25 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +409 -122
- package/dist/operate.d.ts +8 -1
- package/dist/run.d.ts +42 -2
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +74 -14
- package/dist/runtime/ota.d.ts +23 -3
- package/dist/runtime/push.d.ts +15 -3
- package/dist/source-maps.d.ts +1 -0
- package/package.json +7 -3
- package/src/args.ts +35 -20
- package/src/cli.ts +233 -36
- package/src/decide.ts +236 -22
- package/src/doctor.ts +296 -0
- package/src/eas-json.ts +128 -0
- package/src/index.ts +2 -0
- package/src/operate.ts +97 -53
- package/src/run.ts +259 -37
- package/src/runtime/index.ts +2 -0
- package/src/runtime/ota.ts +92 -15
- package/src/runtime/push.ts +46 -13
- package/src/runtime/react-native.d.ts +10 -0
- package/src/source-maps.ts +4 -1
package/README.md
CHANGED
|
@@ -13,8 +13,9 @@ decide — git diff + eas fingerprint:generate + eas build:list
|
|
|
13
13
|
├─ no matching impact paths → skip
|
|
14
14
|
├─ finished native build with this fingerprint → eas update
|
|
15
15
|
├─ matching native build in progress → OTA only
|
|
16
|
-
├─ newest matching build errored → fail
|
|
16
|
+
├─ newest matching build errored → fail with EAS's error and the build link
|
|
17
17
|
└─ no matching native build → eas build --auto-submit --no-wait
|
|
18
|
+
(and name the fingerprint sources that changed)
|
|
18
19
|
```
|
|
19
20
|
|
|
20
21
|
## Install and requirements
|
|
@@ -24,7 +25,11 @@ bun add -D expo-release
|
|
|
24
25
|
bunx expo-release --help
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
The CLI and GitHub Action require Bun. EAS operations also need an authenticated Expo account and the EAS CLI in the calling project; Expo SDK 55+ OTA updates require an
|
|
28
|
+
The CLI and GitHub Action require Bun. EAS operations also need an authenticated Expo account and the EAS CLI in the calling project; Expo SDK 55+ OTA updates require an EAS environment (from `--environment` or the eas.json build profile). CI verifies Bun 1.4.2. Installing the npm package does not deploy an application.
|
|
29
|
+
|
|
30
|
+
In GitHub Actions, use the action (`uses: HansKristoffer/expo-release@v2`). It installs its own dependencies (`hanzio`, `zod`) in `${{ github.action_path }}` and runs `bun ${{ github.action_path }}/src/cli.ts`. The caller must already have checked out the repo (`fetch-depth: 0`), installed Bun and installed the workspace dependencies. Elsewhere, run the `expo-release` bin (see [CLI](#cli)).
|
|
31
|
+
|
|
32
|
+
`eas` and `expo` resolve to the app's own `node_modules/.bin` (searched from the working directory up to the repository root), then to `eas` on `PATH` and `bunx expo`. So the eas-cli version in the app's lockfile is the one that runs; pin it there, and `expo/expo-github-action` is no longer needed. `EAS_BIN` and `EXPO_BIN` override both. `EXPO_TOKEN` must reach the job.
|
|
28
33
|
|
|
29
34
|
## Set up with AI
|
|
30
35
|
|
|
@@ -42,16 +47,16 @@ Do the following in this repo:
|
|
|
42
47
|
2. Ensure the Expo app is ready for fingerprint-based updates:
|
|
43
48
|
- runtimeVersion: { policy: 'fingerprint' } in app config
|
|
44
49
|
- expo-updates installed and configured
|
|
45
|
-
- channel set on each EAS build profile we deploy
|
|
46
|
-
- submit config in eas.json if we use auto-submit (iOS ascAppId, Android serviceAccountKeyPath)
|
|
50
|
+
- channel and environment set on each EAS build profile we deploy, and the variant env vars in the profile's env
|
|
51
|
+
- submit config in eas.json if we use auto-submit (iOS ascAppId, Android serviceAccountKeyPath from an EAS file secret, never a committed file)
|
|
47
52
|
|
|
48
53
|
3. Add .github/workflows/expo-release.yml using the HansKristoffer/expo-release@v2 composite action. Adapt consumer-specific values:
|
|
49
54
|
- paths filter: Expo app dir, shared packages that affect the native bundle, and lockfile if native deps should trigger a release
|
|
50
55
|
- working-directory and impact-paths pointing at the Expo app
|
|
51
|
-
- profile, environment
|
|
52
|
-
- env vars required by app.config.ts on the action step (APP_VARIANT, etc.)
|
|
56
|
+
- profile for each deploy target (channel, environment and env come from eas.json)
|
|
53
57
|
- workflow_dispatch inputs for force-native, retry-ota, republish, rollback-embedded
|
|
54
|
-
- fetch-depth: 0 on checkout,
|
|
58
|
+
- fetch-depth: 0 on checkout, the setup-workspace action, eas-cli pinned in the app devDependencies, and the EXPO_TOKEN secret
|
|
59
|
+
- run `bunx expo-release doctor --profile <profile>` from the Expo app and fix what it reports
|
|
55
60
|
|
|
56
61
|
4. Document that EXPO_TOKEN must be added as a GitHub secret, and that the first run on a new fingerprint always queues a native build.
|
|
57
62
|
|
|
@@ -62,37 +67,18 @@ Do the following in this repo:
|
|
|
62
67
|
Reference the upstream README for full input/output docs and recovery operations. Ask me only if a required value (Expo app path, deploy branch, profile names) cannot be inferred from the repo.
|
|
63
68
|
```
|
|
64
69
|
|
|
65
|
-
## Install
|
|
66
|
-
|
|
67
|
-
### GitHub Action
|
|
68
|
-
|
|
69
|
-
```yaml
|
|
70
|
-
uses: HansKristoffer/expo-release@v2
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
The action installs its own dependencies (`hanzio`, `zod`) in `${{ github.action_path }}`, then runs `bun ${{ github.action_path }}/src/cli.ts`. The caller must already have checked out the repo (`fetch-depth: 0`), installed Bun, set up EAS, and installed workspace dependencies.
|
|
74
|
-
|
|
75
|
-
### npm (CLI)
|
|
76
|
-
|
|
77
|
-
For local use or custom CI:
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
bun add -D expo-release
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Then run `expo-release` (see [CLI](#cli)).
|
|
84
|
-
|
|
85
|
-
Pin `eas-cli` with `expo/expo-github-action`. The CLI invokes `eas` from `PATH` (override with `EAS_BIN` for local tests), so that pin is the version that actually runs.
|
|
86
|
-
|
|
87
70
|
## Setup checklist
|
|
88
71
|
|
|
72
|
+
`expo-release doctor --profile <profile>`, run from the Expo app, checks most of this list.
|
|
73
|
+
|
|
89
74
|
- [ ] GitHub secret `EXPO_TOKEN` from https://expo.dev/settings/access-tokens
|
|
90
75
|
- [ ] `runtimeVersion: { policy: 'fingerprint' }` and `expo-updates` in the Expo app
|
|
91
|
-
- [ ] `eas.json` channel on each profile you deploy (production, staging, …)
|
|
76
|
+
- [ ] `eas.json` `channel` and `environment` on each build profile you deploy (production, staging, …), and the variables `app.config.ts` reads in its `env`
|
|
92
77
|
- [ ] `eas.json` submit config when you use `--auto-submit`:
|
|
93
78
|
- iOS: `submit.<profile>.ios.ascAppId`
|
|
94
|
-
- Android: `submit.<profile>.android.serviceAccountKeyPath
|
|
95
|
-
- [ ] EAS credentials (`eas credentials` from the Expo app directory)
|
|
79
|
+
- Android: `submit.<profile>.android.serviceAccountKeyPath`. Keep the key in an EAS file secret, not in git.
|
|
80
|
+
- [ ] EAS credentials (`eas credentials` from the Expo app directory). A new bundle identifier needs one interactive `eas credentials:configure-build --platform ios --profile <profile>` before CI can build it.
|
|
81
|
+
- [ ] No keystores, `.p8`/`.p12` files, provisioning profiles, service-account JSON, `.expo/` or logs in git. With continuous native generation, `android/` and `ios/` are gitignored too.
|
|
96
82
|
- [ ] At least one finished native build **per profile and platform** on the current fingerprint. The first run against a new app always queues a native build.
|
|
97
83
|
- [ ] `--environment` on every OTA for Expo SDK 55+
|
|
98
84
|
- [ ] Add lockfiles (`bun.lock`, `package-lock.json`) to `impact-paths` if a dependency bump should trigger a fingerprint check. The default `apps/expo/` path does not include the workspace lockfile.
|
|
@@ -153,84 +139,56 @@ jobs:
|
|
|
153
139
|
runs-on: ubuntu-latest
|
|
154
140
|
timeout-minutes: 45
|
|
155
141
|
env:
|
|
156
|
-
|
|
157
|
-
EXPO_PUBLIC_API_URL: https://example.com
|
|
158
|
-
EXPO_PUBLIC_PLATFORM_URL: https://example.com
|
|
142
|
+
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
|
|
159
143
|
steps:
|
|
160
144
|
- name: Checkout
|
|
161
|
-
uses: actions/checkout@
|
|
145
|
+
uses: actions/checkout@v5
|
|
162
146
|
with:
|
|
163
147
|
fetch-depth: 0
|
|
148
|
+
clean: false # keeps node_modules on a self-hosted runner
|
|
164
149
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
- name: Setup Node
|
|
171
|
-
uses: actions/setup-node@v7
|
|
150
|
+
# Bun from package.json packageManager, Node to match the eas.json
|
|
151
|
+
# profile's `node`, the dependency cache and `bun install`. eas-cli comes
|
|
152
|
+
# from the app's devDependencies.
|
|
153
|
+
- name: Setup workspace
|
|
154
|
+
uses: HansKristoffer/expo-release/.github/actions/setup-workspace@v2
|
|
172
155
|
with:
|
|
173
156
|
node-version: 22.12.0
|
|
174
157
|
|
|
175
|
-
- name: Restore Bun workspace cache
|
|
176
|
-
uses: actions/cache@v6
|
|
177
|
-
with:
|
|
178
|
-
path: |
|
|
179
|
-
node_modules
|
|
180
|
-
packages/*/node_modules
|
|
181
|
-
apps/*/node_modules
|
|
182
|
-
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
|
183
|
-
restore-keys: |
|
|
184
|
-
${{ runner.os }}-bun-
|
|
185
|
-
|
|
186
|
-
- name: Setup Expo and EAS
|
|
187
|
-
uses: expo/expo-github-action@v8
|
|
188
|
-
with:
|
|
189
|
-
eas-version: 16.32.0
|
|
190
|
-
token: ${{ secrets.EXPO_TOKEN }}
|
|
191
|
-
|
|
192
|
-
- name: Install dependencies
|
|
193
|
-
run: bun install --frozen-lockfile
|
|
194
|
-
|
|
195
158
|
- name: Expo release
|
|
196
159
|
uses: HansKristoffer/expo-release@v2
|
|
197
160
|
with:
|
|
198
161
|
command: release
|
|
199
|
-
profile: production
|
|
162
|
+
profile: production # channel, environment and env come from eas.json
|
|
200
163
|
operation: ${{ github.event.inputs.operation || 'deploy' }}
|
|
201
|
-
environment: production
|
|
202
164
|
impact-paths: apps/expo/
|
|
203
165
|
working-directory: apps/expo
|
|
204
166
|
group: ${{ github.event.inputs.group }}
|
|
205
167
|
runtime-version: ${{ github.event.inputs.runtime-version }}
|
|
206
|
-
env:
|
|
207
|
-
# Set variant keys on the action step (or the job). They must reach
|
|
208
|
-
# fingerprint:generate, build:list, eas build, and eas update.
|
|
209
|
-
APP_VARIANT: production
|
|
210
|
-
SECRETS_ENV: production
|
|
211
|
-
APS_ENVIRONMENT: production
|
|
212
168
|
```
|
|
213
169
|
|
|
170
|
+
The release reads `build.<profile>` from `eas.json` in `working-directory` (following `extends`, like eas-cli): its `channel` is the default for `channel`; on Expo SDK 55+ its `environment` is the default for `environment` (before 55, `--environment` also turns off `.env` loading in `eas update`, so it is never added unasked); and each variable in its `env` that the job has not set is set for every EAS command and hook, as EAS sets them on the build machine. Inputs and variables you set win.
|
|
171
|
+
|
|
214
172
|
A manual `deploy` from `workflow_dispatch` ignores the impact diff. That is intentional: dispatch after an unrelated commit should still release, not silently skip.
|
|
215
173
|
|
|
216
174
|
### Action inputs
|
|
217
175
|
|
|
218
176
|
| Input | Default | Notes |
|
|
219
177
|
|-------|---------|--------|
|
|
220
|
-
| `command` | `release` | `release` (decide + operate), `decide` (PR checks), or `
|
|
221
|
-
| `profile` | `production` | EAS build profile
|
|
178
|
+
| `command` | `release` | `release` (decide + operate), `decide` (PR checks), `operate`, or `doctor` |
|
|
179
|
+
| `profile` | `production` | EAS build profile. Its eas.json `channel`, `environment` and `env` are the defaults |
|
|
222
180
|
| `operation` | `deploy` | See recovery operations below |
|
|
223
181
|
| `deploy-type` | `none` | Used only when `command` is `operate` |
|
|
224
|
-
| `base` / `head` | event before / `GITHUB_SHA` | Git range for the impact diff |
|
|
182
|
+
| `base` / `head` | merge base on a PR, else event before / `GITHUB_SHA` | Git range for the impact diff |
|
|
225
183
|
| `impact-paths` | `apps/expo/` | Comma-separated repo-root prefixes |
|
|
226
|
-
| `environment` |
|
|
227
|
-
| `channel` |
|
|
184
|
+
| `environment` | eas.json profile `environment` on SDK 55+ | Passed to `eas update`. Required for Expo SDK 55+ |
|
|
185
|
+
| `channel` | eas.json profile `channel`, else `profile` | EAS Update channel |
|
|
228
186
|
| `platform` | `ios` | `ios` or `android`. Passed through to `eas update` and `eas build` |
|
|
229
187
|
| `auto-submit` | `true` | Native builds use `--auto-submit` |
|
|
230
188
|
| `working-directory` | `apps/expo` | Where `eas` runs |
|
|
231
189
|
| `pre-update` | _(empty)_ | Shell command before `eas update` (OTA only) |
|
|
232
190
|
| `post-update` | _(empty)_ | Shell command after a successful `eas update` (OTA only) |
|
|
233
|
-
| `update-extra-args` | _(empty)_ | Extra
|
|
191
|
+
| `update-extra-args` | _(empty)_ | Extra arguments appended to `eas update`, split like a shell (quotes group) |
|
|
234
192
|
| `export-source-maps` | `false` | OTA only: `expo export --source-maps external`, refuse if `dist` has no `.map`, then `eas update --input-dir dist --skip-bundler`. Pair with `post-update` to upload those maps for PostHog error tracking. |
|
|
235
193
|
| `group` | _(empty)_ | Required when `operation` is `republish` |
|
|
236
194
|
| `runtime-version` | current fingerprint | Used when `operation` is `rollback-embedded` |
|
|
@@ -242,14 +200,28 @@ A manual `deploy` from `workflow_dispatch` ignores the impact diff. That is inte
|
|
|
242
200
|
| Output | Notes |
|
|
243
201
|
|--------|--------|
|
|
244
202
|
| `deploy_type` | Stable PR-check type: `none`, `ota`, `native` |
|
|
203
|
+
| `changed` | `true` unless `deploy_type` is `none` |
|
|
204
|
+
| `reason` | Why: `no-impact-path`, `finished-build`, `build-in-progress`, `no-build-for-fingerprint` |
|
|
205
|
+
| `changed_sources` | For a native decision: fingerprint sources changed since the profile's last finished build (`changed node_modules/expo-camera`), one per line |
|
|
245
206
|
| `native_build_pending` | `true` when a matching native build is still in progress |
|
|
246
207
|
| `fingerprint` | Current EAS fingerprint hash |
|
|
247
208
|
| `build_id` / `build_url` | Matching EAS build, if any |
|
|
248
|
-
| `
|
|
209
|
+
| `queued_build_id` / `queued_build_url` | The native build operate queued, if any |
|
|
210
|
+
| `result` | Operate result (`skipped`, `ota`, `native-build`, `recovery`), or `failed` |
|
|
211
|
+
| `failed_stage` | On failure, the step: `fingerprint`, `build list`, `errored build`, `native build`, `ota export`, `ota update`, `pre-update`, `post-update`, `recovery`, `doctor` |
|
|
212
|
+
| `failure` | On failure, the last lines the command printed and a fixed next step for that stage |
|
|
213
|
+
|
|
214
|
+
### When a release fails
|
|
215
|
+
|
|
216
|
+
Every EAS command runs as a named stage, and the log shows how long each took. A failure ends with one annotation that names the stage, profile and platform and quotes EAS's last lines, for example `expo-release: native build failed (production, ios): Credentials are not set up.`, without a stack trace. The job summary repeats the last 20 lines and a next step chosen by stage, and `failed_stage` lets a workflow branch on the stage instead of matching EAS's wording.
|
|
217
|
+
|
|
218
|
+
When `eas fingerprint:generate` prints a warning before its JSON, the app config did not load and eas-cli fell back to its own `@expo/config`. The decision then fails and quotes `expo config`'s error, because that fingerprint may not be the one EAS builds with.
|
|
219
|
+
|
|
220
|
+
An errored build for the current fingerprint fails the run with EAS's error code and message and the build's dashboard link. Fix it, then dispatch `operation: force-native`.
|
|
249
221
|
|
|
250
222
|
### PR checks (decide only)
|
|
251
223
|
|
|
252
|
-
PR jobs
|
|
224
|
+
PR jobs usually only decide. Use `command: decide` and comment or gate on `deploy_type`. On a `pull_request` event `base` defaults to the merge base with the PR's base branch, so no merge-base step is needed:
|
|
253
225
|
|
|
254
226
|
```yaml
|
|
255
227
|
- uses: HansKristoffer/expo-release@v2
|
|
@@ -257,15 +229,9 @@ PR jobs should not operate. Use `command: decide` and comment or gate on `deploy
|
|
|
257
229
|
with:
|
|
258
230
|
command: decide
|
|
259
231
|
profile: pr-preview # or production
|
|
260
|
-
base: ${{ steps.merge-base.outputs.sha }}
|
|
261
|
-
head: ${{ github.sha }}
|
|
262
|
-
env:
|
|
263
|
-
APP_VARIANT: staging
|
|
264
|
-
SECRETS_ENV: staging
|
|
265
|
-
APS_ENVIRONMENT: production
|
|
266
232
|
```
|
|
267
233
|
|
|
268
|
-
Then use `${{ steps.decision.outputs.deploy_type }}` (`none` / `ota` / `native`).
|
|
234
|
+
Then use `${{ steps.decision.outputs.deploy_type }}` (`none` / `ota` / `native`), `reason`, and for `native` the `changed_sources` that make it native. A PR comment built from them tells the author whether the change needs a store build. A PR job that does build can run `command: release` with `auto-submit: 'false'` and read `queued_build_url`.
|
|
269
235
|
|
|
270
236
|
The CLI writes the same JSON to stdout, including `deployType`:
|
|
271
237
|
|
|
@@ -274,7 +240,7 @@ bun "${{ github.action_path }}/src/cli.ts" decide \
|
|
|
274
240
|
--base "$BASE" --head "$HEAD" --profile production
|
|
275
241
|
```
|
|
276
242
|
|
|
277
|
-
|
|
243
|
+
Every EAS command inherits the process env, including the profile's eas.json `env`. Variables that are not in the profile go on the **action step or the job**. When `app.config.ts` switches slug or bundle identifier on `APP_VARIANT`, every EAS command (`fingerprint:generate`, `build:list`, `eas build`, `eas update`) resolves a different project from that config, so keep it in the profile's `env`.
|
|
278
244
|
|
|
279
245
|
### OTA hooks
|
|
280
246
|
|
|
@@ -292,13 +258,9 @@ Use it with PostHog error tracking: the same `dist` bundle that goes to `eas upd
|
|
|
292
258
|
environment: production
|
|
293
259
|
export-source-maps: 'true'
|
|
294
260
|
post-update: bunx posthog-cli hermes upload --directory dist
|
|
295
|
-
env:
|
|
296
|
-
APP_VARIANT: production
|
|
297
|
-
SECRETS_ENV: production
|
|
298
|
-
APS_ENVIRONMENT: production
|
|
299
261
|
```
|
|
300
262
|
|
|
301
|
-
Override the Expo binary with `EXPO_BIN` (defaults to `bunx expo`). `--dry-run` prints the export command and skips the `.map` check.
|
|
263
|
+
Override the Expo binary with `EXPO_BIN` (defaults to the app's `node_modules/.bin/expo`, else `bunx expo`). `--dry-run` prints the export command and skips the `.map` check.
|
|
302
264
|
|
|
303
265
|
### Recovery operations
|
|
304
266
|
|
|
@@ -323,6 +285,41 @@ Override the Expo binary with `EXPO_BIN` (defaults to `bunx expo`). `--dry-run`
|
|
|
323
285
|
dry-run: 'true'
|
|
324
286
|
```
|
|
325
287
|
|
|
288
|
+
### Doctor
|
|
289
|
+
|
|
290
|
+
`expo-release doctor --profile <profile>` (or `command: doctor`) prints one line per problem with its fix and exits 1 on an error:
|
|
291
|
+
|
|
292
|
+
- `eas.json` exists, has the build profile, the profile has a `channel`, and `submit.<profile>` exists when auto-submit is on
|
|
293
|
+
- an EAS environment is known for Expo SDK 55+
|
|
294
|
+
- eas-cli runs and satisfies `eas.json` `cli.version`; `EXPO_TOKEN` is set in CI
|
|
295
|
+
- the running Bun equals the profile's `bun` (error) and Node its `node` (warning)
|
|
296
|
+
- no credentials are committed (`*.keystore` except `debug.keystore`, `*.jks`, `*.p8`, `*.p12`, `*.mobileprovision`, service-account JSON), nor `.expo/` or `*.log`
|
|
297
|
+
- committed `android/` or `ios/` are not hidden by `.fingerprintignore`; with config plugins, committed native folders get a warning
|
|
298
|
+
- the app config loads and uses `runtimeVersion: { policy: 'fingerprint' }`
|
|
299
|
+
|
|
300
|
+
`decide` and `release` run the same checks, except the app config, before their first EAS call and print the findings as warnings without failing.
|
|
301
|
+
|
|
302
|
+
### Fingerprint config
|
|
303
|
+
|
|
304
|
+
EAS increments `ios.buildNumber` and `android.versionCode` remotely, so skip `ExpoConfigVersions` and add the app version back: a new store version then needs a native build, not an OTA. With continuous native generation, ignore the native folders so a local prebuild cannot change the fingerprint. `apps/expo/fingerprint.config.js`:
|
|
305
|
+
|
|
306
|
+
```js
|
|
307
|
+
const { version } = require('./package.json')
|
|
308
|
+
|
|
309
|
+
/** @type {import('@expo/fingerprint').Config} */
|
|
310
|
+
module.exports = {
|
|
311
|
+
sourceSkips: [
|
|
312
|
+
'ExpoConfigRuntimeVersionIfString',
|
|
313
|
+
'ExpoConfigVersions',
|
|
314
|
+
'PackageJsonAndroidAndIosScriptsIfNotContainRun'
|
|
315
|
+
],
|
|
316
|
+
extraSources: [{ type: 'contents', id: 'appVersion', contents: version }],
|
|
317
|
+
ignorePaths: ['ios/**/*', 'android/**/*']
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Do not list `android` or `ios` in `.fingerprintignore` while they are committed: EAS builds the committed tree, and a change there would never queue a native build.
|
|
322
|
+
|
|
326
323
|
## Multiple apps (staging and production)
|
|
327
324
|
|
|
328
325
|
Use one job per profile, or a matrix. Each profile needs its own finished native build before OTAs work. One `EXPO_TOKEN` covers multiple projects under the same Expo account; separate accounts or orgs need separate secrets.
|
|
@@ -350,10 +347,6 @@ jobs:
|
|
|
350
347
|
- uses: HansKristoffer/expo-release@v2
|
|
351
348
|
with:
|
|
352
349
|
profile: production
|
|
353
|
-
environment: production
|
|
354
|
-
channel: production
|
|
355
|
-
env:
|
|
356
|
-
APP_VARIANT: production
|
|
357
350
|
|
|
358
351
|
staging:
|
|
359
352
|
if: github.ref == 'refs/heads/develop'
|
|
@@ -362,10 +355,6 @@ jobs:
|
|
|
362
355
|
- uses: HansKristoffer/expo-release@v2
|
|
363
356
|
with:
|
|
364
357
|
profile: staging
|
|
365
|
-
environment: staging
|
|
366
|
-
channel: staging
|
|
367
|
-
env:
|
|
368
|
-
APP_VARIANT: staging
|
|
369
358
|
```
|
|
370
359
|
|
|
371
360
|
### One job, matrix
|
|
@@ -376,23 +365,15 @@ strategy:
|
|
|
376
365
|
matrix:
|
|
377
366
|
include:
|
|
378
367
|
- profile: production
|
|
379
|
-
environment: production
|
|
380
|
-
variant: production
|
|
381
368
|
- profile: staging
|
|
382
|
-
environment: staging
|
|
383
|
-
variant: staging
|
|
384
369
|
|
|
385
370
|
steps:
|
|
386
371
|
- uses: HansKristoffer/expo-release@v2
|
|
387
372
|
with:
|
|
388
|
-
profile: ${{ matrix.profile }}
|
|
389
|
-
environment: ${{ matrix.environment }}
|
|
390
|
-
channel: ${{ matrix.profile }}
|
|
391
|
-
env:
|
|
392
|
-
APP_VARIANT: ${{ matrix.variant }}
|
|
373
|
+
profile: ${{ matrix.profile }} # channel, environment, env from eas.json
|
|
393
374
|
```
|
|
394
375
|
|
|
395
|
-
`channel` defaults to
|
|
376
|
+
`channel` defaults to the profile's eas.json `channel`, then to the profile name. Set `channel` only to publish somewhere the builds do not listen.
|
|
396
377
|
|
|
397
378
|
## CLI
|
|
398
379
|
|
|
@@ -423,11 +404,13 @@ expo-release operate \
|
|
|
423
404
|
[--pre-update "bun scripts/release/tag-dist.ts"] \
|
|
424
405
|
[--update-extra-args "--private-key-path key"] \
|
|
425
406
|
[--post-update "bunx posthog-cli hermes upload --directory dist"]
|
|
407
|
+
|
|
408
|
+
expo-release doctor --profile production [--platform ios] [--auto-submit true]
|
|
426
409
|
```
|
|
427
410
|
|
|
428
411
|
Run `expo-release <command> --help` for every flag. Each flag also reads `EXPO_RELEASE_<FLAG>` (for example `EXPO_RELEASE_IMPACT_PATHS`) when it is not passed; an empty value counts as unset.
|
|
429
412
|
|
|
430
|
-
`release` is decide + operate (same as the action default). `force-native`, `republish`, and `rollback-embedded` skip decide. `decide` prints JSON including `deployType` and `
|
|
413
|
+
`release` is decide + operate (same as the action default). `force-native`, `republish`, and `rollback-embedded` skip decide. `decide` prints JSON including `deployType`, `reason`, `nativeBuildPending` and, for `native`, `fingerprintDiff`. When `GITHUB_OUTPUT` is set, decide writes `deploy_type`, `reason`, `native_build_pending`, `changed`, `fingerprint`, `build_id`, `build_url`, `changed_sources`, `compared_build_url`. Operate writes `result`, `queued_build_id`, `queued_build_url`; a failure writes `result=failed`, `failed_stage` and `failure`.
|
|
431
414
|
|
|
432
415
|
```bash
|
|
433
416
|
# local (after bun add -D expo-release)
|
|
@@ -436,11 +419,11 @@ expo-release decide --base HEAD^ --head HEAD --profile production
|
|
|
436
419
|
|
|
437
420
|
## App runtime
|
|
438
421
|
|
|
439
|
-
`expo-release/runtime` has the app side of a release: OTA prompts, push notifications and a render-error boundary. Install the package as a dependency (not `-D`) to use it. `react`, `expo-updates`, `expo-notifications` and `expo-device` are optional peers; the CLI never needs them. The helpers render no UI: pass your toast, dialog and error screen.
|
|
422
|
+
`expo-release/runtime` has the app side of a release: OTA prompts, push notifications and a render-error boundary. Install the package as a dependency (not `-D`) to use it. `react`, `react-native`, `expo-updates`, `expo-notifications` and `expo-device` are optional peers; the CLI never needs them. The helpers render no UI: pass your toast, dialog and error screen.
|
|
440
423
|
|
|
441
424
|
### OTA updates
|
|
442
425
|
|
|
443
|
-
Checks once per launch, downloads a new update and hands you a `reload` for your "Restart" prompt. It is off in development and wherever expo-updates is disabled (Expo Go).
|
|
426
|
+
Checks once per launch, downloads a new update and hands you a `reload` for your "Restart" prompt, once per update. It is off in development and wherever expo-updates is disabled (Expo Go). An app that stays open for days should also check on foreground: `recheckOnForeground: 30` checks again when the app becomes active, at most every 30 minutes. A failed check goes to `onError`, by default `console.warn`.
|
|
444
427
|
|
|
445
428
|
```tsx
|
|
446
429
|
import { useOTAUpdates } from 'expo-release/runtime'
|
|
@@ -454,12 +437,15 @@ function AppContent() {
|
|
|
454
437
|
description: 'A new version downloaded. Tap Restart to apply.',
|
|
455
438
|
actionLabel: 'Restart',
|
|
456
439
|
onActionPress: reload
|
|
457
|
-
})
|
|
440
|
+
}),
|
|
441
|
+
{ recheckOnForeground: 30 }
|
|
458
442
|
)
|
|
459
443
|
}
|
|
460
444
|
```
|
|
461
445
|
|
|
462
|
-
The callback is read through a ref, so an unstable toast function never repeats the check. `checkForOTAUpdate(onUpdateReady)` runs the same check outside React.
|
|
446
|
+
The callback is read through a ref, so an unstable toast function never repeats the check. `checkForOTAUpdate(onUpdateReady)` runs the same check outside React, for example from an error screen's "Update and restart" button.
|
|
447
|
+
|
|
448
|
+
`getReleaseInfo()` returns `{ updateId, runtimeVersion, channel, createdAt, isEmbeddedLaunch }` for the running release (nulls in Expo Go and for the embedded bundle). Attach it to crash reports and show it on a version screen.
|
|
463
449
|
|
|
464
450
|
### Push notifications
|
|
465
451
|
|
|
@@ -492,9 +478,10 @@ export const push = createPushNotifications({
|
|
|
492
478
|
| Member | Use |
|
|
493
479
|
|--------|-----|
|
|
494
480
|
| `prompt()` | After sign-in. Asks once per install (soft-ask, then OS dialog), registers the token on a grant. Never throws; concurrent calls share one dialog. |
|
|
495
|
-
| `enable()` / `disable()` | Settings toggle. `enable` requests permission and registers; `disable` calls `registerDevice(null)
|
|
496
|
-
| `syncToken()` | Registers the current token when permission is granted. |
|
|
497
|
-
| `listen({ onReceived, onResponse })` | Subscribes and returns the unsubscribe function. `onResponse` also receives the tap that launched the app. Also keeps the token current: on start and whenever Expo rotates it, a token that differs from the last one sent is registered again. This runs only after a token was sent and `disable()` has not run since. Call it once at the root of the app. |
|
|
481
|
+
| `enable()` / `disable()` | Settings toggle. `enable` requests permission and registers; `disable` calls `registerDevice(null)` and rejects when it does. Use `disable()` to turn push off (also on sign-out, in a `try`) so the token is not re-sent on the next launch. |
|
|
482
|
+
| `syncToken()` | Registers the current token when permission is granted. Call it after a later sign-in when the user's saved preference is on: `prompt()` asks only once per install, and `disable()` on sign-out removed the token. |
|
|
483
|
+
| `listen({ onReceived, onResponse })` | Subscribes and returns the unsubscribe function. `onResponse` also receives the tap that launched the app; each response is delivered once per app start, however often `listen` runs. Also keeps the token current: on start and whenever Expo rotates it, a token that differs from the last one sent is registered again. This runs only after a token was sent and `disable()` has not run since. Call it once at the root of the app. |
|
|
484
|
+
| `setAndroidChannel(channel)` | Recreates the Android `default` channel, for example with translated texts once i18n is ready. |
|
|
498
485
|
| `getPermissionStatus()`, `getPushToken()`, `requestPermissions()`, `hasPrompted()` | Lower-level steps. |
|
|
499
486
|
|
|
500
487
|
`usePushNotifications(push, { enabled, persist })` gives a settings screen `permission`, `isEnabled`, `isLoading` and wrapped `prompt`/`enable`/`disable`. `enabled` is the saved preference; while it is on, the token is re-registered on mount.
|
|
@@ -520,12 +507,16 @@ const onToggle = async (on: boolean) => {
|
|
|
520
507
|
### Error boundary
|
|
521
508
|
|
|
522
509
|
```tsx
|
|
523
|
-
import { ErrorBoundary } from 'expo-release/runtime'
|
|
510
|
+
import { ErrorBoundary, getReleaseInfo } from 'expo-release/runtime'
|
|
524
511
|
|
|
525
512
|
<ErrorBoundary
|
|
526
513
|
fallback={({ error, reset }) => <ErrorScreen message={error.message} onRetry={reset} />}
|
|
527
514
|
onError={(error, info) =>
|
|
528
|
-
posthog.captureException(error, {
|
|
515
|
+
posthog.captureException(error, {
|
|
516
|
+
$exception_source: 'error_boundary',
|
|
517
|
+
component_stack: info.componentStack,
|
|
518
|
+
...getReleaseInfo()
|
|
519
|
+
})
|
|
529
520
|
}
|
|
530
521
|
>
|
|
531
522
|
<App />
|
|
@@ -540,9 +531,9 @@ Composite actions for Bun monorepos, used as `HansKristoffer/expo-release/.githu
|
|
|
540
531
|
|
|
541
532
|
| Action | Does |
|
|
542
533
|
|--------|------|
|
|
543
|
-
| `setup-workspace` | Bun (version from `package.json` `packageManager` by default), optional Node, restore the dependency cache keyed on `bun.lock`, always `bun install --frozen-lockfile`, save on a miss with `save-cache: 'true'
|
|
544
|
-
| `playwright-browsers` | Restores browsers for the installed Playwright version; installs and saves on a miss. A self-hosted runner skips the cache and keeps `~/.cache/ms-playwright`. |
|
|
545
|
-
| `pr-preview` | Railway preview environment per PR with its own PlanetScale Postgres branch and a Cloudflare-managed host. Creates on open/sync, deletes on close. The Railway CLI goes into the job's `$RUNNER_TEMP`. |
|
|
534
|
+
| `setup-workspace` | Bun (version from `package.json` `packageManager` by default), optional Node, restore the dependency cache keyed on the OS, architecture and `bun.lock`, always `bun install --frozen-lockfile`, save on a miss with `save-cache: 'true'` (a failed save is a warning). `cache-paths` and `cache-key-files` are inputs. `env` on the `uses:` step reaches `bun install` and its postinstall scripts (a `DATABASE_URL` for a generated client, say). On a self-hosted runner it keeps `cache-paths` instead (see below). |
|
|
535
|
+
| `playwright-browsers` | Restores browsers for the installed Playwright version and architecture; installs and saves on a miss. A self-hosted runner skips the cache and keeps `~/.cache/ms-playwright`. |
|
|
536
|
+
| `pr-preview` | Railway preview environment per PR with its own PlanetScale Postgres branch and a Cloudflare-managed host. Creates on open/sync, deletes on close. It first checks every input and each provider's access and names each problem in its own annotation; a failed delete fails the job. The Railway CLI goes into the job's `$RUNNER_TEMP`. |
|
|
546
537
|
| `ai-changelog` | Plain-language changelog of a pushed range with Claude; outputs `message` (chat) and `markdown`, optionally POSTs the message to a webhook. |
|
|
547
538
|
|
|
548
539
|
### Warm the caches on main
|
|
@@ -582,7 +573,7 @@ Use the same `cache-paths` in every job, since they are part of the cache versio
|
|
|
582
573
|
|
|
583
574
|
### Self-hosted runners
|
|
584
575
|
|
|
585
|
-
A self-hosted runner keeps its workspace between jobs, so these actions keep what a hosted VM would restore. Check out with `clean: false`; `setup-workspace` then removes every untracked file except `cache-paths` and `keep-paths`, skips the cache, and skips `bun install` when the files named by `install-key-paths` and the Bun version are unchanged since that runner's last install. Add the files a postinstall reads:
|
|
576
|
+
A self-hosted runner keeps its workspace between jobs, so these actions keep what a hosted VM would restore. Check out with `clean: false`; `setup-workspace` then removes every untracked file except `cache-paths` and `keep-paths`, skips the cache, and skips `bun install` when the files named by `install-key-paths` and the Bun version are unchanged since that runner's last install and every `cache-paths` entry without a glob still exists. When a checkout without `clean: false` wiped `node_modules`, it says so in a notice. Add the files a postinstall reads:
|
|
586
577
|
|
|
587
578
|
```yaml
|
|
588
579
|
- uses: actions/checkout@v5
|
|
@@ -603,6 +594,8 @@ A self-hosted runner keeps its workspace between jobs, so these actions keep wha
|
|
|
603
594
|
packages/prisma/schema
|
|
604
595
|
```
|
|
605
596
|
|
|
597
|
+
On a self-hosted runner the release action points `TMPDIR` at the job's `runner.temp` when the job has not set it, so Metro's cache never collides with another user's `/tmp/metro-cache` (`EACCES`).
|
|
598
|
+
|
|
606
599
|
### PR preview environments
|
|
607
600
|
|
|
608
601
|
The preview duplicates `railway-source-environment` (default `staging`) into `pr-<number>`, points git-connected services at the PR branch, sets `DATABASE_URL` and `DATABASE_URL_DIRECT` to a fresh PlanetScale branch, attaches `pr-<number>.<zone>` with Cloudflare DNS, then sets `BASE_URL`, which triggers the one deployment (run migrations and seeds in your pre-deploy step).
|
package/action.yml
CHANGED
|
@@ -4,11 +4,11 @@ author: Kristoffer Hansen
|
|
|
4
4
|
|
|
5
5
|
inputs:
|
|
6
6
|
command:
|
|
7
|
-
description: release (decide + operate), decide only,
|
|
7
|
+
description: release (decide + operate), decide only, operate only, or doctor
|
|
8
8
|
required: false
|
|
9
9
|
default: release
|
|
10
10
|
profile:
|
|
11
|
-
description: EAS build profile
|
|
11
|
+
description: EAS build profile. Its eas.json channel, environment and env are the defaults.
|
|
12
12
|
required: false
|
|
13
13
|
default: production
|
|
14
14
|
operation:
|
|
@@ -20,7 +20,7 @@ inputs:
|
|
|
20
20
|
required: false
|
|
21
21
|
default: none
|
|
22
22
|
base:
|
|
23
|
-
description: Git base SHA for the impact diff. Defaults to github.event.before.
|
|
23
|
+
description: Git base SHA for the impact diff. Defaults to the merge base on a pull request, else github.event.before.
|
|
24
24
|
required: false
|
|
25
25
|
default: ''
|
|
26
26
|
head:
|
|
@@ -32,11 +32,11 @@ inputs:
|
|
|
32
32
|
required: false
|
|
33
33
|
default: apps/expo/
|
|
34
34
|
environment:
|
|
35
|
-
description: EAS environment passed to eas update. Required for Expo SDK 55
|
|
35
|
+
description: EAS environment passed to eas update. Required for Expo SDK 55+, where it defaults to the eas.json profile environment.
|
|
36
36
|
required: false
|
|
37
37
|
default: ''
|
|
38
38
|
channel:
|
|
39
|
-
description: EAS Update channel. Defaults to the profile name.
|
|
39
|
+
description: EAS Update channel. Defaults to the eas.json profile channel, else the profile name.
|
|
40
40
|
required: false
|
|
41
41
|
default: ''
|
|
42
42
|
platform:
|
|
@@ -60,7 +60,7 @@ inputs:
|
|
|
60
60
|
required: false
|
|
61
61
|
default: ''
|
|
62
62
|
update-extra-args:
|
|
63
|
-
description: Extra
|
|
63
|
+
description: Extra arguments appended to eas update, split like a shell (e.g. --input-dir dist --skip-bundler)
|
|
64
64
|
required: false
|
|
65
65
|
default: ''
|
|
66
66
|
export-source-maps:
|
|
@@ -88,6 +88,15 @@ outputs:
|
|
|
88
88
|
deploy_type:
|
|
89
89
|
description: Stable PR-check type (none, ota, native)
|
|
90
90
|
value: ${{ steps.release.outputs.deploy_type }}
|
|
91
|
+
changed:
|
|
92
|
+
description: Whether the change needs a deploy (deploy_type is not none)
|
|
93
|
+
value: ${{ steps.release.outputs.changed }}
|
|
94
|
+
reason:
|
|
95
|
+
description: Why decide chose deploy_type (no-impact-path, finished-build, build-in-progress, no-build-for-fingerprint)
|
|
96
|
+
value: ${{ steps.release.outputs.reason }}
|
|
97
|
+
changed_sources:
|
|
98
|
+
description: For a native decision, the fingerprint sources changed since the profile's last finished build, one per line
|
|
99
|
+
value: ${{ steps.release.outputs.changed_sources }}
|
|
91
100
|
native_build_pending:
|
|
92
101
|
description: Whether a matching native build is still in progress
|
|
93
102
|
value: ${{ steps.release.outputs.native_build_pending }}
|
|
@@ -100,9 +109,21 @@ outputs:
|
|
|
100
109
|
build_url:
|
|
101
110
|
description: Matching EAS build URL, if any
|
|
102
111
|
value: ${{ steps.release.outputs.build_url }}
|
|
112
|
+
queued_build_id:
|
|
113
|
+
description: The native build operate queued, if any
|
|
114
|
+
value: ${{ steps.release.outputs.queued_build_id }}
|
|
115
|
+
queued_build_url:
|
|
116
|
+
description: Dashboard URL of the native build operate queued, if any
|
|
117
|
+
value: ${{ steps.release.outputs.queued_build_url }}
|
|
103
118
|
result:
|
|
104
|
-
description: Operate result (skipped, ota, native-build, recovery)
|
|
119
|
+
description: Operate result (skipped, ota, native-build, recovery), or failed
|
|
105
120
|
value: ${{ steps.release.outputs.result }}
|
|
121
|
+
failed_stage:
|
|
122
|
+
description: The step that failed (fingerprint, build list, errored build, native build, ota export, ota update, pre-update, post-update, recovery, doctor)
|
|
123
|
+
value: ${{ steps.release.outputs.failed_stage }}
|
|
124
|
+
failure:
|
|
125
|
+
description: On failure, the last lines the failed command printed and the next step
|
|
126
|
+
value: ${{ steps.release.outputs.failure }}
|
|
106
127
|
|
|
107
128
|
runs:
|
|
108
129
|
using: composite
|
|
@@ -139,7 +160,9 @@ runs:
|
|
|
139
160
|
EXPO_RELEASE_MESSAGE: ${{ inputs.message }}
|
|
140
161
|
EXPO_RELEASE_DRY_RUN: ${{ inputs.dry-run }}
|
|
141
162
|
GITHUB_EVENT_BEFORE: ${{ github.event.before }}
|
|
142
|
-
|
|
163
|
+
# The action's own tsconfig: Bun would otherwise read the app's, and warn
|
|
164
|
+
# about options it does not support (moduleSuffixes).
|
|
165
|
+
run: bun --tsconfig-override "$ACTION_PATH/tsconfig.json" "$ACTION_PATH/src/cli.ts"
|
|
143
166
|
|
|
144
167
|
- name: Publish job summary
|
|
145
168
|
if: always()
|
|
@@ -149,10 +172,17 @@ runs:
|
|
|
149
172
|
OPERATION: ${{ inputs.operation }}
|
|
150
173
|
PROFILE: ${{ inputs.profile }}
|
|
151
174
|
PLATFORM: ${{ inputs.platform }}
|
|
175
|
+
AUTO_SUBMIT: ${{ inputs.auto-submit }}
|
|
152
176
|
DEPLOY_TYPE: ${{ steps.release.outputs.deploy_type }}
|
|
177
|
+
REASON: ${{ steps.release.outputs.reason }}
|
|
153
178
|
RESULT: ${{ steps.release.outputs.result || 'did not run' }}
|
|
179
|
+
FAILED_STAGE: ${{ steps.release.outputs.failed_stage }}
|
|
180
|
+
FAILURE: ${{ steps.release.outputs.failure }}
|
|
154
181
|
FINGERPRINT: ${{ steps.release.outputs.fingerprint }}
|
|
155
182
|
MATCHING_BUILD: ${{ steps.release.outputs.build_url || steps.release.outputs.build_id }}
|
|
183
|
+
QUEUED_BUILD: ${{ steps.release.outputs.queued_build_url || steps.release.outputs.queued_build_id }}
|
|
184
|
+
CHANGED_SOURCES: ${{ steps.release.outputs.changed_sources }}
|
|
185
|
+
COMPARED_BUILD: ${{ steps.release.outputs.compared_build_url }}
|
|
156
186
|
NATIVE_PENDING: ${{ steps.release.outputs.native_build_pending }}
|
|
157
187
|
run: |
|
|
158
188
|
{
|
|
@@ -160,14 +190,35 @@ runs:
|
|
|
160
190
|
echo ""
|
|
161
191
|
echo "- Command: \`$COMMAND\`"
|
|
162
192
|
echo "- Operation: \`$OPERATION\`"
|
|
163
|
-
|
|
193
|
+
if [[ -n "$FAILED_STAGE" ]]; then
|
|
194
|
+
echo "- Result: \`$RESULT\` at \`$FAILED_STAGE\`"
|
|
195
|
+
else
|
|
196
|
+
echo "- Result: \`$RESULT\`"
|
|
197
|
+
fi
|
|
164
198
|
if [[ -n "$DEPLOY_TYPE" ]]; then
|
|
165
|
-
echo "- Deploy type: \`$DEPLOY_TYPE\`"
|
|
199
|
+
echo "- Deploy type: \`$DEPLOY_TYPE\` (\`${REASON:-unknown}\`)"
|
|
166
200
|
echo "- Native build pending: \`${NATIVE_PENDING:-false}\`"
|
|
167
201
|
echo "- Fingerprint: \`${FINGERPRINT:-none}\`"
|
|
168
202
|
echo "- Matching build: ${MATCHING_BUILD:-none}"
|
|
169
203
|
fi
|
|
170
|
-
if [[ "$
|
|
171
|
-
echo "-
|
|
204
|
+
if [[ -n "$QUEUED_BUILD" ]]; then
|
|
205
|
+
echo "- Queued build: $QUEUED_BUILD"
|
|
206
|
+
fi
|
|
207
|
+
if [[ "$RESULT" == "native-build" && "$AUTO_SUBMIT" == "true" ]]; then
|
|
208
|
+
echo "- The new build is submitted when it finishes (TestFlight on iOS, Play on Android)."
|
|
209
|
+
fi
|
|
210
|
+
if [[ -n "$CHANGED_SOURCES" ]]; then
|
|
211
|
+
echo ""
|
|
212
|
+
echo "Fingerprint sources changed since ${COMPARED_BUILD:-the last finished build}:"
|
|
213
|
+
echo ""
|
|
214
|
+
echo '```'
|
|
215
|
+
echo "$CHANGED_SOURCES"
|
|
216
|
+
echo '```'
|
|
217
|
+
fi
|
|
218
|
+
if [[ -n "$FAILURE" ]]; then
|
|
219
|
+
echo ""
|
|
220
|
+
echo '```'
|
|
221
|
+
echo "$FAILURE"
|
|
222
|
+
echo '```'
|
|
172
223
|
fi
|
|
173
224
|
} >> "$GITHUB_STEP_SUMMARY"
|