Compare commits

..
Author SHA1 Message Date
Krystof Woldrich eb3caef49b feat(simulator): expose topic commands 2026-05-20 16:24:09 +02:00
Stanisław Chmiela 6ec2ea0530 [eas-cli] Capture iOS config introspection fallback in Sentry (#3759)
## Summary
- capture local Expo CLI iOS entitlements introspection failures in Sentry before falling back to bundled config resolution
- tag the event with `app_id` from `EAS_BUILD_PROJECT_ID` and the config resolution path
- capture a short fallback message so this is easier to distinguish from generic spawn failures
- include the failed command stdout, stderr, and exit status as Sentry extras

## Why
If `expo config --json --type introspect` fails but the bundled config fallback succeeds, users no longer hit a hard failure. Capturing the original command failure gives us visibility into the remaining fallback cases without breaking builds, and the explicit message makes these events easier to identify.

## Validation

CI should pass.
2026-05-20 15:51:37 +02:00
Szymon Dziedzic d5a6b13dae [eas-cli] Add --json mode to simulator:start (#3751)
# Why

`eas simulator:start` currently only emits human-readable connect instructions, which is awkward to consume from scripts/automation. `--json` (which implies `--non-interactive`) makes the session id, job run URL, and remote config directly parseable.

# How

- Swapped `EASNonInteractiveFlag` for `EasNonInteractiveAndJsonFlags` and resolved both via `resolveNonInteractiveAndJsonFlags`, matching the pattern used by `eas upload` and other JSON-capable commands.
- Calls `enableJsonOutput()` early so spinners/log lines redirect to stderr when JSON is enabled.
- After the remote config is ready, emits a single JSON payload (`deviceRunSessionId`, `type`, `jobRunUrl`, `remoteConfig`) and returns without entering the interactive wait loop.
- Existing `--non-interactive` (without `--json`) behavior is preserved.

# Test Plan

- `yarn typecheck` reports no new errors (the two pre-existing `src/build/metadata.ts` errors are unrelated to this PR).
- `yarn run -T oxlint packages/eas-cli/src/commands/simulator/start.ts` → 0 errors.
- `yarn run -T oxfmt --check packages/eas-cli/src/commands/simulator/start.ts` → clean.
- Manual (TODO before merge): run `eas simulator:start --platform ios --json`, confirm a single JSON object lands on stdout while spinners/log output go to stderr, and that the payload contains the expected fields.
2026-05-20 12:47:00 +02:00
Ash Wu 41eb9f1499 [build-tools] Enable build time statistics for xcodebuild (#3750)
* [build-tools] Enable build time statistics for xcodebuild

Signed-off-by: Ash Wu <[email protected]>

* address comment

Signed-off-by: Ash Wu <[email protected]>

---------

Signed-off-by: Ash Wu <[email protected]>
2026-05-20 15:45:59 +08:00
Ash Wu 5c9b93fc34 [build-tools] Pass ctx.env to parse xcactivitylog (#3758)
Signed-off-by: Ash Wu <[email protected]>
2026-05-20 15:08:55 +08:00
Ash Wu fbc57831cc [build-tools] Use pre-install xclogparser (#3747)
Signed-off-by: Ash Wu <[email protected]>
2026-05-20 09:40:19 +08:00
Mustafa Abban 34ca7b0ab9 [build-tools] ignore no Gradle profile found error (#3755)
* [build-tools] ignore no Gradle profile found error

* [build-tools] ignore no Gradle profile found error

* [build-tools] ignore no Gradle profile found error
2026-05-19 15:42:33 -04:00
Expo CI 7d6e4db4e8 update CHANGELOG.md 2026-05-19 18:28:20 +00:00
Expo CI 32355274ec v19.0.1 2026-05-19 18:26:02 +00:00
Stanisław Chmiela 50ca4cbec1 [eas-cli] Restore iOS entitlements introspection fallback (#3753)
## Summary
- wrap managed iOS `expo config --json --type introspect` entitlement resolution in a local Expo CLI try/catch
- fall back to bundled `getPrebuildConfigAsync` + `compileModsAsync`, matching the previous behavior
- include stderr/stdout in the thrown error when both the local CLI and fallback fail
- check `isExpoInstalled` before spawning the local Expo CLI, consistent with the regular config reader

## Why
After `18.13.1`, managed iOS builds fail hard if the local Expo CLI introspection command exits non-zero. Previously this path could still resolve entitlements through the bundled config fallback. Restoring that fallback limits the regression while preserving the local CLI path when it is available.

## Validation

CI should pass.
2026-05-19 20:24:00 +02:00
Stanisław Chmiela 2dbb4372d8 [eas-cli] Add EXPO_NO_DOTENV when resolving iOS entitlements back (#3752)
## Summary
- restore `EXPO_NO_DOTENV=1` when resolving managed iOS entitlements through `expo config --type introspect`
- keep the introspection environment consistent with the regular Expo config path

## Why
`18.13.1` changed this path to call the local Expo CLI directly, but the entitlements-specific call no longer disabled dotenv loading. This can make the iOS-only entitlements introspection evaluate app config under a different environment than the rest of EAS CLI config resolution.

## Validation

CI should pass. Repacks should work.
2026-05-19 20:14:45 +02:00
Expo CI 4bf38452a9 update CHANGELOG.md 2026-05-19 16:42:12 +00:00
Expo CI fc7c2950e7 v19.0.0 2026-05-19 16:38:56 +00:00
Byron Karlen 30248528ad [auth] Make browser-based login the default (#3746) 2026-05-19 09:19:39 -07:00
Mustafa Abban ecf58ab8a8 [build-tools] - Gradle build profile report with task execution breakdown (#3629)
* [build] gradle parse profiler

* gradleProfileTasks

* individual tasks nested underneath with tree characters

* filter out whats less than 1s

* parseGradleProfile

* profile flag

* enable by default

* gradleProfile utils

* parse profile HTML with fast-xml-parser

* parse profile HTML with fast-xml-parser

* error handling, send to sentry

* sentry capture
2026-05-19 12:07:36 -04:00
Douglas Lowder 10c7ac64ba [eas-cli] filter observe:routes results by route names (#3744)
# Why

The `eas observe:routes` command lacked the ability to filter navigation route results by specific route names. Users had no way to narrow down the routes data to only the routes they cared about.

# How

A new `--route-name` flag (repeatable) was added to the command. When one or more values are provided, they are collected into a `routeNames` array and forwarded to the `fetchObserveNavigationRoutesAsync` function, which passes them along in the GraphQL filter. An empty array is treated the same as omitting the flag — no `routeNames` key is sent in the filter.

Example:

```
$ easd observe:routes 
EAS Observe is in preview and subject to breaking changes.

Med values (navigation count) for the last 60 days

Android
Route           Nav Cold TTR  Nav Warm TTR  Nav TTI    
--------------  ------------  ------------  -----------
/               0.18s (63)    0.08s (217)   0.22s (280)
/task/[taskId]  0.55s (39)    0.17s (139)   0.35s (178)
/new            0.37s (20)    0.13s (83)    0.26s (103)
/stats          0.88s (19)    0.36s (94)    0.64s (113)
/settings       0.15s (13)    0.06s (41)    0.12s (54) 

iOS
Route           Nav Cold TTR  Nav Warm TTR  Nav TTI    
--------------  ------------  ------------  -----------
/               0.18s (106)   0.08s (504)   0.22s (610)
/task/[taskId]  0.51s (88)    0.18s (325)   0.34s (413)
/stats          1.06s (68)    0.34s (243)   0.62s (311)
/new            0.32s (67)    0.13s (219)   0.27s (286)
/settings       0.16s (17)    0.06s (87)    0.13s (104)

$ easd observe:routes --route-name /new --route-name /settings
EAS Observe is in preview and subject to breaking changes.

Med values (navigation count) for the last 60 days

Android
Route      Nav Cold TTR  Nav Warm TTR  Nav TTI    
---------  ------------  ------------  -----------
/new       0.37s (20)    0.13s (83)    0.26s (103)
/settings  0.15s (13)    0.06s (41)    0.12s (54) 

iOS
Route      Nav Cold TTR  Nav Warm TTR  Nav TTI    
---------  ------------  ------------  -----------
/new       0.32s (67)    0.13s (219)   0.27s (286)
/settings  0.16s (17)    0.06s (87)    0.13s (104)
```

# Test Plan

- New unit tests cover both the command flag parsing and the fetch function behavior
- Manual testing with the kadiexpo/cadence app in staging
2026-05-19 03:50:12 -07:00
Phil Pluckthun 7ee81e856b fix(eas-cli): Check if expo is resolvable before assuming it's installed (#3748)
* Add resolution check to `isExpoCommand` shared with `spawnExpoCommand`

* Add CHANGELOG entry
2026-05-19 11:02:14 +01:00
Gabe Debes 8328172827 [worker] report build phase duration to www metrics endpoint (#3732)
# Why

Turtle reports build-phase durations to Datadog. Orchestrator-routed builds don't, so we lose those stats post-migration.

Ref ENG-21208

# How

Adds a `Datadog` helper in `@expo/build-tools`, following the existing Sentry singleton pattern. Worker configures it once per task with the www API base URL, turtle build id, and robot token when a token is available. `BuildContext` emits `eas.build.phase_duration` distribution samples when build phases end, while the existing build phase stats callback remains only for launcher websocket reporting.

The payload matches what shipped in https://github.com/expo/universe/pull/27475: `POST /v2/turtle-builds/:buildId/metrics` with `metrics: [{ name: 'eas.build.phase_duration', type: 'distribution', value, tags }]`. Upload failures are captured through Sentry and never fail the build. Worker flushes pending metric sends before sending terminal build results.

# Test Plan

- CI
- `yarn exec oxfmt --check CHANGELOG.md packages/build-tools/src/datadog.ts packages/build-tools/src/index.ts packages/build-tools/src/context.ts packages/build-tools/src/__tests__/datadog.test.ts packages/build-tools/src/__tests__/context.test.ts packages/worker/src/context.ts packages/worker/src/service.ts packages/worker/src/__unit__/service.test.ts`
- `yarn workspace @expo/build-tools build`
- `yarn workspace @expo/build-tools jest-unit src/__tests__/datadog.test.ts src/__tests__/context.test.ts --runInBand`
- `yarn workspace @expo/worker test:unit packages/worker/src/__unit__/service.test.ts --runInBand`
2026-05-19 11:13:53 +02:00
Expo CI 29909b223d update CHANGELOG.md 2026-05-18 21:34:02 +00:00
Expo CI dbf597957d v18.13.1 2026-05-18 21:15:12 +00:00
Mustafa Abban 4d2b406dc5 [build-job] add GRADLE_PROFILE build phase enum (#3743) 2026-05-18 16:22:22 -04:00
Douglas Lowder 774195ff74 [eas-cli] Add direct dependency on debug (#3741)
# Why

The `Log.debug()` helper in `src/log.ts` has been calling `require('debug')` under an `eslint-disable` comment, relying on the package being resolved transitively through `@babel/*` and other dependencies.

# How

Declare `debug` and `@types/debug` directly so the require is stable under stricter resolution modes and so we can drop the `eslint-disable` and the runtime require in favor of a typed import.

Fixes #3630 .

# Test Plan

- Unit tests pass
- Tested various commands and tested with `DEBUG=eas:*`
2026-05-18 12:37:09 -07:00
Phil Pluckthun 255590cd45 refactor(eas-cli): Remove npx expo invocations and config/entitlements fallbacks (#3282)
* Expose raw `spawnExpoCommand` utility

* Remove use of `npx expo` invocation for `spawnExpoCommand`

* Replace manual `npx expo` invocation in onboarding with `expoCommandAsync`

* Remove npx invocation in "new" command

* Remove catch-case from `getExpoConfigInternalAsync`

* Remove catch-case from `getManagedApplicationTargetEntitlementsAsync`

* Force fallback to `@expo/config` in tests

This isn't great and shouldn't be done, but otherwise we're mocking the
expo command utility here which also isn't great

* Update commands.test.ts

* Add CHANGELOG entry

* Apply lints
2026-05-18 11:59:30 +01:00
Szymon Dziedzic d15641948c Improve tunnel perf by using constrained stream quality and dimensions (#3737)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->

<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

Use parameters to improve tunnel perf.

# How

Use quic cloudflared tunnel protocol + constrain stream max dimension + quality to improve speed.

# Test Plan

Tested locally.
2026-05-18 11:54:14 +02:00
Stanisław Chmiela 6cbcc8537c [eas-cli] Add expoPackageVersion to build metadata (#3725)
# Why

Persist the installed `expo` package version separately from `sdkVersion` so server-side log uploads can reproduce the legacy `expo_package_version` Datadog tag without conflating it with SDK version.

# How

- Added `expoPackageVersion` to build metadata schema/types.
- Moved the existing worker `getExpoPackageVersionAsync(...)` behavior into shared `@expo/eas-build-job#getInstalledExpoPackageVersionAsync(...)`: `node --print require.resolve('expo/package.json')`, `fs.readJson`, semver validation, and the same `EAS_BUILD_EXPO_PACKAGE_VERSION_*` `UserError`s.
- `collectMetadataAsync(...)` now resolves `expoPackageVersion` inline with `asyncResult(getInstalledExpoPackageVersionAsync(...)).value`, so build creation and the existing internal `updateBuildMetadata` path send it alongside the rest of the build metadata.
- Worker error-log tags now read `expo_package_version` from build metadata; the worker no longer resolves or mutates this field.

# Test Plan

CI should pass.
2026-05-15 16:32:13 +02:00
Expo CI 5ec4eaf052 update CHANGELOG.md 2026-05-14 20:15:30 +00:00
Expo CI 08a9b5ff7f v18.13.0 2026-05-14 20:13:13 +00:00
Douglas Lowder 501af5213e [eas-cli] Extract shared observe flag groups into observe/flags.ts (#3731)
# Why

It was found that observe commands were using common flag definitions with duplicate code.

# How

Refactored the observe commands to extract flag definitions.

# Test Plan

Commands tested locally.

Unit tests pass.
2026-05-14 13:10:13 -07:00
Douglas Lowder d48f931d77 [eas-cli] Add observe:routes for navigation metrics by route name (#3730)
# Why

New command `observe:routes` to get navigation metrics by route.

```
USAGE
  $ eas observe:routes [--platform android|ios] [--metric cold_ttr|warm_ttr|nav_tti...] [--stat
    median|med|p90|count|event_count|eventCount...] [--after <value>] [--limit <value>] [--start <value> | --days
    <value>] [--end <value> | ] [--app-version <value>] [--update-id <value>] [--build-number <value>] [--project-id
    <value>] [--json] [--non-interactive]

FLAGS
  --after=<value>         Cursor for pagination. Use the endCursor from a previous query to fetch the next page.
  --app-version=<value>   Filter by app version
  --build-number=<value>  Filter by app build number
  --days=<value>          Show results from the last N days (mutually exclusive with --start/--end)
  --end=<value>           End of time range (ISO date)
  --json                  Enable JSON output, non-JSON messages will be printed to stderr. Implies --non-interactive.
  --limit=<value>         The number of items to fetch each query. Defaults to 50 and is capped at 200.
  --metric=<option>...    Navigation metric to display (can be specified multiple times). Defaults to all three.
                          <options: cold_ttr|warm_ttr|nav_tti>
  --non-interactive       Run the command in non-interactive mode.
  --platform=<option>     Filter by platform
                          <options: android|ios>
  --project-id=<value>    EAS project ID (defaults to the project ID of the current directory)
  --start=<value>         Start of time range (ISO date)
  --stat=<option>...      Statistic to display per metric (can be specified multiple times)
                          <options: median|med|p90|count|event_count|eventCount>
  --update-id=<value>     Filter by EAS update ID

DESCRIPTION
  display app navigation route metrics (Cold TTR, Warm TTR, TTI) grouped by route name

```

# How

Added a new command that that queries the new navigationRoutes GraphQL field to display Cold TTR, Warm TTR, and TTI, broken down per route name and platform.

# Test Plan

Tested with staging data in `@expokadi/cadence` project.

New unit tests added.
2026-05-14 12:11:38 -07:00
Mustafa Abban 99b18a2189 [worker] - remove logging errors to datadog (#3729)
* [worker] - remove logging errors to datadog

* [worker] - remove logging errors to datadog

* rem tests

* rem tests
2026-05-13 14:09:38 -04:00
Expo CI 9ece20157f update CHANGELOG.md 2026-05-13 14:21:35 +00:00
Expo CI 985cd9d52f v18.12.3 2026-05-13 14:16:02 +00:00
Szymon DziedzicandClaude Opus 4.7 576c731d87 [eas-cli] Show web preview URL for agent-device simulator sessions (#3728)
# Why

The build-tools side of agent-device remote sessions now reports `agentDeviceRemoteSessionUrl`, `agentDeviceRemoteSessionToken`, and (on iOS only) `webPreviewUrl` via `startDeviceRunSession`. The eas-cli simulator commands still queried the old `url`/`token` fields and didn't surface the new web preview URL at all.

# How

- Updated the `AgentDeviceRunSessionRemoteConfig` selection in `DeviceRunSessionQuery` to read `agentDeviceRemoteSessionUrl`, `agentDeviceRemoteSessionToken`, and `webPreviewUrl`.
- `formatRemoteSessionInstructions` now uses the new field names for the daemon env-var exports, and — when `webPreviewUrl` is present — appends a "Open the following URL in your browser to preview the simulator" block. Android sessions (where `webPreviewUrl` is null) get the daemon-only output as before.
- Regenerated `graphql.schema.json` and `generated.ts` against staging (`EXPO_STAGING=1 yarn generate-graphql-code`).

# Test Plan

- [ ] `yarn typecheck` and `yarn lint` pass.
- [ ] `eas simulator:start --platform ios --type agent-device` — verify the output includes both the daemon exports and the web preview URL.
- [ ] `eas simulator:start --platform android --type agent-device` — verify the output includes only the daemon exports (no web preview block).
- [ ] `eas simulator:get --id <id>` — same checks against an existing session.

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-13 15:45:41 +02:00
Ash Wu efe26a729e [build-tools] Route xcactivitylog parser failures to Sentry (#3708)
Signed-off-by: Ash Wu <[email protected]>
2026-05-13 21:25:50 +08:00
Szymon DziedzicandClaude Opus 4.7 3dcfef1e2c [build-tools] Send web preview URL in agent-device remote session config (#3727)
# Why

Agent-device remote sessions on iOS need to expose a web preview URL alongside the device tunnel so clients can render the simulator preview. The config we report to the API server also lacked semantically-named keys.

# How

- On Darwin, after the agent-device daemon + cloudflared tunnel are up, also launch `serve-sim --tunnel` and capture its preview URL.
- Report `{ agentDeviceRemoteSessionUrl, agentDeviceRemoteSessionToken, webPreviewUrl? }` via `startDeviceRunSession`. `webPreviewUrl` is omitted on Linux/Android (serve-sim is iOS-only).
- Extract the serve-sim launch + URL-wait into a shared helper (`startServeSimWithTunnelAsync`) so `startServeSimRemoteSession` and `startAgentDeviceRemoteSession` share one implementation.

# Test Plan

- [ ] `yarn typecheck` and `yarn lint` pass.
- [ ] Manual: iOS device run session — verify the API server receives `agentDeviceRemoteSessionUrl`, `agentDeviceRemoteSessionToken`, and `webPreviewUrl`.
- [ ] Manual: Android device run session — verify `webPreviewUrl` is omitted but the agent device URL + token are present.

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-13 14:58:52 +02:00
Ash Wu c0e86f837e [build-tools] Move Sentry SDK from worker to build-tools as a singleton (#3712)
* [build-tools] Move Sentry SDK from worker to build-tools as a singleton

Signed-off-by: Ash Wu <[email protected]>

* address comments

Signed-off-by: Ash Wu <[email protected]>

---------

Signed-off-by: Ash Wu <[email protected]>
2026-05-13 20:35:07 +08:00
Gabe Debes ae65e74ed0 Fix CHANGELOG for version 18.12.2 (#3723)
Fix bug fixes for version 18.12.2; Wrong changelog entry from https://github.com/expo/eas-cli/pull/3657
2026-05-12 15:28:53 -07:00
Expo CI 2ba012bb30 update CHANGELOG.md 2026-05-12 22:02:59 +00:00
Expo CI 48c47bcc0c v18.12.2 2026-05-12 22:00:39 +00:00
Gabe Debes 6ad59488b0 [eas-cli] eas go server-side project setup and SDK version selection (#3657)
* eas go: server-side project config, in-memory tar.gz upload, repack workflow dispatch

.

* eas go: remove unused uploadAccountScopedBufferToGCSAsync and its tests

* eas go: restore import * as style for fs, os, path

* eas go: use fs-extra methods and restore fail() on spinner errors

* fix fmt

* eas go: restore spinner failure messages

* eas go: address review comments

* eas go: clean up minor issues

* eas go: use primaryAccount, fix noVcs test expectations
2026-05-12 13:54:41 -07:00
Brent Vatne c476f07818 [gha] Audit workflows (#3718) 2026-05-12 12:46:30 -07:00
Expo CI 329a02d06f update CHANGELOG.md 2026-05-12 15:27:09 +00:00
Expo CI f46b469267 v18.12.1 2026-05-12 15:24:18 +00:00
Szymon Dziedzic d708aaefab Add --type serve-sim to eas simulator:start (#3711)
* [build-tools] Add eas/start_serve_sim_remote_session function

# Why

Adds a new EAS build step function that starts a serve-sim server with a Cloudflare tunnel and reports the preview/stream tunnel URLs back to the API server as the remote config for a DeviceRunSession. Mirrors the existing eas/start_agent_device_remote_session flow.

# How

- Adds eas/start_serve_sim_remote_session in packages/build-tools/src/steps/functions/startServeSimRemoteSession.ts. It:
  - Selects Xcode and ensures cloudflared is installed via Homebrew.
  - Launches npx serve-sim-szdziedzic@latest --tunnel detached, with stdout/stderr captured to /tmp/serve-sim/serve-sim.log.
  - Polls the log for the Tunnel: and Stream: URLs (both *.trycloudflare.com), with a 60s timeout.
  - Calls StartDeviceRunSession with remoteConfig: { previewUrl, streamUrl }, then blocks so the tunnel stays alive until the session is stopped.
  - Declares supportedRuntimePlatforms: [BuildRuntimePlatform.DARWIN] so it only runs on iOS/macOS runners.
- Extracts the duplication shared with start_agent_device_remote_session into a new packages/build-tools/src/steps/utils/remoteDeviceRunSession.ts module (StartDeviceRunSession mutation, getDeviceRunSessionIdOrThrow, reportRemoteSessionConfigAndBlockAsync, spawnDetachedAsync, ensureBrewPackageInstalledAsync, readFileOrEmptyAsync). Both step functions now consume the shared helpers.
- Registers the new function in packages/build-tools/src/steps/easFunctions.ts.

# Test Plan

- yarn typecheck, yarn lint, yarn fmt:check all pass for the changed files.
- Not yet exercised end-to-end — requires a DEVICE_RUN_SESSION_ID injected by the API server. Once the server side is ready, smoke-test by triggering a device run session against an iOS/macOS runner and verifying that previewUrl/streamUrl show up on the session and the tunnel URLs are reachable.

* [eas-cli] Support --type serve-sim in eas simulator:start

# Why

Now that the API server exposes DeviceRunSessionType.ServeSim and ServeSimRunSessionRemoteConfig (previewUrl, streamUrl), wire up the CLI to actually launch a serve-sim session and tell the user how to use it. Companion to the build-tools step function in the previous PR.

# How

- Add ... on ServeSimRunSessionRemoteConfig { previewUrl streamUrl } to DeviceRunSessionByIdQuery and regenerate types.
- Register DeviceRunSessionType.ServeSim → 'serve-sim' in the flag-value map in eas simulator:start. Because the map is typed Record<DeviceRunSessionType, string>, the build was already failing on the missing entry (this is what was breaking verify-graphql-code in CI).
- Replace formatRemoteConfigShellSnippet with formatRemoteSessionInstructions in simulator/utils.ts. The switch over remoteConfig.__typename is now exhaustive: agent-device returns the export … shell snippet (unchanged); serve-sim returns "🌐 Open the following URL in your browser to access the simulator:" followed by previewUrl.
- start.ts and get.ts call the new function, so the headline now lives inside the formatter and stays variant-appropriate.
- Rename the user-facing "${type} daemon" copy to "${type} session" so it reads naturally for both agent-device and serve-sim.
- Tweak the command description to drop the agent-device-specific phrasing.

# Test Plan

- yarn typecheck, yarn lint, yarn fmt:check, yarn verify-graphql-code all pass locally.
- Smoke test by running eas simulator:start --platform ios --type serve-sim once the server side has rolled out and the build-tools step from the parent PR is published. Verify the CLI prints the preview URL and that opening it in a browser loads the simulator.
2026-05-12 16:52:22 +02:00
Szymon Dziedzic 0d1c91aad2 [eas-cli] Add eas simulator:get command (#3710)
# Why

We need a way to inspect an existing remote simulator session and retrieve its
shell credentials again — for example after losing the original terminal output
from `eas simulator:start`. Without this, the only way to re-attach is to start
a new session.

# How

- Adds a hidden `eas simulator:get --id <ID>` command that fetches the device
  run session by ID.
- Always prints session id, type, status, job run id, job run status, and job
  run URL (blank when the turtle job run is not available yet).
- Prints the shell snippet to attach to the session only when the session is
  `IN_PROGRESS` and `remoteConfig` is present.
- Extracts `formatRemoteConfigShellSnippet` (and the
  `DeviceRunSessionRemoteConfig` type) into `src/simulator/utils.ts` so
  `start.ts` and the new `get.ts` can share it. The helper lives outside
  `src/commands/` so oclif does not try to treat it as a command.
- Expands `DeviceRunSessionByIdQuery` with the app's `slug` and
  `ownerAccount.name` so the job-run URL can be built from the session
  response.

# Test Plan

Ran against a running session:

```
$ easd simulator:get --id 019e1c30-465b-7fd9-98a1-eb0ceaafafb5
✔ Fetched device run session 019e1c30-465b-7fd9-98a1-eb0ceaafafb5

ID:               019e1c30-465b-7fd9-98a1-eb0ceaafafb5
Type:             AGENT_DEVICE
Status:           IN_PROGRESS
Job run ID:       019e1c30-465b-75e1-b746-a528054649e9
Job run status:   IN_PROGRESS
Job run URL:      https://expo.dev/accounts/expo/projects/testapp/job-runs/019e1c30-465b-75e1-b746-a528054649e9

🔑 Run the following in your shell to attach to the session:

export AGENT_DEVICE_DAEMON_BASE_URL='...'
export AGENT_DEVICE_DAEMON_AUTH_TOKEN='...'
```

- `yarn typecheck`, `yarn lint`, and `yarn fmt:check` pass.
- Verified the previous `command simulator:utils not found` warning is gone
  (helper now lives outside `commands/`).
2026-05-12 15:37:45 +02:00
Expo CI 4f670f90b6 update CHANGELOG.md 2026-05-12 12:22:37 +00:00
Expo CI 53d294330d v18.12.0 2026-05-12 12:19:03 +00:00
Szymon Dziedzic ed4b39c55e [eas-cli] Read agent device remote config from API instead of scraping logs (#3698)
* [eas-cli] Read agent device remote config from API instead of scraping logs

Migrate `eas simulator:start` from greppping `export AGENT_DEVICE_DAEMON_*=…`
lines out of turtle job logs to reading the typed `remoteConfig` field on
`DeviceRunSession` via GraphQL.

The `DeviceRunSessionByIdQuery` now selects:

  remoteConfig {
    __typename
    ... on AgentDeviceRunSessionRemoteConfig { url token }
  }

…and the readiness poll succeeds as soon as `session.remoteConfig` is
non-null. The per-type `ReadinessChecker` indirection and all the log
fetch/parse helpers (fetchLogMessagesAsync, extractLogMessage,
extractExportedEnvValue, escapeRegExp, …) are removed; a small
`formatRemoteConfigShellSnippet` switch on `__typename` replaces them and
preserves the existing `export AGENT_DEVICE_DAEMON_*=…` user-facing
output.

Pairs with the producer-side change in expo/eas-cli#3696 — that PR
removes the `logger.info('export AGENT_DEVICE_DAEMON_*=…')` lines this
command previously scraped. Without this consumer migration, anyone on a
worker image that includes #3696 would hit POLL_TIMEOUT_MS waiting for
log lines that no longer exist. Also picks up an incidental
`yarn generate-graphql-code` regen of `generated.ts` /
`graphql.schema.json`.

* improvements after cr
2026-05-12 14:16:28 +02:00
Szymon Dziedzic a04d336921 [build-tools] Report agent device remote config via startDeviceRunSession (#3696)
* [build-tools] Report agent device remote config via startDeviceRunSession

Take CustomBuildContext so the eas/start_agent_device_remote_session step
can use ctx.graphqlClient (already authenticated with the scoped JWT from
job.secrets.robotAccessToken), matching the maestro reporter pattern.
After the cloudflared tunnel is up, call startDeviceRunSession(
deviceRunSessionId, { url, token }) using DEVICE_RUN_SESSION_ID from the
job env, instead of just printing the credentials. Throw if the env is
missing — this step is only meant to run as part of an
API-server-created device run session.

* use system error
2026-05-12 13:03:23 +02:00
Ash Wu f83d3b8ee0 [build-tools] rename smart_retry to retry_failed_only for maestro job (#3709)
Signed-off-by: Ash Wu <[email protected]>
2026-05-12 17:44:13 +08:00
Brent Vatne 9b5f237bf2 Fix .yarnrc.yml formatting 2026-05-11 19:33:48 -07:00
Brent Vatne 66cc6d2cf9 chore: Enforce minimum age for npm packages 2026-05-11 18:38:15 -07:00
Mustafa Abban d9ff560dab [build-internal] - handle errors when running internal build (#3706)
* [build-internal] - handle errors when running internal build

* [build-internal] - handle errors when running internal build

* [build-internal] - handle errors when running internal build
2026-05-11 13:15:49 -04:00
Ash Wu 1f53177ecf [build-tools] bump xclogparser default version (#3703)
Signed-off-by: Ash Wu <[email protected]>
2026-05-11 21:55:38 +08:00
Ash Wu b1f606979a [build-tools] improve xcactivitylog output (#3702)
Signed-off-by: Ash Wu <[email protected]>
2026-05-11 21:37:01 +08:00
Ash Wu 71f968e971 [build-tools] add smart_retry to maestro job (#3688)
Signed-off-by: Ash Wu <[email protected]>
2026-05-11 21:18:51 +08:00
Szymon Dziedzic 687e8897c1 [build-tools] Add Android support to start_agent_device_remote_session (#3697)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

`eas/start_agent_device_remote_session` only worked on macOS (iOS) workers — it ran `xcode-select` and used `brew` to install `cloudflared`, so an Android workflow that tried to call it would fail. We want the same agent-device remote session functionality on Android (Linux) workers so a single step can drive both platforms with identical outputs.

# How

- Made the function platform-aware by reading `global.runtimePlatform`.
- Darwin path is unchanged: `xcode-select`, then `brew install cloudflared`.
- Linux path: skip `xcode-select` and, if `cloudflared` isn't already on `PATH`, download it from GitHub releases via `sudo curl` into `/usr/local/bin/cloudflared` (arch-aware: `linux-amd64` / `linux-arm64`) and `chmod +x`.
- `bun` is pre-installed on both worker images, so no install logic is needed.
- Daemon launch (`bun run src/daemon.ts`), `~/.agent-device/daemon.json` polling, the cloudflared tunnel, and the emitted `AGENT_DEVICE_DAEMON_BASE_URL` / `AGENT_DEVICE_DAEMON_AUTH_TOKEN` are unchanged — same final effect on both platforms.

# Test Plan

- `yarn typecheck` — clean
- `yarn lint` — 0 new errors (pre-existing warnings only)
- `yarn fmt:check` — clean
- `yarn test` in `packages/build-tools` — 480 tests pass
- Still to verify on a real Android worker: confirm `cloudflared` gets installed on a fresh image, the daemon writes credentials, the tunnel returns a `*.trycloudflare.com` URL, and the exported env vars work end-to-end against a connected Android device/emulator.
- iOS path on macOS workers should be unaffected (no behavioral change).
2026-05-11 14:24:54 +02:00
Stanisław Chmiela 0d5ef2ee89 [eas-build-job] Preserve workflow job outputs (#3704)
## Summary

This adds a shared `JobOutputs` type/schema and preserves declared workflow job outputs in Android, iOS, and generic job definitions.

Before this change, `outputs` was stripped from several build job shapes during Joi validation, including normal build jobs and custom builds that use `customBuildConfig.path`. That meant downstream worker code could not upload `jobs.<name>.outputs` for workflow `type: build` jobs.

## Changes

- Add shared `JobOutputs`, `JobOutputsSchema`, and `JobOutputsSchemaZ` definitions in `common.ts`.
- Use the shared output type/schema from Android, iOS, and Generic job definitions.
- Validate `outputs` directly on the Android and iOS top-level job schemas.
- Remove output handling from `CustomBuildConfigSchema`, so that schema only controls custom config vs inline step shape.
- Add Android and iOS schema coverage for normal build jobs, config-path custom jobs, and iOS resign jobs.

## Validation

- `packages/eas-build-job`: `yarn test android.test.ts ios.test.ts generic.test.ts`
- `packages/eas-build-job`: `yarn build`
- `yarn oxfmt --check ...`
2026-05-11 13:56:44 +02:00
Stanisław Chmiela f856d4022f Enable precompiled modules proxying for pod install (#3681) 2026-05-11 10:21:35 +02:00
Juwan Wheatley 5585ef99c8 [eas-cli] set the Convex env var on EAS automatically (#3685)
# Why

In our Convex docs (https://docs.expo.dev/guides/using-convex/#save-the-convex-url-as-an-eas-environment-variable), we tell people to set an ENV var with EAS to use in their deployments. With our Convex integration, we can automate this step and make the setup UX even smoother. 

Fixes ENG-21068

# How

I added a step to the convex:connect command to set the `EXPO_PUBLIC_CONVEX_URL` EAS env var. If it already exists, we prompt the user to prevent accidental rewrites. 

# Test Plan

`yarn build` and using the local `easd` alias, run `EXPO_STAGING=true easd integrations:convex:connect`

You should see the new output talking about the env var, and the env var should also be visible in the env var dashboard page:

<img width="1564" height="264" alt="Screenshot 2026-05-08 at 04 29 16" src="https://github.com/user-attachments/assets/b9a0340b-8993-4198-b9c0-971fabe464e0" />

<img width="656" height="162" alt="Screenshot 2026-05-08 at 04 29 35" src="https://github.com/user-attachments/assets/a3721b98-30b3-4055-b8a2-276760605c4c" />
2026-05-09 05:51:47 +09:00
Juwan Wheatley 4eb0753a06 [eas-cli] recommend that users read the docs after connecting Convex (#3683)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->

<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

We should direct users to places to learn more about using Convex after connecting the integration.   
  
Fixes ENG-21071

# How

I added some extra output that links to both our docs quickstart and the Convex docs.

# Test Plan

`yarn build` and using the local `easd` alias, run `EXPO_STAGING=true easd integrations:convex:connect`  
  
You should see the new text content at the end:

![Screenshot 2026-05-07 at 21.40.41.png](https://app.graphite.com/user-attachments/assets/132a9115-c0d5-4f2b-aaa2-c08354ac3a6d.png)
2026-05-09 05:32:53 +09:00
Ash Wu bfaff08894 [build-tools] Fix ai-*.json is not guarenteed in Maestro 2.5 (#3639)
* [build-tools] Fix ai-*.json is not guarenteed in Maestro 2.5

* fix param name & use zod according to previous comments

Signed-off-by: Ash Wu <[email protected]>

* address comments

Signed-off-by: Ash Wu <[email protected]>

---------

Signed-off-by: Ash Wu <[email protected]>
2026-05-07 14:51:23 +08:00
Mustafa Abban 64874f7dfe [build-tools] route to api v2 endpoint (#3680) 2026-05-06 14:48:19 -04:00
Mustafa Abban daa4c60e9b [build-tools] Log Gradle cache restore events to Datadog (#3674)
* [build-tools] Log Gradle cache restore events to Datadog

* route

* update routes

* remove key tags

* remove key tags

* tags
2026-05-06 13:10:39 -04:00
Stanisław Chmiela ba4e4eddbc [build-tools] Update minimum precompiled modules Expo version (#3677)
<img width="501" height="112" alt="Zrzut ekranu 2026-05-5 o 17 02 16" src="https://github.com/user-attachments/assets/7ee40f33-af83-4504-b1d9-b088674d7e81" />
2026-05-06 10:26:43 +02:00
Expo CI 6bda9c7fa0 update CHANGELOG.md 2026-05-05 16:42:27 +00:00
Expo CI 4dec5b3765 v18.11.0 2026-05-05 16:39:51 +00:00
Szymon Dziedzic 0e3216e681 [eas-cli] Hold eas simulator:start until session ends or Ctrl+C (#3676) 2026-05-05 18:35:29 +02:00
Matheus Michels 2a191700f7 [eas-cli] Add simulator flag to build dev (#3637) 2026-05-05 10:51:36 +01:00
Ash Wu 4da5b40520 [build-tools] Add eas/run_maestro_tests step with smart retry (#3635)
* [build-tools] Add smart retry to eas/run_maestro_tests

In `reuse_devices` mode, narrow the flow_paths to only the failing flows
on retry. After a failed attempt, parse the per-attempt JUnit XML to
identify the flows that failed and pass just those to the next maestro
invocation.

Falls back to retrying everything when the JUnit cannot be trusted
(missing testcases, conflicting names, malformed XML).

Replaces copy-latest with mergeJUnitReports — picks the latest attempt's
result per flow across retries — so the merged final report reflects the
best outcome each flow achieved. Falls back to copy-latest on data errors.

Signed-off-by: Ash Wu <[email protected]>

* address comments

Signed-off-by: Ash Wu <[email protected]>

---------

Signed-off-by: Ash Wu <[email protected]>
2026-05-05 17:27:20 +08:00
Szymon Świerk d23248e4ac [eas cli] Tar creation: force the owner write bit on for directories (#3489)
* [eas cli] tar creation on windows: force the owner write bit on for directories

* add changelog

* merge branch main

* force write bit for all platforms, not windows only
2026-05-05 10:01:29 +02:00
Douglas Lowder 3318392620 [eas-cli] Add observe:logs command for viewing custom events (#3638)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->

<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

Add new command to retrieve custom events for Expo Observe.

```
USAGE
  $ eas observe:logs [EVENTNAME] [--platform android|ios] [--after <value>] [--limit <value>] [--start <value> |
    --days <value>] [--end <value> | ] [--app-version <value>] [--update-id <value>] [--session-id <value>]
    [--all-events] [--project-id <value>] [--json] [--non-interactive]

ARGUMENTS
  [EVENTNAME]  Custom event name to filter by

FLAGS
  --after=<value>        Cursor for pagination. Use the endCursor from a previous query to fetch the next page.
  --all-events           When no event name argument is provided, list all events across all event names instead of a
                         summary of event names + counts.
  --app-version=<value>  Filter by app version
  --days=<value>         Show events from the last N days (mutually exclusive with --start/--end)
  --end=<value>          End of time range (ISO date)
  --json                 Enable JSON output, non-JSON messages will be printed to stderr. Implies --non-interactive.
  --limit=<value>        The number of items to fetch each query. Defaults to 10 and is capped at 100.
  --non-interactive      Run the command in non-interactive mode.
  --platform=<option>    Filter by platform
                         <options: android|ios>
  --project-id=<value>   EAS project ID (defaults to the project ID of the current directory)
  --session-id=<value>   Filter by session ID
  --start=<value>        Start of time range (ISO date)
  --update-id=<value>    Filter by EAS update ID

DESCRIPTION
  display individual custom events (logs) emitted by the app, filtered by the event name in the argument. With no
  arguments, a list of the available event names and associated event counts is returned.


```

# How

- Add new GraphQL query and fragment, and new formatter for the retrieved custom events
- New command `observe:logs`, designed to be similar to `observe:events`
- Did some refactoring so that observe commands reuse methods for formatting results

The command can return a list of the different event names found and their counts, a list of events for a specific name, or a list of all events found:

```
  ┌───────────────────────────┬───────────────────────────────────────────┐                                             
  │        invocation         │                  result                   │                                             
  ├───────────────────────────┼───────────────────────────────────────────┤                                             
  │ observe:logs              │ event-name summary table (names + counts) │                                             
  ├───────────────────────────┼───────────────────────────────────────────┤                                             
  │ observe:logs --all-events │ full list of events across all names      │                                             
  ├───────────────────────────┼───────────────────────────────────────────┤                                             
  │ observe:logs <event>      │ full list of events filtered by name      │                                             
  └───────────────────────────┴───────────────────────────────────────────┘  
```

# Test Plan

- New unit tests added
- Will test against Kadi's ingestion endpoint data
2026-05-04 13:24:31 -07:00
Juwan Wheatley b5fac2a066 [eas-cli] Fix Convex invite output (#3672) 2026-05-04 15:43:51 -04:00
Mustafa Abban 138f7973ae [eas-cli] Add lockfile existence preflight check for eas build (#3665) 2026-05-04 15:31:36 -04:00
Szymon Dziedzic 51311f1813 [eas-cli] stop device run session when time out happens (#3634) 2026-05-04 18:59:07 +02:00
Expo CI a85b60c11e update CHANGELOG.md 2026-05-04 09:47:52 +00:00
Expo CI cacd48bf66 v18.10.0 2026-05-04 09:45:14 +00:00
Stanisław Chmiela bc213a51b9 [eas-cli] Create portable project archives (#3234)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

Project archives created on one platform can be extracted on another platform with incompatible metadata or mode bits. In particular, Windows read-only directories can be represented as read-only POSIX directories in the tarball, which can make worker extraction fail when child files need to be created.

Using `portable: true` consistently also strips owner/group metadata and avoids making Windows a special case.

Supersedes the approach in #3489 and avoids changing worker extraction as proposed in #3663.

# How

Create project tarballs with `portable: true` on all platforms.

# Test Plan

- `yarn workspace eas-cli test src/build/utils/__tests__/repository.test.ts`
- `yarn workspace eas-cli typecheck`
- `yarn run -T oxfmt CHANGELOG.md packages/eas-cli/src/build/utils/repository.ts packages/eas-cli/src/build/utils/__tests__/repository.test.ts`
2026-05-04 11:41:52 +02:00
Juwan Wheatley ddd8bb7fdd [eas-cli] Add Convex E2E runner (#3654)
# Why
The Convex integration needs a repeatable manual end-to-end check against the deployed API without putting flaky real-resource setup in CI.

# How
Adds `packages/eas-cli/scripts/convex-e2e.ts`, which bootstraps one fresh Expo app, runs the Convex connect/display/invite/dashboard/delete flow with the repo-local CLI, verifies env output and package install, and cleans up EAS-side links when safe. Usage and cleanup caveats live in the script header.

# Test Plan
Run locally from the top of the stack:
```sh
cd packages/eas-cli

yarn build

yarn ts-node scripts/convex-e2e.ts
```

Verification from the successful run:
```text
✔ Created @fiberjw/convex-e2e-molzwy56
✔ Installed the convex npm package
✔ Created Convex team fiberjw's team (Expo) / fiberjw-c7b29
✔ Created Convex project convex-e2e-molzwy56 with deployment zany-trout-356
✔ Sent Convex team invitation
✔ Wrote Convex config to .env.local
Verified .env.local contains Convex environment variables
Verified package.json includes convex@^1.37.0
Convex teams linked to @fiberjw
Team: fiberjw's team (Expo) / fiberjw-c7b29
Convex project linked to convex-e2e-molzwy56
✔ Opened https://dashboard.convex.dev/t/fiberjw-c7b29/convex-e2e-molzwy56
✔ Removed Convex project convex-e2e-molzwy56 from EAS servers
✔ Removed Convex team fiberjw's team (Expo) / fiberjw-c7b29 from EAS servers
Convex-side resources are not destroyed by this cleanup.
```
2026-05-01 18:45:20 +09:00
Juwan Wheatley ed251cb0d1 [eas-cli] Add Convex dashboard command (#3653)
# Why
Users need a quick way to open the Convex dashboard for the linked app project.

# How
Adds `eas integrations:convex:dashboard` to open the linked project dashboard, with empty-state and browser-open failure handling.

# Test Plan
Local E2E rerun after the formatting change. Linked project setup is currently blocked by the API project setup throttle, so this rerun covers the no-project dashboard path from a fresh Expo app.

Commands run (with `REPO=/Users/fiberjw/Developer/expo/eas-cli`):
```sh
cd packages/eas-cli && yarn build
cd /tmp/eas-convex-e2e-teamfmt
$REPO/packages/eas-cli/bin/run integrations:convex:dashboard
```

Output/verification:
```text
No Convex project is linked to Expo app eas-convex-e2e-teamfmt on EAS.
```
2026-05-01 18:40:54 +09:00
Juwan Wheatley 5176495e9e [eas-cli] Add Convex project delete command (#3652)
# Why
Users need a way to remove the EAS-side Convex project link for an Expo app.

# How
Adds `eas integrations:convex:project:delete`, with `--yes`, non-interactive handling, and prompts that include the Convex dashboard URL and state Convex-side resources are not destroyed.

# Test Plan
Local E2E rerun after the formatting change. Linked project setup is currently blocked by the API project setup throttle, so this rerun covers the no-project delete path from a fresh Expo app.

Commands run (with `REPO=/Users/fiberjw/Developer/expo/eas-cli`):
```sh
cd packages/eas-cli && yarn build
cd /tmp/eas-convex-e2e-teamfmt
$REPO/packages/eas-cli/bin/run integrations:convex:project:delete --non-interactive --yes
```

Output/verification:
```text
No Convex project is linked to Expo app eas-convex-e2e-teamfmt on EAS.
```
2026-05-01 18:35:42 +09:00
Juwan Wheatley 32211614eb [eas-cli] Add Convex project command (#3651)
# Why
Users need to inspect the Convex project linked to the current Expo app.

# How
Adds `eas integrations:convex:project` to display the app-linked Convex project with IDs, slugs, identifiers, team, and dashboard URL. The team line now uses the human-readable team name/slug first, then the Convex ID.

# Test Plan
Local E2E rerun after the formatting change. Linked project setup is currently blocked by the API project setup throttle, so this rerun covers the command's no-project state from a fresh Expo app.

Commands run (with `REPO=/Users/fiberjw/Developer/expo/eas-cli`):
```sh
cd packages/eas-cli && yarn build
cd /tmp/eas-convex-e2e-teamfmt
$REPO/packages/eas-cli/bin/run integrations:convex:project
```

Output/verification:
```text
No Convex project is linked to Expo app eas-convex-e2e-teamfmt on EAS.
```
2026-05-01 18:30:43 +09:00
Juwan Wheatley ce5b48244c [eas-cli] Add Convex team delete command (#3650)
# Why
Users need a way to remove stale EAS-side Convex team links.

# How
Adds `eas integrations:convex:team:delete`, with selection by ID/prompt, `--yes`, non-interactive handling, and prompts that state Convex-side resources are not destroyed.

# Test Plan
Local E2E rerun against the team link created in a fresh Expo app before project setup was API-throttled.

Commands run (with `REPO=/Users/fiberjw/Developer/expo/eas-cli`):
```sh
cd packages/eas-cli && yarn build
cd /tmp/eas-convex-e2e-teamfmt
$REPO/packages/eas-cli/bin/run integrations:convex:team:delete 019dda27-c450-7647-8ce4-97124c421f91 --non-interactive --yes
$REPO/packages/eas-cli/bin/run integrations:convex:team
```

Output/verification:
```text
Team: fiberjw's team (Expo) / fiberjw-ef4a2 (Convex ID: 478548)
EAS connection ID: 019dda27-c450-7647-8ce4-97124c421f91
Dashboard: https://dashboard.convex.dev/t/fiberjw-ef4a2
Invited email: [email protected]
Removing the Convex team link from EAS servers. This does not destroy resources on Convex: https://dashboard.convex.dev/t/fiberjw-ef4a2
✔ Removed Convex team fiberjw's team (Expo) / fiberjw-ef4a2 (Convex ID: 478548) from EAS servers
No Convex team is linked to account fiberjw on EAS.
```
2026-05-01 18:26:10 +09:00
Juwan Wheatley ece1d00ca7 [eas-cli] Add Convex team invite command (#3649)
# Why
Users may need to resend a Convex team invite if they missed the original email.

# How
Adds `eas integrations:convex:team:invite`, including previous invite metadata, recent-resend confirmation in interactive mode, and non-interactive warning behavior.

# Test Plan
Local E2E rerun against the team link created in a fresh Expo app before project setup was API-throttled.

Commands run (with `REPO=/Users/fiberjw/Developer/expo/eas-cli`):
```sh
cd packages/eas-cli && yarn build
cd /tmp/eas-convex-e2e-teamfmt
$REPO/packages/eas-cli/bin/run integrations:convex:team:invite --non-interactive
```

Output/verification:
```text
Convex team fiberjw's team (Expo) / fiberjw-ef4a2 (Convex ID: 478548)
EAS connection ID: 019dda27-c450-7647-8ce4-97124c421f91
Dashboard: https://dashboard.convex.dev/t/fiberjw-ef4a2
- Sending Convex team invitation to [email protected]
✔ Sent Convex team invitation to [email protected] for fiberjw's team (Expo) / fiberjw-ef4a2 (Convex ID: 478548)
Convex dashboard: https://dashboard.convex.dev/t/fiberjw-ef4a2
```
2026-05-01 18:21:22 +09:00
Juwan Wheatley 82dd218a2b [eas-cli] Add Convex team command (#3648)
# Why
Users need to inspect Convex team links stored for the current Expo app owner account.

# How
Adds `eas integrations:convex:team` to list linked Convex teams with the human-readable team name/slug first, then the Convex ID, plus the EAS connection ID, dashboard URL, and invite metadata.

# Test Plan
Local E2E rerun against a fresh Expo app. Project setup is currently API-throttled, but team creation succeeded before setup was rejected, allowing the display command to be verified.

Commands run (with `REPO=/Users/fiberjw/Developer/expo/eas-cli`):
```sh
cd packages/eas-cli && yarn build
npx create-expo-app@latest /tmp/eas-convex-e2e-teamfmt --yes
cd /tmp/eas-convex-e2e-teamfmt
$REPO/packages/eas-cli/bin/run init --non-interactive --force
$REPO/packages/eas-cli/bin/run integrations:convex:connect --non-interactive --project-name e2eteamfmt || true
$REPO/packages/eas-cli/bin/run integrations:convex:team
```

Output/verification:
```text
✔ Created @fiberjw/eas-convex-e2e-teamfmt
✔ Project successfully linked (ID: 194b1351-323e-4b8d-a0ce-af93fee1ac74)
✔ Created Convex team fiberjw's team (Expo) / fiberjw-ef4a2 (Convex ID: 478548)
Convex teams linked to @fiberjw
Team: fiberjw's team (Expo) / fiberjw-ef4a2 (Convex ID: 478548)
EAS connection ID: 019dda27-c450-7647-8ce4-97124c421f91
Dashboard: https://dashboard.convex.dev/t/fiberjw-ef4a2
```
2026-05-01 18:12:36 +09:00
Juwan Wheatley 9f763c6957 [eas-cli] Add Convex connect command (#3575)
# Why
Expo is integrating with Convex so users can provision an EAS-linked Convex backend from the EAS CLI.

# How
Adds `eas integrations:convex:connect`, which installs `convex`, creates or reuses an EAS-side Convex team link, sets up the app project, sends a verified-email invite with recent-resend confirmation, writes `CONVEX_DEPLOY_KEY` and `EXPO_PUBLIC_CONVEX_URL` to `.env.local`, and points users at `npx convex dev`.

This PR also includes the generated GraphQL types, Convex GraphQL helpers, shared Convex formatting/prompt utilities, and the changelog entry for the stack.

# Test Plan
Local E2E rerun after updating Convex team output formatting.

Commands run (with `REPO=/Users/fiberjw/Developer/expo/eas-cli`):
```sh
cd packages/eas-cli && yarn build
npx create-expo-app@latest /tmp/eas-convex-e2e-3575-fmt --yes
cd /tmp/eas-convex-e2e-3575-fmt
$REPO/packages/eas-cli/bin/run init --non-interactive --force
$REPO/packages/eas-cli/bin/run integrations:convex:connect --non-interactive --project-name e2e3575fmt
```

Output/verification:
```text
yarn build exited 0
✅ Your project is ready!
✔ Created @fiberjw/eas-convex-e2e-3575-fmt
✔ Project successfully linked (ID: 56a35c21-e0a0-4423-a3bd-070cd5440370)
✔ Installed the convex npm package
✔ Created Convex team fiberjw's team (Expo) / fiberjw-1d252 (Convex ID: 478547)
- Setting up Convex project
Too many Convex project setup attempts. Try again later.
Request ID: 89cc010c-e894-49df-9214-85f60d7bb334
```

The project setup API is temporarily throttled for about an hour, so the full successful connect flow could not be rerun after the formatting change. The EAS-side team link created by this blocked run was removed afterward.
2026-05-01 18:07:44 +09:00
Juwan Wheatley e74c93d80c [eas-cli] Resolve GraphQL codegen URL from API config (#3636)
# Why

Addresses the review request from https://github.com/expo/eas-cli/pull/3575#discussion_r3041624004 to have GraphQL codegen use the same Expo API URL logic as the CLI instead of hardcoding or duplicating the staging GraphQL endpoint.

# How

Replaced `graphql-codegen.yml` with a typed `graphql-codegen.ts` config.

The config imports `getExpoApiBaseUrl()` from `src/api.ts` and computes the schema URL directly in the codegen config:

- `GRAPHQL_SCHEMA_URL=... yarn generate-graphql-code` still uses the explicit override
- `EXPO_LOCAL=1 yarn generate-graphql-code` defaults to `http://127.0.0.1:3000/graphql`
- `EXPO_STAGING=1 yarn generate-graphql-code` defaults to `https://staging-api.expo.dev/graphql`
- default codegen uses `https://api.expo.dev/graphql`

This removes the package-script shell substitution and the one-off resolver script.

# Test Plan

```bash
cd packages/eas-cli

yarn ts-node -e "import config from './graphql-codegen'; console.log(config.schema)"
# https://api.expo.dev/graphql

EXPO_STAGING=1 yarn ts-node -e "import config from './graphql-codegen'; console.log(config.schema)"
# https://staging-api.expo.dev/graphql

EXPO_LOCAL=1 yarn ts-node -e "import config from './graphql-codegen'; console.log(config.schema)"
# http://127.0.0.1:3000/graphql

GRAPHQL_SCHEMA_URL=http://example.test/graphql yarn ts-node -e "import config from './graphql-codegen'; console.log(config.schema)"
# http://example.test/graphql

EXPO_LOCAL=1 yarn generate-graphql-code
yarn build
```
I tested this locally with the local WWW server both alive and dead to make sure it was calling into the API:

<img width="1864" height="1878" alt="Screenshot 2026-04-28 at 01 05 33" src="https://github.com/user-attachments/assets/18e5d017-2c3b-4c45-8b78-b471bcef93e2" />
2026-05-01 05:51:01 +09:00
MalcolmandClaude Sonnet 4.6 3d691d602c [eas-cli] Fix invalid json for eas update with --json flag (#3659)
* [eas-cli] Pass silent:true to @expo/fingerprint in createFingerprintWithoutLoggingAsync

@expo/fingerprint spawns subprocesses concurrently (autolinking, react-native
config, ExpoConfigLoader) that can write to stdout, corrupting the parent
process's output when --json is in use. The silent option exists specifically
to suppress this, but was never set. The function is already named
WithoutLogging so this is the correct default regardless of --json.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* [eas-cli] Add changelog entry for fingerprint silent fix

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
2026-04-30 11:46:25 -07:00
Stanisław Chmiela 7fde621858 [build-tools] Fix worker integration test mocks (#3662)
## Summary
- Mock `@expo/turtle-spawn` to surface the missing `expo/package.json` error path in the worker integration test
- Mock `turtleFetch` so the test can proceed without external fetch behavior
2026-04-30 18:39:45 +02:00
Ash Wu 91c9331e43 [eas-cli] remove image override in eas build:resign (#3661)
* [eas-cli] remove image override in eas build:resign

Signed-off-by: Ash Wu <[email protected]>

* [eas-cli] add changelog

Signed-off-by: Ash Wu <[email protected]>

---------

Signed-off-by: Ash Wu <[email protected]>
2026-04-30 22:57:39 +08:00
Stanisław Chmiela 02ba5880c4 [build-tools] Add expo package version to Datadog error logs (#3658)
## Summary
- Resolve the installed Expo package version in the worker when sending Turtle error logs.
- Tag Datadog error logs with `expo_package_version` while leaving `sdk_version` unchanged.
- Add unit coverage for exact-version resolution, missing package resolution, invalid semver, and the error-log payload.

## Testing
- `yarn workspace @expo/worker test:unit -- service.test.ts`
- `yarn workspace @expo/worker typecheck`
- `yarn exec oxfmt .`
2026-04-30 16:09:03 +02:00
Ash Wu 8229dc8265 [build-tools] Port Maestro Tests bash step to TypeScript (#3641)
* [build-tools] Port Maestro Tests bash step to TypeScript

Replace the bash shellscript step (defined in www/WorkflowMaestroTestJob)
with a typed BuildFunction. Behaviorally identical to the bash version:
runs maestro with the full flow_paths set, retries the whole set on
failure, copies the latest attempt's JUnit XML to final_report_path.

Smart retry (only re-running failed flows) is added in a follow-up PR.

Signed-off-by: Ash Wu <[email protected]>

* address comments

Signed-off-by: Ash Wu <[email protected]>

* address comments 2

Signed-off-by: Ash Wu <[email protected]>

---------

Signed-off-by: Ash Wu <[email protected]>
2026-04-30 17:47:35 +08:00
Stanisław Chmiela 15a456970b [build-tools] Scope precompiled modules env to pod install (#3646)
## Summary

- Stops adding precompiled modules environment variables to the whole worker build environment.
- Adds `EXPO_USE_PRECOMPILED_MODULES=1` only to the iOS `pod install` environment when requested and when the installed Expo package is at least `55.0.18`.
- Logs the detected Expo package version, the reason precompiled modules are not enabled, or the exact pod-install environment values that are added.
- Leaves the precompiled modules base URL helper commented out for the later third-party module rollout.

## Validation

- `yarn oxfmt packages/build-tools/src/ios/pod.ts packages/build-tools/src/ios/__tests__/pod.test.ts packages/worker/src/env.ts packages/worker/src/__unit__/env.test.ts`
- `yarn workspace @expo/build-tools jest-unit pod.test.ts`
- `yarn workspace @expo/worker test:unit -- env.test.ts`
- `yarn workspace @expo/build-tools typecheck`
- `yarn workspace @expo/worker typecheck`
2026-04-30 11:31:52 +02:00
Expo CI e74751c26c update CHANGELOG.md 2026-04-30 00:35:41 +00:00
Expo CI 0e2a1756ce v18.9.1 2026-04-30 00:33:23 +00:00
Brent Vatne 766e278aec [eas-cli] Alias --build-id to --id in build:download (#3656) 2026-04-29 17:29:41 -07:00
Expo CI 77e22c740a update CHANGELOG.md 2026-04-30 00:26:16 +00:00
Expo CI 8fdf47aa7d v18.9.0 2026-04-30 00:08:37 +00:00
Douglas Lowder 2279b650d5 [eas-cli] Allow eas build:download to accept a build ID (#3655)
# Why

For certain workflows, it is useful to be able to download build artifacts for a known build ID.

# How

Added a new flag `--build-id` to the `eas build:download` command. When this flag is used,`--fingerprint`, `--dev-client`, and `--platform` are excluded.

Also added the `--all-artifacts`flag. When set, all available artifacts are downloaded. Otherwise, the previous behavior applies (only application archive URL is downloaded).

# Test Plan

- Tested in a real project
- New unit tests added
2026-04-29 16:43:28 -07:00
Gabe Debes c6a36e2e47 [steps] Fix job context interpolation when one string has multiple ${{ }} blocks (#3644) 2026-04-29 15:53:34 -07:00
Szymon Świerk 27cb8c12d6 [eas-cli] Add support for configuring app store connect integration (#3558)
* ASC app link GQL queries

* ASC app link GQL mutations

* ASC utils

* status command

* disconnect command

* select or create ASC API key util function

* connect command

* add oclif connections topic

* gql codegen

* changelog

* regenerate readme

* dedicated app store api key purpose

* prompt user to choose the api key role

* remove unncecessary defensiveness in getting links remoteAppStoreConnectApp

* handle GQL error when invalid connection

* format

* rename connections to integations
2026-04-29 15:14:40 +02:00
Szymon Dziedzic 1122a7f374 [eas-cli] prompt users to stop device run session when they are done by running eas simulator:stop command (#3633) 2026-04-28 20:24:20 +02:00
Stanisław Chmiela c06d18138c [worker] Set precompiled modules base URL (#3642)
## Summary

Sets `EXPO_PRECOMPILED_MODULES_BASE_URL` for iOS worker builds that opt into precompiled modules with `EAS_USE_PRECOMPILED_MODULES=1`.

When the CocoaPods cache is configured, the URL is routed through that cache using the same host-prefix style as other cached downloads. When the cache is not configured, it points directly to `https://storage.googleapis.com/eas-build-precompiled-modules/`.

Local builds are unchanged and do not receive this env var.

## Test Plan

- `yarn oxfmt packages/worker/src/env.ts packages/worker/src/__unit__/env.test.ts`
- `yarn workspace @expo/worker test:unit -- env.test.ts`
- `yarn workspace @expo/worker typecheck`
2026-04-28 19:33:42 +02:00
Szymon Dziedzic 3f4ff1dc3e [eas-cli] add eas simulator:stop command (#3626) 2026-04-27 14:11:13 +02:00
Mustafa Abban e852ab1cb7 [worker] - wrap error log fetch in try-catch to prevent unhandled rejections (#3631)
* [worker] - wrap error log fetch in try-catch to prevent unhandled rejections

* prevent real HTTP requests in integration tests
2026-04-24 12:28:11 -04:00
Mustafa Abban d941398ce6 [build-errors] - log all build errors with enriched metadata (#3612)
* enrich logs

* more metadata

* all error codes

* top level tags
2026-04-24 11:28:15 -04:00
Expo CI bae7684996 update CHANGELOG.md 2026-04-23 16:02:14 +00:00
Expo CI 714aec69c7 v18.8.1 2026-04-23 15:59:34 +00:00
Szymon Dziedzic bec6e670de [eas-cli] add eas simulator:start command (#3625) 2026-04-23 15:55:57 +00:00
Expo CI 900ad765bf update CHANGELOG.md 2026-04-22 17:53:23 +00:00
Expo CI 7c7fad4a13 v18.8.0 2026-04-22 17:50:27 +00:00
Douglas Lowder 7226c72122 [eas-cli] Add customParams support to observe:events (#3620)
# Why

Allow any custom params in Observe events to be retrieved as part of `observe:events` queries, and included in display and JSON output.

# How

Updated the event query and formatter.

# Test Plan

- Tested against projects in production
- Unit tests pass
2026-04-21 11:54:44 -07:00
Ash Wu ae6e0fd7f6 [build-tools] Reuse target_names to avoid duplicate iOS credentials preparation (#3611)
* [build-tools] Reuse target_names to avoid duplicate iOS credentials preparation

Signed-off-by: Ash Wu <[email protected]>

* clean up

Signed-off-by: Ash Wu <[email protected]>

* add test

Signed-off-by: Ash Wu <[email protected]>

* fix import order

Signed-off-by: Ash Wu <[email protected]>

* address comment

Signed-off-by: Ash Wu <[email protected]>

---------

Signed-off-by: Ash Wu <[email protected]>
2026-04-21 22:32:01 +08:00
Kadi Kraman d070f73bc5 [eas-cli] add update and channel insights (#3614)
* Add error handler to prompt for update required

* Add an update:insights command

* Add an --insights flag to update:view

* Refactor shared util

* Improve naming

* Add a create table helper

* Pull date formating to a util

* Improve readability

* Validate insights params

* Add a test to ensure the table renders

* Add channel insights

* Fix lint

* Add PR to changelog
2026-04-21 10:30:29 +01:00
Ash Wu 325bb29b57 [build-tools] Add EAS_FALLBACK_PACKAGE_MANAGER env var support to package manager resolver (#3608)
* [build-tools][eas-build-job] Add workflow fallback package manager support to build-tools resolver

Signed-off-by: Ash Wu <[email protected]>

* address comment

Signed-off-by: Ash Wu <[email protected]>

---------

Signed-off-by: Ash Wu <[email protected]>
2026-04-21 17:27:21 +08:00
Ash Wu 82a8e3c2d0 [build-tools] add parseXcactivitylogFunction (#3578)
* [build-tools] add parseXcactivitylogFunction

Signed-off-by: Ash Wu <[email protected]>

* address comments

Signed-off-by: Ash Wu <[email protected]>

* address comments

Signed-off-by: Ash Wu <[email protected]>

* clean up

Signed-off-by: Ash Wu <[email protected]>

* clean up

Signed-off-by: Ash Wu <[email protected]>

* clean up

Signed-off-by: Ash Wu <[email protected]>

* address comment

Signed-off-by: Ash Wu <[email protected]>

---------

Signed-off-by: Ash Wu <[email protected]>
2026-04-21 17:00:22 +08:00
Gabe DebesandStanisław Chmiela ec310dd115 [build-tools] Introduce eas/deploy and eas/export functions (#3598)
* [build-tools] Add eas/deploy function

* format

* Update CHANGELOG.md

Co-authored-by: Stanisław Chmiela <[email protected]>

* Remove no-source-maps, set if not undefined

* [build-tools] Add eas/export and refine eas/deploy for EAS Hosting workflows

- Add eas/export: expo export via runExpoCliCommand and getExportCommand, structured inputs, export_dir output.
- Refine eas/deploy: build argv with getDeployCommand, parse deploy JSON with parseDeploymentOutput, expanded tests.
- Register eas/export in getEasFunctions; update CHANGELOG for both steps.

* build-tools: prefer easd in dev with probe and logging

Probe easd --help when ENVIRONMENT is development; use easd if it
succeeds, otherwise log and fall back to npx eas-cli@staging. Log
when easd is used. Pass logger from easBuildInternal into resolver
so messages appear in build logs.

* build-tools: warn when using easd in dev; no fallback log

Probe easd --help in development; on success log a warn and use easd.
Fall back to npx eas-cli@staging without an info line. Pass logger from
easBuildInternal and runEasCliCommand so the warn appears in logs.

* Fix parsing

* Improved error message

* self review

* Increase test coverage

* Update packages/build-tools/src/steps/functions/export.ts

Co-authored-by: Stanisław Chmiela <[email protected]>

* Update packages/build-tools/src/steps/functions/deploy.ts

Co-authored-by: Stanisław Chmiela <[email protected]>

* Update packages/build-tools/src/steps/functions/deploy.ts

Co-authored-by: Stanisław Chmiela <[email protected]>

* build-tools: align eas/deploy and eas/export with workflow env and Expo CLI

- deploy: stop passing --environment from job metadata; rely on workflow step env
- deploy: parseDeploymentOutput returns {} on failure; always set output providers
- deploy tests: assert env forwarding; drop --environment expectations
- export: use minify, dump_assetmap, ssg inputs (defaults match expo export)
- export: drop local platform allowlist; invalid values fail in expo export

* test(build-tools): cover deploy_url alias fallback without production

Assert deploy_url uses aliases[0].url when production is absent but aliases exist, so the middle tier of the fallback chain is exercised alongside the url-only partial JSON case.

Use camelCase for the dump_assetmap local in the export step (input id unchanged).

* Update default values and checks

* Remove logger from resolveEasCommandPrefixAndEnvAsync

* Fix CI

* revert uneeded change

* test(build-tools): restore ENVIRONMENT after runEasCliCommand tests

Mirror the resolveEasCommandPrefixAndEnvAsync suite so staging does not leak to later tests.

---------

Co-authored-by: Stanisław Chmiela <[email protected]>
2026-04-20 11:47:15 -07:00
Jakub Grzywacz 84b09c4a28 [eas-cli] Fix adding existing capability identifiers (#3615)
# Why

When an iOS app extension target declares App Group entitlements (e.g. `'com.apple.security.application-groups': ['group.com.easwidgets']`) and the app group already exists in the Apple Developer team but is not linked to the target's bundle identifier, credential setup enabled the App Groups capability on the bundle but never attached the group resulting with capability on but 0 groups linked.
```
Current local entitlements:
{
  "com.apple.security.application-groups": [
    "group.com.easwidgets"
  ]
}
Will enable remote capability: com.apple.security.application-groups (App Groups).
Existing to disable:  [ 'FJXGJ28CQS_IN_APP_PURCHASE' ]
Patch Request: [ { capabilityType: 'APP_GROUPS', option: 'ON' } ]
✔ Synced capabilities: Enabled: App Groups
- Syncing capabilities identifiers
No capability identifiers need to be updated
✔ Synced capability identifiers: No updates
```
This is a regression introduced in https://github.com/expo/eas-cli/pull/3088/
Fixes https://github.com/expo/expo/issues/43677

# How

Fetch the bundle via `BundleId.infoAsync` to determine which IDs are already linked to this bundle.
- If it exists and ID is already linked -> skip.
- If it exists but is not linked -> just link.
- If it doesn't exist -> create it, then link (unchanged behavior).

# Test Plan

After this changes:
```
Current local entitlements:
{
  "com.apple.security.application-groups": [
    "group.com.easwidgets"
  ]
}
Will enable remote capability: com.apple.security.application-groups (App Groups).
Existing to disable:  [ '6C659CPDFA_IN_APP_PURCHASE' ]
Patch Request: [ { capabilityType: 'APP_GROUPS', option: 'ON' } ]
✔ Synced capabilities: Enabled: App Groups
- Syncing capabilities identifiers
Updating bundle identifier with capability identifiers: [
  {
    "capabilityType": "APP_GROUPS",
    "option": "ON",
    "relationships": {
      "appGroups": [
        "NW5UULASH8"
      ]
    }
  }
]
✔ Synced capability identifiers: Linked: group.com.easwidgets
```
2026-04-20 11:05:33 +02:00
Evan BaconandClaude Opus 4.6 d9338799a0 [eas-cli] Fix metadata:push preview video deletion and improve validation (#3603)
- Delete remote previews when their type is removed from store config
- Warn and skip unknown preview types with APP_ prefix suggestion
- Clear all existing previews before upload to avoid Apple limits
- Fix nock mock type error for axios adapter

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-17 15:15:02 -07:00
Mustafa Abban b22d89e41f [build-tools] remove experimental prefix from gradle cache env var (#3613) 2026-04-16 15:34:28 -04:00
Szymon Świerk 81fe0f237f extend the app_store_connect.build_upload interpolation context (#3599) 2026-04-16 15:50:39 +02:00
Douglas Lowder b9ddc04cf7 [eas-cli] Improve observe:metrics handling of app version update IDs (#3609)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->

<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

Currently the output of observe:metrics is unusable if an app version has many updates. Since the metrics are not broken out by update, it makes sense to make two changes:

- Remove update IDs from the display table
- Add the update IDs to the JSON output as an array, in case they are needed as input to other commands, or to AI

Additionally, made a change so that if a project ID is manually passed in, the observe commands do not unnecessarily try to create a new EAS project where none is needed.

# Test Plan

- Unit tests pass
- Tested manually against existing projects with metrics
2026-04-15 12:04:40 -07:00
Expo CI d6a8a4f694 update CHANGELOG.md 2026-04-14 18:51:07 +00:00
Expo CI 7f163e3e52 v18.7.0 2026-04-14 18:48:31 +00:00
Matheus Michels f821cb176c [eas-cli] Add --skip-bundler to eas build:dev (#3604)
* [eas-cli] Add skip-bundler to build:dev

* [eas-cli] Drop build:dev skip-bundler test

* [eas-cli] Tweak skip-bundler wording

* [eas-cli] Update changelog entry for skip-bundler

* [eas-cli] Drop README update for skip-bundler
2026-04-14 19:02:51 +01:00
Gabe Debes 3a70a5d9b8 [build-tools] Early stop eas/start_android_emulator when Android emulator host setup is invalid (#3580)
* [build-tools] early stop with improved error message on start android emulator when not on nested virtualization enabled host

* Add changelog entry

* Move towards cpuinfo approach

* Align var names

* fmt

* [build-tools] Use runtimePlatform for Android emulator virt checks

Align Android emulator nested-virtualization detection with step runtime metadata instead of Node process platform, and expand tests to assert early failure on non-Linux runtime platforms.

* [build-tools] Throw UserError for emulator virt precheck failure

Return a typed user-facing error when Android emulator virtualization requirements are not met so this early-stop path avoids noisy unknown-error stack output.

* [build-tools] move check towards begging of function

* Update error message

* Update CHANGELOG for recent build-tools changes

* Fix blank line on changelog

* tightening upon feedback, move to emulator accel check instead of cpuinfo

* Update CHANGELOG

* format fix

* test: restore startAndroidEmulator helper signature

Keep the startAndroidEmulator test helper aligned with main by removing the temporary runtime platform override from createStep.

* refactor build-tools emulator host support check naming

* format startAndroidEmulator test

* Update err code to align with rename

* Add docsUrl to user error

* Display inline link for now
2026-04-13 12:36:21 -07:00
Gabe Debes 4d74298bb7 [build-tools] add read app config and package.json functions (#3585)
add new eas/read_package_json and eas/read_app_config workflow functions
2026-04-10 14:58:32 -07:00
Expo CI 3155fc26e7 update CHANGELOG.md 2026-04-10 20:55:15 +00:00
Expo CI 0fb6dfc334 v18.6.0 2026-04-10 20:52:25 +00:00
Douglas Lowder dd84646768 [eas-cli] Add observe namespace and commands (#3564)
This is a merge of @Ubax 's changes from #3431 and #3432 , with refactoring to align with the recent OCLIF upgrade and some other adjustments:

- a new command `observe:versions` to query statistics for app versions after the www change in https://github.com/expo/universe/pull/26373
- changes to `observe:metrics` and `observe:events` to use the new app versions, following the www change in https://github.com/expo/universe/pull/26391
- commands are hidden for now

# Why

Provide CLI commands to display Observe metrics and events.

```
USAGE
  $ eas observe:metrics [--platform android|ios] [--metric tti|ttr|cold_launch|warm_launch|bundle_load...] [--stat
    min|median|max|average|p80|p90|p99|eventCount...] [--start <value> | --days <value>] [--end <value> | ]
    [--project-id <value>] [--json] [--non-interactive]

FLAGS
  --days=<value>        Show metrics from the last N days (mutually exclusive with --start/--end)
  --end=<value>         End of time range for metrics data (ISO date).
  --json                Enable JSON output, non-JSON messages will be printed to stderr. Implies --non-interactive.
  --metric=<option>...  Metric name to display (can be specified multiple times).
                        <options: tti|ttr|cold_launch|warm_launch|bundle_load>
  --non-interactive     Run the command in non-interactive mode.
  --platform=<option>   Filter by platform
                        <options: android|ios>
  --project-id=<value>  EAS project ID (defaults to the project ID of the current directory)
  --start=<value>       Start of time range for metrics data (ISO date).
  --stat=<option>...    Statistic to display per metric (can be specified multiple times)
                        <options: min|median|max|average|p80|p90|p99|eventCount>

DESCRIPTION
  display app performance metrics grouped by app version

---------------------------

USAGE
  $ eas observe:events [METRIC] [--sort slowest|fastest|newest|oldest] [--platform android|ios] [--after <value>]
    [--limit <value>] [--start <value> | --days <value>] [--end <value> | ] [--app-version <value>] [--update-id
    <value>] [--project-id <value>] [--json] [--non-interactive]

ARGUMENTS
  [METRIC]  (tti|ttr|cold_launch|warm_launch|bundle_load) Metric to query (e.g. tti, cold_launch). Defaults to tti.

FLAGS
  --after=<value>        Cursor for pagination. Use the endCursor from a previous query to fetch the next page.
  --app-version=<value>  Filter by app version
  --days=<value>         Show events from the last N days (mutually exclusive with --start/--end)
  --end=<value>          End of time range (ISO date)
  --json                 Enable JSON output, non-JSON messages will be printed to stderr. Implies --non-interactive.
  --limit=<value>        The number of items to fetch each query. Defaults to 10 and is capped at 100.
  --non-interactive      Run the command in non-interactive mode.
  --platform=<option>    Filter by platform
                         <options: android|ios>
  --project-id=<value>   EAS project ID (defaults to the project ID of the current directory)
  --sort=<option>        [default: oldest] Sort order for events
                         <options: slowest|fastest|newest|oldest>
  --start=<value>        Start of time range (ISO date)
  --update-id=<value>    Filter by EAS update ID

DESCRIPTION
  display individual app performance events ordered by metric value

DESCRIPTION
  display individual app performance events ordered by metric value

--------------------------------------

USAGE
  $ eas observe:versions [--platform android|ios] [--start <value> | --days <value>] [--end <value> | ] [--project-id
    <value>] [--json] [--non-interactive]

FLAGS
  --days=<value>        Show versions from the last N days (mutually exclusive with --start/--end)
  --end=<value>         End of time range (ISO date)
  --json                Enable JSON output, non-JSON messages will be printed to stderr. Implies --non-interactive.
  --non-interactive     Run the command in non-interactive mode.
  --platform=<option>   Filter by platform
                        <options: android|ios>
  --project-id=<value>  EAS project ID (defaults to the project ID of the current directory)
  --start=<value>       Start of time range (ISO date)

DESCRIPTION
  display app versions with build and update details
```

# How

New commands and GraphQL queries.

# Test Plan

New unit tests added in `fetchEvents-test.ts`
2026-04-10 13:41:04 -07:00
Phil Pluckthun c3fbf63a71 fix(eas-cli): Abort early on asset upload errors (#3600) 2026-04-10 19:28:36 +01:00
Szymon Dziedzic 7c0d7b60c7 [ENG-17052][build-tools] Add working_directory input to eas/build function group (#3582) 2026-04-10 11:58:56 +00:00
Szymon Dziedzic 5a8e1b4ba3 [ENG-14822][steps] Coerce env in steps to string (#3583) 2026-04-10 12:18:21 +02:00
Szymon Świerk 12b648a603 [eas-build-job] Add missing events support to app_store_connect workflows interpolation context (#3587)
* add missing events support to app store connect interpolation context

* tests
2026-04-10 10:35:03 +02:00
Evan BaconandClaude Opus 4.6 8c170af829 feat(metadata): add app clips support (#3590)
* Add App Clip metadata support

Introduce App Clip support across metadata pipeline: add JSON schema definitions for AppleAppClip and related types; extend Apple types with AppClip interfaces and action enum; add reader accessors (getAppClip*, getAppClipLocalizedInfo) and writer mutators (setAppClipDefaultExperience, setAppClipReviewDetail, setAppClipLocalizedInfo). Implement AppClipTask to prepare/download/upload App Clip default experience, localizations, header images and review invocation URLs, including helper functions for syncing and downloading header images. Wire the task into the tasks index, include AppClipData in AppleData, and add unit tests for reader, writer, and task behavior.

* Preserve placeholder assets and app clip fallback

Fix race/regression issues when App Store Connect records are not yet rendered: - ScreenshotsTask and PreviewsTask now preserve placeholder local paths when imageAsset/videoUrl is null (AWAITING_UPLOAD) so pulled config doesn't drop entries and users can recover by adding files or removing entries. - PreviewsTask/ScreenshotsTask use the downloaded path when available, otherwise build a deterministic store path (with filename or index-based fallback). - AppClipTask.downloadAsync now falls back to the template experience when the current version has no default experience.

Also adds/updates unit tests for app clip behaviors (fallback to template, preserving header image path, skipping no-op PATCHes and review detail updates, and aborting when no editable version), screenshots (placeholder paths and index-based filename fallback), and previews (preserving placeholder entries when videoUrl is null). These changes address intermittent failures and improve round-trip push/pull resilience.

* [eas-cli] Bump @expo/apple-utils to 2.1.19

Brings in the fix for image and video uploads via metadata:push. The
asset client was inheriting Bearer token injection from the App Store
Connect API client, which caused S3 presigned URL uploads to be silently
mishandled by Apple's CDN — leaving screenshots, previews, and App Clip
header images stuck in AWAITING_UPLOAD state.

Also adds new App Clip models (AppClipDefaultExperience,
AppClipDefaultExperienceLocalization, AppClipHeaderImage,
AppClipAppStoreReviewDetail) used by the new App Clip metadata support.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-09 12:31:22 -07:00
Byron Karlen ad4c5299e3 [auth] logout changes (#3555) 2026-04-09 08:44:14 -07:00
Evan BaconandClaude Opus 4.6 dee07ae0a2 [eas-cli] Bump @expo/apple-utils to 2.1.18 (#3588)
* Sync Apple metadata schema, docs, and flags

Add new Apple advisory fields to metadata-0.json (advertising, ageAssurance, ageRatingOverrideV2, developerAgeRatingInfoUrl, gunsOrOtherWeapons, healthOrWellnessTopics, messagingAndChat, parentalControls, userGeneratedContent) and an AppleAgeRatingOverrideV2 enum. Introduce CLAUDE.md documenting how to keep schema and reader/writer in sync. Replace manual 'non-interactive' flags in metadata push/pull with the shared EASNonInteractiveFlag. Add a unit test that validates AppleConfigWriter output against the JSON schema (imports validateConfig) as a drift guard to surface mismatches between writer output and the schema early. These changes prevent confusing validation errors when pulling/pushing metadata by keeping schema, docs, CLI flags, and tests aligned.

* [eas-cli] Bump @expo/apple-utils to 2.1.18

Fixes `eas metadata:push` failing in the age-rating phase with
"'gamblingAndContests' is not an attribute on the resource
'ageRatingDeclarations'". The published 2.1.17 bundle still emitted
that field at runtime even though its `.d.ts` typed it as `never`,
because @expo/apple-utils' prepare script regenerated types from source
but bundled @expo/app-store from a stale `build/` directory.
2.1.18 rebuilds @expo/app-store as part of the prepare flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* Fix changelog PR link

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-09 08:34:49 -07:00
Evan BaconandClaude Opus 4.6 4ad12340f2 Add new Apple metadata attributes (#3584)
* Add new Apple metadata attributes

Extend AppleConfigReader and AppleConfigWriter to include additional app rating/content fields (advertising, ageAssurance, ageRatingOverrideV2, developerAgeRatingInfoUrl, gunsOrOtherWeapons, healthOrWellnessTopics, messagingAndChat, parentalControls, userGeneratedContent) with sensible defaults (false/null or Rating.NONE). This ensures these attributes are preserved when reading and writing Apple metadata.

* update package

* Add missing fields to test fixtures and CHANGELOG entry

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-09 07:45:54 -07:00
c6d4faefe7 [build-tools][eas-cli] Support iOS Development provisioning profiles (#3496)
* [build-tools] Detect iOS Development provisioning profiles and set correct code signing identity

Add DEVELOPMENT distribution type to distinguish Development profiles
from Ad Hoc. Development profiles have get-task-allow=true in their
entitlements; Ad Hoc profiles don't.

When a Development profile is detected, override CODE_SIGN_IDENTITY to
"Apple Development" instead of the "iPhone Distribution" that
@expo/config-plugins hardcodes. This fixes builds that use
credentialsSource: "local" with Development profiles (e.g., for
Family Controls entitlement).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* [eas-cli] Accept Development provisioning profiles for internal distribution

Fix isAdHocProfile() to distinguish Development from Ad Hoc profiles by
checking the get-task-allow entitlement. Add isDevelopmentProfile() for
explicit detection.

Update assertProvisioningProfileType() to accept Development profiles
for internal distribution, and reject them for store distribution.

This enables credentialsSource: "local" with Development profiles needed
for entitlements like Family Controls (com.apple.developer.family-controls)
that only work with Development provisioning profiles.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* [build-tools] Use certificateCommonName for development code signing identity

Address review feedback: use the certificate common name from the
provisioning profile instead of hardcoding "Apple Development", which
doesn't cover all certificate types (e.g. "iPhone Developer", "iOS
Development"). Also fix grammar in error message ("an development" →
"a development").

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* [build-tools] Add CHANGELOG entry for development provisioning profile fix

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* [build-tools] Use upstream codeSignIdentity parameter instead of workaround

Now that @expo/[email protected] supports the optional
codeSignIdentity parameter in setProvisioningProfileForPbxproj,
pass profile.certificateCommonName directly for development profiles
and remove the overrideCodeSignIdentityForDevelopment workaround
that was reading/writing the pbxproj a second time.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix changelog

Temporary Commit at 4/2/2026, 1:09:37 PM

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Quinlan Jung <[email protected]>
2026-04-02 13:25:13 -07:00
Phil Pluckthun 2f8712a0f3 [build-tools] Differentiate between Yarn Classic/Modern based on lockfile contents (#3572)
* Detect Yarn legacy/classic based on lockfile contents

* Add CHANGELOG entry

* Fix typo

* Add tests
2026-04-02 21:11:11 +01:00
Expo CI a439721054 update CHANGELOG.md 2026-04-02 10:44:22 +00:00
Expo CI 2e2dddb59a v18.5.0 2026-04-02 10:41:22 +00:00
Szymon ŚwierkandStanisław Chmiela b7df3ff4f8 [eas-build-job] Add app_store_connect.build_upload workflow interpolation context (#3566)
* Add app_store_connect.build_upload workflow interpolation context

* limit the possible states

* make the id type uuid

* Update packages/eas-build-job/src/common.ts

Co-authored-by: Stanisław Chmiela <[email protected]>

* Update packages/eas-build-job/src/common.ts

Co-authored-by: Stanisław Chmiela <[email protected]>

* remove no longer valid test

---------

Co-authored-by: Stanisław Chmiela <[email protected]>
2026-04-02 12:06:44 +02:00
Stanisław Chmiela 8a74758d56 [build-tools] Remove reuse_devices: false dead code (#3560)
## Summary
- remove the internal `eas/__maestro_test` function registration and implementation
- delete the Maestro flow/config discovery helper used by that internal path
- remove the related tests and fixture files for Maestro config parsing and flow resolution

## Testing
- `yarn --cwd packages/build-tools typecheck`
- `yarn --cwd packages/build-tools jest-unit --runInBand maestroResultParser reportMaestroTestResults easFunctions`
2026-04-02 10:38:59 +02:00
Szymon Świerk 26fdf8fb75 [eas-build-job] Add app_store_connect event context to WorkflowInterpolationContext (#3530)
* [eas-build-job] Add app_store_connect event context to WorkflowInterpolationContext

* app_store_connect.app, not app_store_connect.event.app
2026-04-01 11:10:48 +02:00
Stanisław Chmiela 9328063547 [build-tools] Remove precompiled modules preparation flow (#3565)
## Summary
- remove the worker and build-tools plumbing for precompiled dependency downloads and extraction
- stop waiting on precompiled dependency preparation before `pod install`
- delete the dedicated precompiled dependency helpers, fixtures, and tests
- keep the `EAS_USE_PRECOMPILED_MODULES=1` passthrough so iOS jobs still set `EXPO_USE_PRECOMPILED_MODULES=1`

## Testing

CI should pass.
2026-04-01 09:44:39 +02:00
Stanisław Chmiela fc66b8251e [eas-cli] Fix hasIgnoredIosProjectAsync (#3562)
## Why

`hasIgnoredIosProjectAsync()` currently always returns `false` because its `finally` block unconditionally returns `false`, overriding the value from the `try` block.

That means callers never see a positive ignored result even when the Xcode project is actually ignored.

## How

- replace the unconditional `finally` return with a `catch` fallback
- add a unit test that covers the ignored iOS project case

## Test Plan

CI should pass. Verified the test would fail if not for this change.
2026-04-01 09:25:07 +02:00
Stanisław Chmiela 394576368a [worker] Lower project dependencies telemetry errors to warning (#3535)
## Summary
- mark `FAILED_TO_REPORT_PROJECT_DEPENDENCIES_EVENT` as a Sentry warning instead of an error
- reduce noise from non-critical analytics failures in worker project dependency reporting
2026-03-31 10:30:17 +02:00
Evan BaconandClaude Opus 4.6 f80e34cffe feat(metadata): add screenshots and preview support (#3301)
* Add support for Apple screenshots and video previews

Introduces new tasks for managing App Store screenshots and video previews, including downloading and uploading assets per locale and display type. Updates AppleData and PartialAppleData types to include screenshots and previews, and extends config reader/writer to handle these assets. Also bumps @expo/apple-utils to 2.1.13 and updates related tests and types.

* Update apple-utils and improve asset file handling

Bump @expo/apple-utils to 2.1.14 and add a version check for preview support. Normalize preview and screenshot asset directory structure and filenames for consistency and future compatibility.

* Use original type names for preview and screenshot dirs

Updated directory structure for storing Apple previews and screenshots to use the original previewType and displayType values instead of lowercased, hyphenated versions. This change ensures consistency with the original type naming.

* Improve Apple metadata tasks error handling and schema

Replaces assert-based error handling with conditional checks and log messages in Apple metadata tasks (age-rating, app-review-detail, previews, screenshots) to prevent crashes when context is missing. Updates the metadata schema to add support for 'screenshots' and 'previews' fields, including detailed structure for video previews.

* Use original filenames for Apple previews and screenshots

Updated the download logic to use the original filenames from Apple metadata for previews and screenshots instead of normalized index-based names. This change improves file matching during sync operations.

* Update tests to skip on missing context instead of abort

Changed age rating and app review detail task tests to expect skipping (not throwing) when required context is missing, reflecting updated behavior. Also updated CHANGELOG to note new screenshots and previews support for metadata commands.

* wip -- unit tests for metadata

* Include .query(true) in apple metadata tests

Allow nock mocks to match requests with query parameters in two tests. Added `.query(true)` to the GET interceptors in packages/eas-cli/src/metadata/apple/tasks/__tests__/previews.test.ts and packages/eas-cli/src/metadata/apple/tasks/__tests__/screenshots.test.ts so the mocks won't fail when requests include query strings.

* [eas-cli] Fix age rating API and update @expo/apple-utils to 2.1.16

Move age rating from appStoreVersions to appInfos — Apple removed the
ageRatingDeclaration relationship from appStoreVersions. Also apply
formatting fixes from oxfmt.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* Update CHANGELOG.md

* [eas-cli] Improve screenshots/previews tasks: native fs, fileSize matching, parallel fetching

- Replace fs-extra with native fs/fs.promises
- Add fileSize check alongside filename matching to detect changed files
- Parallelize locale fetching in prepareAsync with Promise.all
- Skip reorder API call when screenshot order is unchanged
- Remove unnecessary runtime method check for getAppPreviewSetsAsync
- Use consistent index-based fallback filenames for previews

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-30 17:46:55 -07:00
Quinlan Jung 🦥 59ad2c2ca0 Reapply "[eas-cli][build-tools][worker] bump @expo/config and @expo/config-plugins (#3527)" (#3534) (#3543)
This reverts commit 92ad6b5ba3.
2026-03-30 14:04:44 -07:00
Evan BaconandClaude Opus 4.6 4e95a29797 Support non-interactive metadata commands with ASC API key auth (#3548)
* Support non-interactive metadata commands

Add a --non-interactive flag to metadata pull/push and thread nonInteractive through context and helper calls. Propagate the flag to ensureProjectConfiguredAsync, getProfilesAsync, getAppStoreAuthAsync, downloadMetadataAsync and uploadMetadataAsync so prompts are skipped or fail fast when non-interactive.

Improve App Store Connect auth resolution: try ASC API key from submit profile, then EAS credentials service (via GraphQL lookup), then environment variables; fall back to interactive cookie auth only when allowed. In non-interactive mode, throw a clear error if no ASC API key is available. Also update upload/download to auto-overwrite in non-interactive mode and make config validation in upload fail instead of prompting.

Minor: import/fs usage and GraphQL client types added, small logging improvements and error messages to aid non-interactive workflows.

* Add metadata tests and enhance App Store auth

Add unit tests for metadata auth, download, and upload flows and update App Store auth resolution to include team metadata.

- New tests: src/metadata/__tests__/auth.test.ts, download.test.ts, upload.test.ts covering API key resolution, interactive vs non-interactive behavior, telemetry, and validation prompts.
- auth.ts: refactor tryResolveAscApiKeyAsync to return ascApiKey plus optional teamId/teamName, read profile-provided keyP8 and prefer profile keys over stored keys, and include teamId/teamName/teamType (defaulting to COMPANY_OR_ORGANIZATION) when calling ensureAuthenticatedAsync.

These changes improve handling of ASC API keys and team info during authentication and add coverage for metadata CLI flows and edge cases.

* Use fs.promises.readFile instead of fs-extra

Replace import of fs-extra with Node's built-in fs and call fs.promises.readFile when loading the ASC API key. Update tests to mock fs.promises.readFile accordingly. This removes the dependency on fs-extra for this code path and aligns the readFile usage with the fs.promises API.

* [eas-cli] Add CHANGELOG entry for metadata non-interactive support

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-30 11:09:41 -07:00
Stanisław Chmiela 66d8e3d6b8 Add test precompiled iOS dependency preparation support (#3556)
## Summary
- export precompiled modules helpers from `@expo/build-tools` and start preparation when flagged builds enable precompiled modules
- wait for precompiled module preparation before running `pod install` in both build-tools pod flows
- set worker env and config for precompiled module downloads, including proxy-aware download handling and coverage for the new behavior

## Testing
<img width="812" height="325" alt="Zrzut ekranu 2026-03-28 o 22 26 35" src="https://github.com/user-attachments/assets/9e4deb10-d233-4bb6-ac5d-48cef31c17ed" />

<img width="1032" height="548" alt="Zrzut ekranu 2026-03-28 o 22 27 27" src="https://github.com/user-attachments/assets/a6bbcb75-4845-4dce-9f31-5097ece800ae" />
2026-03-30 19:37:41 +02:00
Stanisław Chmiela 5e4a88094f [build-tools] Disable Android emulator window transitions (#3557)
## Summary
- disable Android emulator window and transition animation scales once the emulator is booted and ready
- apply the setting for the initial emulator and any cloned emulator instances used by the start step
- add unit coverage for the new adb settings commands and the startup orchestration

## Why
Maestro test runs already avoid the emulator boot animation, but Android window and transition animations still run after boot. Those system animations add avoidable UI transition latency and can introduce test flake without affecting in-app animator timing.

See https://bsky.app/profile/rdperottoni.bsky.social/post/3mfnhnt77mk2f.

## Validation
- `yarn fmt packages/build-tools/src/utils/AndroidEmulatorUtils.ts packages/build-tools/src/steps/functions/startAndroidEmulator.ts packages/build-tools/src/steps/functions/__tests__/startAndroidEmulator.test.ts packages/build-tools/src/utils/__tests__/AndroidEmulatorUtils.test.ts`
- `yarn --cwd packages/build-tools jest-unit -- runInBand src/steps/functions/__tests__/startAndroidEmulator.test.ts src/utils/__tests__/AndroidEmulatorUtils.test.ts`
2026-03-30 19:04:46 +02:00
Kudo Chien 7a4c81f4e3 [build-tools] resolve repack version (#3549) 2026-03-28 01:48:50 +08:00
Mustafa Abban c9cdfd1eb5 [build-tools] support Gradle build cache for Android builds (#3510)
* gradle cache

* cache modules-2

* cache modules-2

* proper path

* set properties, rename

* rem modules-2

* set org.gradle.cache.cleanup to ALWAYS

* set org.gradle.cache.cleanup to ALWAYS

* consolidate caches in restore/save step

* restore gradle

* cleanup nullthrows

* always set properties if env set, cleanup

* consistent path, clean log

* modify gradle props in trycatch

* eas-cache-cleanup script

* shorter prune
2026-03-26 12:34:18 -05:00
Quinlan Jung 🦥 8115dc8766 [eas-cli] bump apple-utils (#3545)
* [eas-cli] bump apple-utils

* Temporary Commit at 3/25/2026, 6:42:37 PM

* Temporary Commit at 3/25/2026, 7:18:01 PM
2026-03-25 19:27:14 -07:00
Byron Karlen 113467c91d Reapply "Use auth code flow (#3398)" (#3422) (#3542)
This reverts commit 0fdf2dd44a.
2026-03-25 12:32:58 -07:00
Phil Pluckthun be967f4b9a [build-tools] Read app config with expo config CLI invocation before falling back to @expo/config (#3536)
* Add `expo config` app config reading to build-tools

* Align with other CLI invocations

* Switch to turtle-spawn for consistency

* Update `appConfig` reading for async calls

* Add CHANGELOG entry

* Update worker

* Fix typo

* Add unit tests
2026-03-25 15:13:48 +00:00
Stanisław Chmiela 660303b0c3 [steps] Use counter-based generated step ids (#3541)
## Summary
- replace UUID-based autogenerated step ids with a counter in `BuildStep.getNewId()`
- keep explicit user-defined ids unchanged
- rename the step-id test helper so tests no longer talk about UUIDs

## Testing
- yarn --cwd packages/steps test --runInBand BuildStep-test.ts BuildFunction-test.ts BuildConfigParser-test.ts StepsConfigParser-test.ts AbstractConfigParser-test.ts
- yarn --cwd packages/steps typecheck
2026-03-25 15:02:30 +01:00
Stanisław Chmiela 5514289648 [steps][local-build-plugin] Remove buildStepInternalId (#3540)
## Summary
- remove `buildStepInternalId` emission from `BuildStep`
- stop generating the extra UUID entirely
- drop the now-unused field from local build log sanitization

Just want to make logs smaller.

## Testing
- yarn --cwd packages/steps test --runInBand BuildStep-test.ts
- yarn --cwd packages/steps typecheck
2026-03-25 13:36:12 +00:00
Stanisław Chmiela 7924e33e96 [eas-cli] Key workflow logs by buildStepId (#3539)
## Summary
- key workflow log groups by `buildStepId` instead of display text (this might cause two steps named the same to appear as one)
- treat `buildStepInternalId` as nonexistent in the CLI parser (want to remove it)
- keep display names for prompts and status output while preserving stable selection keys

## Testing
- yarn --cwd packages/eas-cli test src/__tests__/commands/workflow-logs-test.ts src/commandUtils/workflow/__tests__/utils-test.ts
- yarn --cwd packages/eas-cli typecheck
2026-03-25 14:31:11 +01:00
Stanisław Chmiela e699a06c07 [steps] Simplify name/displayName handling (#3538)
## Summary
- remove `BuildStep.getDisplayName` and inline display-name selection at the shell/function callsites
- simplify shell step naming to `name ?? first non-empty command line ?? command`
- update step tests to use explicit display names instead of the removed helper

## Verification
- yarn --cwd packages/steps test --runInBand BuildStep-test.ts BuildFunction-test.ts BuildWorkflowValidator-test.ts BuildStepOutput-test.ts BuildStepInput-test.ts
- yarn --cwd packages/steps typecheck
2026-03-25 13:42:41 +01:00
Stanisław Chmiela ce819c1a14 [eas-cli] Add optional Sentry error reporting (#3537)
## Summary
- add a dedicated `packages/eas-cli/src/sentry.ts` module that initializes `@sentry/node` for eas-cli
- capture command failures from `EasCommand`, including `EasCommandError`, and flush Sentry on exit
- propagate `EAS_CLI_SENTRY_DSN` into the worker child-process environment so worker-run eas-cli commands can report errors too

https://exponent-internal.slack.com/archives/C06EFBQK3B7/p1773432161848589?thread_ts=1773417343.042269&cid=C06EFBQK3B7

## Validation
- `yarn test src/commandUtils/__tests__/EasCommand-test.ts --runInBand`
- `yarn test:unit --runInBand src/__unit__/env.test.ts`
- `yarn build` in `packages/eas-cli`
- `node ./bin/run --version`
- `node ./bin/run --help`
- `node ./bin/run analytics`
- `EAS_CLI_SENTRY_DSN=https://[email protected]/1 node ./bin/run analytics`

## Manual Sentry Verification
- ran the compiled CLI against the test Sentry project `expo-stanley-test/test-local` with a real DSN
- forced an `EasCommandError` from `eas analytics` and confirmed the CLI failed locally as expected
- verified that Sentry ingested the event and created issue `TEST-LOCAL-1`
- confirmed the event in Sentry at `https://expo-stanley-test.sentry.io/issues/TEST-LOCAL-1`
2026-03-25 13:37:09 +01:00
Quinlan Jung 🦥 92ad6b5ba3 Revert "[eas-cli][build-tools][worker] bump @expo/config and @expo/config-plugins (#3527)" (#3534)
This reverts commit d63d51f548.
2026-03-24 17:44:58 -07:00
Quinlan Jung 🦥 d63d51f548 [eas-cli][build-tools][worker] bump @expo/config and @expo/config-plugins (#3527)
* [eas-cli][build-tools][worker] bump @expo/config and @expo/config-plugins

* Temporary Commit at 3/23/2026, 5:55:24 PM

* Temporary Commit at 3/23/2026, 6:10:13 PM
2026-03-24 11:56:19 -07:00
Stanisław Chmiela bfb12f0317 [repo] Standardize clean scripts on rimraf (#3520)
## Summary
- standardize workspace clean/rebuild/prepack deletion steps on `rimraf`
- add `rimraf` where package-local scripts invoke it directly
- keep `worker` and root clean flows working from a clean install

Also adds Codex config for new workspace.

And the problem it solves is that sometimes `yarn clean` would fail if there were no `*.tsbuildinfo` files. Something weird about zsh in package.json

## Validation
- mise exec -- yarn clean
- mise exec -- yarn install --immutable
- mise exec -- yarn build
- mise exec -- yarn typecheck
- mise exec -- yarn clean (in packages/worker)
- mise exec -- yarn clean (in packages/logger)
2026-03-23 17:33:02 +01:00
Douglas LowderandClaude Opus 4.6 b162162007 [eas-cli] Fix workflow:logs for builds using built-in EAS build steps (#3523)
# Why

Logs from EAS build jobs using the built-in flow are in a different form than those from build jobs using custom YAML. Because of this, `eas workflow:logs` and `eas workflow:status` were not working correctly for workflow runs with these jobs.

# How

Analyze the differences in format, and modify the utils that parse logs to handle both formats.

# Test Plan

- Tested with recent workflow runs from expo/expo
- Unit tests updated

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-20 08:11:33 +01:00
Stanisław Chmiela 6ed7e14a69 [eas-cli] Update code for the new PartnerActor (#3521)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

After new union type addition GQL fails TSC in `eas-cli`.

# How

Added fetching of `username` for `PartnerActor` and used it where we would previously use `username` of a `User`.

Removed `getUsernameForBuildMetadataAndBuildJob` in favor of `getActorUsername`, I don't think these will every be different?

Removed 

# Test Plan

CI should pass.
2026-03-19 15:31:43 +01:00
Kudo Chien 37910b17e1 [build-tools] add js_bundle_only to repack function (#3517)
# Why

close ENG-17737

# How

adding `js_bundle_only` support to repack
2026-03-19 20:01:06 +08:00
Stanisław Chmiela 0d3c56f4d6 [build-tools] Improve ASC app ID error guidance (#3512)
## Summary
- surface example visible App Store Connect apps for invalid bundle-id upload failures
- reuse shared ASC app summary formatting for not-found and invalid-bundle-id errors
- add unit coverage for visible app summaries and message contents

Should help debug https://discord.com/channels/695411232856997968/1483454279204343910/1483492265312780340

## Testing
- ./node_modules/.bin/jest --config packages/build-tools/jest/unit-config.ts packages/build-tools/src/steps/utils/ios/__tests__/AscApiUtils.test.ts packages/build-tools/src/steps/functions/__tests__/uploadToAsc.test.ts
- ./node_modules/.bin/tsc -p packages/build-tools/tsconfig.json --noEmit
2026-03-17 16:59:49 +01:00
Stanisław Chmiela bc8e60d385 [build-tools] Handle ASC SDK version issue (90725) (#3464)
## Why
UNKNOWN submit failures include App Store Connect SDK compatibility error `90725`.

This should be surfaced as actionable EAS guidance (rebuild with newer iOS SDK/Xcode image) instead of generic unknown.

## How
- add `90725` detection in `upload_to_asc` failed-state handling
- throw `EAS_UPLOAD_TO_ASC_SDK_VERSION_ISSUE` with EAS-specific remediation
- include docs URL to iOS build image/Xcode configuration docs
- add unit tests for `isSdkVersionIssueError`

## Examples
- https://expo.dev/uuid/019cbbe7-9d71-7c90-80b5-a98870478265
- https://expo.dev/uuid/019cbf52-3cbd-7491-b35e-c3fc2f24afc9
- https://expo.dev/uuid/019cbf4f-41d9-7ab7-8e85-e53596526a82

## Test Plan
- `yarn --cwd packages/build-tools jest-unit src/steps/functions/__tests__/uploadToAsc.test.ts`
- `yarn --cwd packages/build-tools typecheck`
- `yarn fmt`
2026-03-17 16:35:00 +01:00
Expo CI 99b8d67d3f update CHANGELOG.md 2026-03-16 20:24:39 +00:00
Expo CI 4e202db843 v18.4.0 2026-03-16 20:21:54 +00:00
Stanisław Chmiela ab209463ab [worker] Prevent forwarding HTTP log failures to GCS logstream (#3507)
Makes sure we don't log "Failed to upload batch of logs" to user-facing log as we did [here](https://staging.expo.dev/accounts/expo-services/projects/worker-system-tests/workflows/019cf7b3-19d6-7b57-bb6f-41d5c63491e3).
2026-03-16 19:02:25 +01:00
Stanisław Chmiela 99e9d367a1 [worker] Add ITSAppUsesNonExemptEncryption flag to worker app config (#3506)
Summary
- add the missing `ITSAppUsesNonExemptEncryption` boolean to `packages/worker/app.json` so the worker app can declare non-exempt encryption usage and avoid iOS config errors

Should fix system tests!
2026-03-16 18:23:50 +01:00
Stanisław Chmiela a6f68462c9 Enable staging HTTP log streaming (#3505)
## Summary
- enable worker HTTP log streaming for staging using https://staging-logs.expo.dev/logs/


This is not tied to anything in UI yet. Just want to see the logs flowing.
2026-03-16 16:41:07 +01:00
Szymon ŚwierkandStanisław Chmiela 1c58823991 [eas cli] Builds: wait before showing the concurrency limit message (#3493)
* [eas cli] builds: wait before showing the concurrency limit message

* Update packages/eas-cli/src/build/build.ts

Co-authored-by: Stanisław Chmiela <[email protected]>

* apply style/naming CR suggestions

* remove the unnecessarily defensive clean-ups

---------

Co-authored-by: Stanisław Chmiela <[email protected]>
2026-03-16 15:11:46 +01:00
Ash Wu 6a804ab8d7 [build-tools] Store per-attempt results for Maestro test retries (#3498)
Signed-off-by: Ash Wu <[email protected]>
2026-03-16 21:01:42 +08:00
Stanisław Chmiela 74eff9bf67 [worker] Stop mutating errors in Sentry reporting (#3502)
## Summary
- stop mutating the original error instance in worker Sentry reporting
- attach the worker message to Sentry as an extra instead of rewriting the exception message — matches `universe/server-logging`
- keep the launcher-facing error payload unchanged
- fixes integration tests

## Testing
- yarn test:integration:watch stateSync
- yarn test:unit sentry
2026-03-13 12:42:54 +01:00
Stanisław Chmiela d20c465ddf [eas-build-job] Introduce SystemError and replace BuildError (#3471)
## Summary
- introduce `SystemError` as the explicit system-failure type
- replace in-repo `BuildError` usage with `ExpoError`, `UserError`, and `SystemError`
- keep unknown errors on the user-error side

## Changes
- remove the old `BuildError` class from the in-repo error flow
- make `SystemError` always publish `SERVER_ERROR` while keeping `trackingCode` for internal use
- update worker handling, analytics, and state sync to use the shared `ExpoError` contract
- avoid mutating the user-facing error object during Sentry reporting

## Testing
- `packages/eas-build-job`: `npm exec -- corepack yarn build && npm exec -- corepack yarn test src/__tests__/errors.test.ts --runInBand`
- `packages/build-tools`: `npm exec -- corepack yarn jest-unit src/buildErrors/__tests__/detectError.test.ts --runInBand`
- `packages/worker`: `npm exec -- corepack yarn test:integration src/__integration__/stateSync.test.ts`
2026-03-13 09:20:09 +01:00
Evan Bacon 0c17846e67 Auto-detect non-interactive mode in CI / non-TTY environments (#3486)
# Why

`eas build -p android` prompts for keystore generation and fails in CI or non-TTY environments:

```
Input is required, but stdin is not readable. Failed to display prompt: Generate a new Android Keystore?
```

Passing `--non-interactive` works around this, but it should be auto-detected when `CI=1` or stdin is not a TTY.

# How

Set a `default` function on both `--non-interactive` flag definitions (`EasNonInteractiveAndJsonFlags` and `EASNonInteractiveFlag`) in `packages/eas-cli/src/commandUtils/flags.ts`. The default checks `CI` env var (via `boolish` from `getenv`) and `process.stdin.isTTY`.

- oclif evaluates `default` only when the user does NOT pass `--non-interactive` on CLI
- `noCacheDefault: true` prevents baking the developer's environment into `oclif.manifest.json`
- All ~60 commands using these flag definitions automatically get CI/TTY auto-detection with no other changes needed

# Test Plan

- Added 5 unit tests for `isNonInteractiveByDefault()` covering: TTY without CI (false), CI=1 (true), CI=true (true), non-TTY stdin (true), both CI and non-TTY (true)
- All existing `resolveNonInteractiveAndJsonFlags` tests pass unchanged
- `yarn typecheck` passes
- `yarn lint` passes (0 errors)

```
PASS src/commandUtils/__tests__/flags-test.ts
  isNonInteractiveByDefault
    ✓ returns false when stdin is a TTY and CI is not set
    ✓ returns true when CI=1
    ✓ returns true when CI=true
    ✓ returns true when stdin is not a TTY
    ✓ returns true when both CI is set and stdin is not a TTY
  resolveNonInteractiveAndJsonFlags
    ✓ returns both false when no flags are set
    ✓ --json implies --non-interactive
    ✓ --non-interactive alone does not enable json
    ✓ both flags explicitly set
    ✓ both flags explicitly false
    ✓ --json false with --non-interactive true

Test Suites: 1 passed, 1 total
Tests:       11 passed, 11 total
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-03-12 12:29:04 -07:00
Douglas Lowder d5a3fc06f9 Enable manual testing of changelog GitHub workflow (#3500)
# Why

There have been failures recently with the GitHub CHANGELOG check. Adjusting the workflow so that it can be manually tested.

# How

Add `workflow_dispatch:`

# Test Plan

CI should pass
2026-03-12 10:57:58 -07:00
Matheus MichelsandDouglas Lowder c51ac27e25 [eas-cli] add skip-build-if-not-found option to eas build:dev (#3495)
* add skip-build-if-not-found to build:dev

* add changelog entry for build:dev skip flag

* Remove duplicate line in CHANGELOG

* Adjust CHANGELOG

* Bump add-pr-comment workflow dep

---------

Co-authored-by: Douglas Lowder <[email protected]>
2026-03-11 17:57:44 -07:00
Jacob KaplanandClaude Opus 4.6 88506d01a6 fix(build-tools): check all certificates in provisioning profile during verification (#3484)
* [build-tools] Check all certificates in provisioning profile during verification

Apple provisioning profiles can contain multiple developer certificates
(e.g., when a team has multiple developers). The previous code only
checked DeveloperCertificates[0], causing false "profile and
distribution certificate don't match" errors when the signing
certificate was at a different index.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* [build-tools] Add CHANGELOG entry for multi-cert provisioning profile fix

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-11 15:06:29 -07:00
Stanisław Chmiela 77e36e0743 [build-tools] Enforce C.utf8 locale for Gradle invocations (#3494)
# Why

Some environments that run Gradle do not have `en_US.UTF-8` available, which can lead to locale warnings and Gradle failures when Java falls back to a non-UTF-8 default charset.

In the affected environment, `locale -a` returned only:

```sh
C
C.utf8
POSIX
```

Setting `LC_ALL=C.UTF-8` fixed the Gradle issue. This matches the behavior described in [gradle/gradle#23391](https://github.com/gradle/gradle/issues/23391#issuecomment-1878979127), where Gradle/archive handling fails under a non-UTF-8 locale.

# How

Inline `LC_ALL: 'C.UTF-8'` at the two Android Gradle subprocess call sites in `@expo/build-tools`:

- `packages/build-tools/src/android/gradle.ts`
- `packages/build-tools/src/steps/utils/android/gradle.ts`

This keeps the change narrowly scoped to Gradle execution instead of changing the broader worker environment or unrelated iOS/macOS build paths.

# Test Plan

- Verified the affected environment does not support `en_US.UTF-8`:
  ```sh
  locale -a
  ```
  Output:
  ```sh
  C
  C.utf8
  POSIX
  ```

- Confirmed that setting `LC_ALL=C.UTF-8` fixes the Gradle issue locally.

- Ran a targeted TypeScript check after the code change:
  ```sh
  ./node_modules/.bin/tsc -p packages/build-tools/tsconfig.json --noEmit
  ```
2026-03-11 13:39:38 +01:00
Stanisław Chmiela ce8f438610 [build-tools] Inject Android version without credentials (#3482)
# Why

The Android builder currently only injects Gradle config during `PREPARE_CREDENTIALS`. That means `job.version.versionCode` and `job.version.versionName` are ignored for builds that do not provide Android build credentials, even though those version values are present on the job.

This ports the Android version injection change from `expo/eas-build#425`, now on top of the merged enum-backed `CONFIGURE_ANDROID_VERSION` build phase.

# How

- switch the Android builder from the legacy `eas-build.gradle` shim to the existing direct Gradle injection helpers
- keep credential injection gated on build credentials
- inject the Android version Gradle config whenever `job.version` includes `versionCode` or `versionName`, even if there are no credentials
- run that injection in `BuildPhase.CONFIGURE_ANDROID_VERSION`
- remove the now-unused legacy shim files
- warn when deprecated `eas-build.gradle` still exists, matching the previous conservative behavior
- add focused Android builder coverage for the no-credentials version case
- add helper coverage for the legacy warning path

# Test Plan

- `mise exec -- yarn workspace @expo/build-tools jest-unit src/steps/utils/android/__tests__/gradleConfig.test.ts src/builders/__tests__/android.test.ts`
- `mise exec -- yarn workspace @expo/build-tools test`
- `mise exec -- yarn workspace @expo/build-tools typecheck`
- `git diff --check`
2026-03-11 13:16:44 +01:00
Stanisław Chmiela f3fd05d2dc [eas-build-job] Rename UserFacingError to UserError (#3468)
## Summary
- rename `UserFacingError` to `UserError` across the stack
- keep payloads and behavior unchanged
- move unknown user-visible errors under the new name

## Changes
- rename the shared user error class in `eas-build-job`
- update `build-tools` and `worker` imports and `instanceof` checks
- update tests and direct `dist/errors` imports in build-tools

## Testing
- `packages/eas-build-job`: `npm exec -- corepack yarn build && npm exec -- corepack yarn test src/__tests__/errors.test.ts --runInBand`
- `packages/build-tools`: `npm exec -- corepack yarn jest-unit src/buildErrors/__tests__/detectError.test.ts src/steps/utils/ios/__tests__/AscApiUtils.test.ts --runInBand`
- `packages/worker`: `npm exec -- corepack yarn test:integration src/__integration__/stateSync.test.ts`
2026-03-10 10:17:55 +01:00
Ash Wu 1ca478cb5e [build-tools][steps] Report step metrics after each step (#3479)
* [build-tools][steps] Report step metrics after each step instead of batching at job completion

Signed-off-by: Ash Wu <[email protected]>

* address comments

Signed-off-by: Ash Wu <[email protected]>

* move reportStepMetric to customBuildContext

Signed-off-by: Ash Wu <[email protected]>

* address comments

Signed-off-by: Ash Wu <[email protected]>

---------

Signed-off-by: Ash Wu <[email protected]>
2026-03-10 14:57:53 +08:00
Expo CI efbb61b088 update CHANGELOG.md 2026-03-10 02:04:35 +00:00
Expo CI 3984c9a938 v18.3.0 2026-03-10 02:02:33 +00:00
Brent Vatne a3c9e6727e [eas-cli] Show user email in account:view and whoami (#3485) 2026-03-09 16:43:53 -07:00
Expo CI e06a5babcf update CHANGELOG.md 2026-03-09 22:20:37 +00:00
Expo CI 5f400d7766 v18.2.0 2026-03-09 22:17:42 +00:00
Hassan Khan 4338aea702 feat(cli): add flag to prevent uploading server sourcemaps (#3478) 2026-03-09 18:45:07 +00:00
Stanisław Chmiela ecf42e68f3 [eas-build-job] Add configure Android version build phase (#3483)
# Why

Preparation for https://github.com/expo/eas-cli/pull/3483.

# How

- add `BuildPhase.CONFIGURE_ANDROID_VERSION`
- add display metadata for the new phase
- add website ID metadata for the new phase

# Test Plan

CI should pass.
2026-03-09 16:25:01 +01:00
Stanisław Chmiela f23579fa32 [eas-cli] Upgrade eas-cli to oclif v4 (#3472)
## Why

Old task I asked Codex to do.

## How

- upgrade `eas-cli` from oclif v3 to v4 using the native v4 APIs
- migrate legacy array-style `static args = [...]` declarations to native v4 object-style `Args.*` definitions
- keep command typing fully native and type-safe, including `Flags.option(...).options` const assertions where needed for inference
- update tests and shared test helpers for the v4 config/hook behavior

Notes:
- argument order is preserved in oclif v4 because `static args = { ... }` is treated as an ordered object; positional args follow object insertion order
- the bin entrypoints now use `@oclif/core` `execute(...)`, which is the supported v4 boot path
- `execute(...)` already performs the old `run().then(flush).catch(handle)` flow internally, so the explicit `flush` and `handle` chaining was removed as redundant rather than as a behavior change
- `bin/dev` keeps the dev entrypoint behavior through `execute({ development: true, ... })`, which enables the development boot path and verbose debugging behavior in v4

## Test plan

- `./node_modules/.bin/tsc --project packages/eas-cli/tsconfig.build.json`
- `node packages/eas-cli/bin/run --help`
- `node packages/eas-cli/bin/dev --help`
- `npx -y @yarnpkg/[email protected].0 workspace eas-cli test --runInBand`
2026-03-09 11:18:15 +01:00
Stanisław Chmiela afdc626fcb [build-tools] Introduce ExpoError as the shared build error base (#3470)
## Summary
- add `ExpoError` as the shared base for build-related errors
- centralize shared metadata and formatting there
- keep runtime behavior unchanged

## Changes
- introduce `ExpoError` with `errorCode`, `trackingCode`, `docsUrl`, `cause`, and `buildPhase`
- make `BuildError` and `UserFacingError` extend `ExpoError`
- add unit coverage for the shared base behavior

## Testing
- `packages/eas-build-job`: `npm exec -- corepack yarn build && npm exec -- corepack yarn test src/__tests__/errors.test.ts --runInBand`
- `packages/build-tools`: `npm exec -- corepack yarn jest-unit src/buildErrors/__tests__/detectError.test.ts --runInBand`
- `packages/worker`: `npm exec -- corepack yarn test:integration src/__integration__/stateSync.test.ts`
2026-03-09 10:57:56 +01:00
Evan BaconandClaude Opus 4.5 f9f4583a39 [eas-cli] Auto-generate Android keystore in non-interactive mode (#3477)
Instead of throwing an error when running `eas build -p android --non-interactive`
without existing credentials, automatically generate a keystore using the existing
`generateRandomKeystoreAsync()` function.

Co-authored-by: Claude Opus 4.5 <[email protected]>
2026-03-07 13:11:03 -08:00
Krystof WoldrichandClaude Opus 4.6 2d955440ca feat(cli): enable --non-interactive when --json (#3476)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-03-07 11:45:07 +01:00
Mustafa Abban 642bb6fa4d [build-tools] Remove cache support check in restoreCache for build jobs (#3475) 2026-03-06 14:42:30 -06:00
Douglas Lowder fb1f2d2add [cli] CI check and environment flag override for update (#3442)
# Why

Provide a way to override the new requirement for the `--environment` flag in `eas update`.

# How

Modify the check to skip when the `EAS_UPDATE_SKIP_ENVIRONMENT_CHECK` variable is set to a boolish value (1 or true).

If the command is run interactively, we now prompt the user for the environment, instead of throwing an error. This is skipped if the `--auto` flag is set. A missing environment flag already throws an error if `--non-interactive` is set.

Add the `CI` flag to the tests for whether we should skip the check in a CI environment.

# Test Plan

New unit tests added.
2026-03-06 06:55:52 -08:00
Stanisław Chmiela dbcca53f1d [worker] Add HttpLogStream (#3391)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

We want to push logs to a logs service.

# How

Initially I wanted to set up a WebSocket connection between worker and the logs service to reduce the HTTP overhead, but I found having a persistent/reconnecting WebSocket connection is not trivial and HTTP should be ok.

Confirmed failure to upload is not logged in the GCS log.

# Test Plan

Tested manually.


https://github.com/user-attachments/assets/f80f0482-73f5-4dfb-855f-2361ed04aa4f
2026-03-06 14:43:58 +01:00
Stanisław Chmiela 4a0dc9f426 [worker] Remove duplicated user-facing fields from build errors (#3469)
## Summary
- remove the duplicated `userFacingMessage` and `userFacingErrorCode` fields from the build error flow
- keep current runtime behavior and unknown-error semantics
- preserve the current worker protocol shape

Stanley: I don't want to have separate `userFacingMessage` and `message` / `userFacingErrorCode` and `errorCode`. It looks like the reason we had them separate was to log "UNKNOWN_ERROR" to the user if we encounter an internal issue like "NPM_CORRUPTED_PACKAGE". This pull request inverts the logic — we're going to have a `trackingCode` so we keep logging "NPM_CORRUPTED_PACKAGE" to Sentry and the default `errorCode` is the one we log to the user.

## Changes
- make `BuildError.format()` use canonical `message` and `errorCode`
- make `build-tools` store internal-only classification in `trackingCode` instead of parallel public fields
- make worker derive `internalErrorCode` from `trackingCode ?? errorCode`
- delete the obsolete `userFacing...` fields from `BuildError`

## Testing
- `packages/eas-build-job`: `npm exec -- corepack yarn test src/__tests__/errors.test.ts --runInBand`
- `packages/build-tools`: `npm exec -- corepack yarn jest-unit src/buildErrors/__tests__/detectError.test.ts --runInBand`
- `packages/worker`: `npm exec -- corepack yarn test:integration src/__integration__/stateSync.test.ts`
2026-03-06 14:11:10 +01:00
Stanisław Chmiela 146656c7f7 [build-tools] Handle ASC 90478 as closed version train (#3460)
## Why
`submit_ios_from_linux` UNKNOWN_ERROR analysis showed App Store Connect responses that clearly indicate the same version-train failure class but with mixed codes (`90062`, `90186`, `90478`).

Current handling recognizes only `90062` and `90186`, so runs that include `90478` fall back to `UNKNOWN_ERROR`.

## How
- extend `isClosedVersionTrainError` to include ASC code `90478` while keeping strict `.every` matching
- broaden `EAS_UPLOAD_TO_ASC_CLOSED_VERSION_TRAIN` user-facing message to cover both closed train and lower-than-approved version cases
- update unit test to include `90478` in positive closed-train detection

## Examples
Existing `EAS_UPLOAD_TO_ASC_CLOSED_VERSION_TRAIN` runs:
- https://expo.dev/uuid/019cbe83-ac3b-756f-b7ef-447bae5ef4dc
- https://expo.dev/uuid/019cbe66-4f9b-7d95-a35e-06886b646434

Would-be closed-train runs currently persisted as `UNKNOWN_ERROR` (log errors include `90478` with `90062`/`90186`):
- https://expo.dev/uuid/019cbe71-62fa-7fee-9e77-bdc051cc6e8b
- https://expo.dev/uuid/019cbe44-5c75-7211-af66-35fe96a86903

## Test Plan
- `yarn --cwd packages/build-tools jest-unit src/steps/functions/__tests__/uploadToAsc.test.ts`
- `yarn --cwd packages/build-tools typecheck`
- `yarn fmt`
2026-03-05 16:29:48 +01:00
Phil Pluckthun e37411040f [build-tools] Add --include=dev for npm install and --production false for Yarn classic (#3459)
* Add missing `--include=dev` for regular npm install

* Override NODE_ENV globally for yarn

* Add `--production false` for Yarn Classic and remove NODE_ENV change

* Add CHANGELOG entry
2026-03-05 14:39:03 +00:00
Mustafa Abban b71226d90f [worker] Handle non-OK HTTP responses before JSON parsing in upload session (#3457) 2026-03-04 15:35:09 -06:00
Stanisław Chmiela 9a9fe11f73 [build-tools] Handle ASC missing purpose strings as user-facing error (#3456)
## Why
Uploads to App Store Connect can fail with code `90683` when required `*UsageDescription` privacy keys are missing in `Info.plist`.

Without dedicated handling this ends up as a generic upload failure, even though ASC already returns structured details that can guide the user.

## How
- add ASC error recognition for `90683` in `uploadToAsc` failed-state handling
- throw a specific `UserFacingError` with code `EAS_UPLOAD_TO_ASC_MISSING_PURPOSE_STRING`
- parse ASC error descriptions to extract and list missing `*UsageDescription` keys
- provide concrete remediation for both CNG (`ios.infoPlist`) and non-CNG (`Info.plist`) workflows
- set `docsUrl` on the user-facing error to Expo permissions docs
- add unit tests for detection and missing-key parsing helpers

## Test Plan
- `yarn --cwd packages/build-tools jest-unit src/steps/functions/__tests__/uploadToAsc.test.ts`
- `yarn --cwd packages/build-tools typecheck`
- `yarn fmt`
2026-03-04 21:27:59 +01:00
Stanisław Chmiela 1b62b6feae [build-tools] Add ASC invalid bundle ID user-facing error (#3455)
## Why
Some App Store Connect uploads currently end up as generic failures even when ASC returns explicit bundle identifier mismatch validation ([`90054`](https://expo.dev/accounts/dobrokruh/projects/dobrokruh/job-runs/019cb865-ef97-71d1-9137-f6ae4c05d94d)/[`90055`](https://expo.dev/accounts/central-studio/projects/hfc-fuels/job-runs/019cb882-1550-7801-9b4b-9647e1bce364)). We want a user-facing error that explains the mismatch and shows both identifiers so users can quickly determine whether they selected the wrong app or the wrong build profile/build.

## How
- add `isInvalidBundleIdentifierError` detector in `uploadToAsc` for ASC error codes `90054`/`90055`
- when matched, throw `UserFacingError` with code `EAS_UPLOAD_TO_ASC_INVALID_BUNDLE_ID`
- include both identifiers in message:
  - IPA bundle identifier (best-effort read via `readIpaInfoAsync`)
  - selected ASC app bundle identifier
- provide remediation covering both paths:
  - wrong selected app identifier in submit profile
  - wrong build selected / rebuild with different profile
- use `asyncResult` for best-effort IPA introspection (no nested catch needed)
- add unit tests for invalid bundle-id detector

## Test Plan
- `yarn --cwd packages/build-tools jest-unit src/steps/functions/__tests__/uploadToAsc.test.ts`
- `yarn --cwd packages/build-tools typecheck`
2026-03-04 13:58:10 +01:00
Stanisław Chmiela ebb215db1a [build-tools] Remove -z from nc (#3454)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

Older (default) Android system image package does not support `-z`.

```
  Forward stdin/stdout to a file or network connection.

  -4	Force IPv4
  -6	Force IPv6
  -L	Listen for multiple incoming connections (server mode)
  -U	Use a UNIX domain socket
  -W	SECONDS timeout for more data on an idle connection
  -f	Use FILENAME (ala /dev/ttyS0) instead of network
  -l	Listen for one incoming connection
  -p	Local port number
  -q	Quit SECONDS after EOF on stdin, even if stdout hasn't closed yet
  -s	Local source address
  -t	Allocate tty (must come before -l or -L)
  -u	Use UDP
  -w	SECONDS timeout to establish connection
```

# How

Replaced `-z` with a closed stdin.

# Test Plan

System tests should work.

Deployed worker from this branch at https://github.com/expo/eas-cli/actions/runs/22666833774/job/65700594158.

Confirmed works at https://staging.expo.dev/accounts/expo-services/projects/worker-system-tests/workflows/019cb88e-f710-74d3-87ba-3c17bfe45c00.
2026-03-04 12:31:52 +01:00
Stanisław Chmiela cdf49377cd [build-tools] Replace ping with nc for Android network readiness check (#3453)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

`ping` is blocked on Linux VMs. https://expo.dev/accounts/layerztec/projects/layerzwallet/workflows/019cb835-cf4a-7b9f-af32-db474797eb85

# How

Replaced `ping` check with `nc`.

# Test Plan

Tested manually on staging that it works. https://staging.expo.dev/accounts/expo-services/projects/workflows-testing/workflows/019cb862-4ea6-7966-9148-5cb8dbfb1724
2026-03-04 10:50:17 +00:00
Stanisław Chmiela 8fb84e86bb [build-tools] Harden Android emulator startup readiness and retries (#3450)
# Why

Android emulators started for workflow/Maestro runs can report boot completion while still lacking functional networking. This makes Maestro tests more flaky.

# How

- Extend `AndroidEmulatorUtils.waitForReadyAsync` to require both:
  - `sys.boot_completed == 1`
  - successful guest-side network probe (`ping 1.1.1.1`)
- Add reusable `AndroidEmulatorUtils.stopAsync` and compose `deleteAsync` from stop+delete logic.
- Add startup retries with escalating readiness timeouts (60s, 120s, 180s) in:
  - `eas/start_android_emulator` base emulator startup
  - `eas/start_android_emulator` clone startup path
  - Android startup path inside `eas/__maestro_test` (`withCleanDeviceAsync`)
- Keep retries at 3 total attempts (initial + 2 retries).

# Test Plan

Tested with integration tests.
2026-03-04 10:18:43 +01:00
Stanisław ChmielaandCopilot d660e3602d [worker] Preserve UserFacingError instead of UNKNOWN_ERROR (#3445)
## Why
Generic/custom workflow steps can throw `UserFacingError`, but worker-level error handling currently maps any non-`BuildError` to `UnknownBuildError` / `UnknownCustomBuildError`. That causes actionable step errors to be persisted and exposed as `UNKNOWN_ERROR`.

## How
- add a `toBuildError` conversion in worker service catch path
- preserve existing `BuildError` values
- convert `UserFacingError` into `BuildError` with the same code/message/docsUrl
- keep unknown fallbacks only for truly unstructured errors
- add integration coverage in `stateSync` to assert generic job `UserFacingError` is exposed in `externalBuildError` and `internalErrorCode`

We should make a bigger refactor to error handling, but it's more involved — https://github.com/expo/eas-cli/pull/3447.

## Test Plan

CI should pass.


```
 PASS  src/__integration__/stateSync.test.ts
  State sync mechanism
    query new worker
      ✓ should return 'new' state (210 ms)
    query worker after dispatch
      ✓ should return 'in-progress' state (170 ms)
    query worker after build
      ✓ should return 'success' state (2062 ms)
    query worker after error
      ✓ should return 'error' state with artifacts from error (88 ms)
      ✓ should expose user-facing error from generic job (60 ms)
```

Co-authored-by: Copilot <[email protected]>
2026-03-04 09:38:36 +01:00
Expo CI 5b9d47c02b update CHANGELOG.md 2026-03-04 08:36:00 +00:00
Expo CI 61b601de88 v18.1.0 2026-03-04 08:33:39 +00:00
Byron KarlenandClaude Sonnet 4.6 c5867c8c07 [eas-cli] Fix login spinner interfering with prompts in eas go command (#3451)
https://github.com/user-attachments/assets/37835e4d-fca0-48a3-87e7-8c26e0c80a32



## Summary

- When running `eas go` while not logged in, an `ora` spinner ("Logging in to Expo...") was started before `getContextAsync` was called
- `getContextAsync` triggers interactive login prompts when unauthenticated, but the active spinner animation conflicts with readline input display, causing garbled/invisible prompts
- Fix: remove the spinner before `getContextAsync` and replace with `Log.withTick(...)` after it completes

## Test plan

- [ ] Run `eas go` while not logged in and verify login prompts are visible and interactive
- [ ] Run `eas go` while already logged in and verify "Logged in as ..." tick appears as expected

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
2026-03-04 09:25:12 +01:00
Stanisław Chmiela f2d5c210a7 [eas-cli] Enable merge when parsing workflow YAML (#3435)
## Summary

https://exponent-internal.slack.com/archives/C02VC9BLMQW/p1772224345536829?thread_ts=1763747642.435979&cid=C02VC9BLMQW

I think it'll be nice to add support for aliases and YAML merge. We already "support" it if the user specifies YAML 1.1 preamble.

## Test plan

Added unit tests. CI should pass. Ran `workflow:run shell.yml` with https://staging.expo.dev/accounts/expo-services/projects/workflows-testing/workflows/019cb7ed-5765-720e-8c2d-ee047af84933.
2026-03-04 09:19:12 +01:00
Stanisław Chmiela 6e70f7ee4b [build-tools] Improve ASC build upload error handling and diagnostics (#3444)
## Why
App Store Connect build upload failures were often surfaced as generic errors, which made remediation unclear for common cases like duplicate build numbers. We also needed stricter and safer response parsing so JSON parsing failures don’t leak raw parse errors.

## How
- Added typed `AscApiRequestError` in `AscApiClient` for structured ASC error payloads only.
- Kept non-structured ASC failures as regular `Error`.
- Hardened response parsing in `AscApiClient`:
  - parse response text with `asyncResult` + `JSON.parse`,
  - convert invalid success JSON into a controlled `Malformed JSON response...` error.
- Added `AscApiUtils.createBuildUploadAsync` and switched `uploadToAsc` to call it.
- Added duplicate build-number recognition for `/v1/buildUploads` failures when all ASC error codes are `ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE`.
- Added specific `UserFacingError` (`EAS_UPLOAD_TO_ASC_VERSION_DUPLICATE`) with concrete remediation:
  - increment `ios.buildNumber` (or set `"autoIncrement": true`),
  - rebuild and resubmit,
  - include docs URL.
- Inlined closed-train constants in `uploadToAsc` as requested.

## Test Plan
- `yarn --cwd packages/build-tools jest-unit src/steps/utils/ios/__tests__/AscApiClient.test.ts`
- `yarn --cwd packages/build-tools jest-unit src/steps/utils/ios/__tests__/AscApiUtils.test.ts`
- `yarn --cwd packages/build-tools jest-unit src/steps/functions/__tests__/uploadToAsc.test.ts`

All commands above pass on this branch.
2026-03-03 14:18:34 +01:00
Douglas Lowder 1d36f4f636 [cli] Remove environment requirement for update:configure (#3440)
# Why

This adjusts the change in #3418 that added a requirement for the `--environment` flag in `eas update` for SDK 55 and greater.

The requirement should not have applied to the `update:configure` subcommand. This PR removes the requirement there.

# How

Remove the check in `configure.ts`, and adjust flag descriptions.

# Test Plan

Existing tests should pass.
2026-02-28 11:36:58 -08:00
Expo CI f8deba78a5 update CHANGELOG.md 2026-02-27 22:36:57 +00:00
Expo CI df8ebc8f84 v18.0.6 2026-02-27 22:34:25 +00:00
Tomek Czajecki 62ab329f39 [eas-cli] Fix invisible credential prompts in eas go command (#3429) 2026-02-25 19:22:47 +02:00
Ash Wu 198b796804 [build-tools] Wrap parseMaestroResults in try-catch to prevent step failure (#3433)
Signed-off-by: Ash Wu <[email protected]>
2026-02-25 21:32:18 +08:00
Tomek Czajecki 110055d943 Add time estimates to go command spinners (#3428)
* [eas-cli] Add time estimates to go command spinners

* Fix formatting
2026-02-25 12:27:36 +02:00
Tomek Czajecki 06e2ba5bdb Update eas go fyi link (#3430) 2026-02-25 12:27:11 +02:00
Ash Wu 15690e7f6b [build-tools] Add eas/report_maestro_test_results build function (#3388)
Signed-off-by: Ash Wu <[email protected]>
2026-02-25 17:31:18 +08:00
Expo CI 134e74f50b update CHANGELOG.md 2026-02-25 00:42:47 +00:00
Expo CI 4b3dcda99d v18.0.5 2026-02-25 00:40:34 +00:00
Douglas Lowder 0c32aa3a39 [release] Work around the removal of yarn --silent (#3424)
# Why

When yarn moved to version 4, they removed the `--silent` flag. Because of this change, any use of yarn to call scripts and get output will also get yarn debugging garbage, and there is no way to suppress it.

The release scripts are mostly ok with this, except for calling the `next-version` script. The output of this script has to be clean for it to be used as input for `lerna version`.

# How

Replaced the yarn call with a direct invocation of the node Typescript source, bypassing yarn.

# Test Plan

Since the `trigger-release.yml` workflow supports a dry run mode, it's safe to modify it to check out a test branch and run the workflow many times to debug the issue.

Once this change is merged, a dry run invocation of the workflow should succeed even if no version string is passed in.
2026-02-24 03:37:51 -08:00
Tomek Czajecki 59ac8ae77a [eas-cli] Add go command (#3376) 2026-02-23 15:23:22 -08:00
Douglas Lowder 3e13df470e [eas-cli] Require --environment flag for eas update when SDK >= 55 (#3418)
# Why

Problems can occur if an update intended for a development build is published with the environment defaulting to or set to production, and vice versa.

To prevent such problems in the future, this PR makes the `--environment` flag a required flag for projects using SDK 55 and greater.

# How

Add a new utility function to throw an error if sdkVersion does not meet the requirement, and use that function in `eas update:configure` and `eas update`.

Skip the check if EAS_BUILD env is set.

# Test Plan

Unit tests added.

Testing locally with different projects.
2026-02-23 13:48:35 -08:00
Expo CI 83877f6285 update CHANGELOG.md 2026-02-23 20:47:53 +00:00
Expo CI 56018338e7 v18.0.4 2026-02-23 20:45:25 +00:00
Byron Karlen 0fdf2dd44a Revert "Use auth code flow (#3398)" (#3422)
This reverts commit 1604b7bc20.
2026-02-23 12:30:47 -08:00
Expo CI 0dcd6d465a update CHANGELOG.md 2026-02-20 08:02:18 +00:00
Expo CI d5da4f4167 v18.0.3 2026-02-20 07:59:25 +00:00
Stanisław Chmiela d0b9281bb2 [chore] Fix Yarn v4 prepack commands (#3417)
## Why
Release publish failed in `Publish to npm` for `v18.0.2`.

- `packages/eas-cli/scripts/prepack.js` invoked `yarn oclif manifest`, which fails on Yarn v4 (`Couldn't find a script named \"oclif\"`).
- `packages/create-eas-build-function` `clean` script used an unquoted `*.tsbuildinfo` glob, which fails when no files match.

## How
- Update `packages/eas-cli/scripts/prepack.js` to run `yarn run -T oclif manifest`.
- Quote the tsbuildinfo glob in `packages/create-eas-build-function/package.json`:
  - `rimraf ./build/ "*.tsbuildinfo"`

## Test plan
- `cd packages/eas-cli && mise exec [email protected].4 -- node ./scripts/prepack.js`
- `cd packages/create-eas-build-function && mise exec [email protected].4 -- yarn run clean`
- CI
2026-02-20 08:48:21 +01:00
Expo CI 77e04a3bad v18.0.2 2026-02-19 12:34:59 +00:00
Stanisław Chmiela 92ff6a0da5 [eas-build-job] Add hooks schema (#3408)
## Summary
- introduce a shared `Hooks` type/schema with reusable Zod/Joi validation so any string key maps to step arrays
- wire the new hooks definition through the generic, Android, and iOS job schemas and exports
- expand existing tests to cover accepting arbitrary hook names, empty hook arrays, and invalid hook step payloads

Not sure if it makes sense to make this "smart". I think let's just roll this out manually? Like, have `eas/install_node_modules` steps be sort of manually wrapped with hooks? Or maybe we'll sprinkle them when parsing the steps. I'll see. What I think is right is _not_ to limit the keys to only a few known values.

## Test plan

CI should pass.
2026-02-19 12:07:07 +01:00
Stanisław Chmiela 25d945b63f [worker] Move Yarn version from Mise to Corepack (#3412)
## Why

In https://github.com/expo/eas-cli/pull/3406 EAS workflow building the worker was **not** updated to Yarn v4. This breaks building worker.

I tried adding `defaults.tools.yarn: 4.12.0`, but this unfortunately [is **not** the way to use Yarn v4 on EAS as I've learned](https://docs.expo.dev/more/create-expo/#yarn-2-modern). The right way is using Corepack.

However, previously we explicitly did **not** use Corepack, because `pnpm --version` check the worker does in the beginning of its run was failing because it was run without `cwd`, so it was running within worker package which was configured with Yarn.

Since we want to use Yarn v4, we need to figure out a way to use Corepack.

## How

- Enabled Corepack.
- Set up `yarn` executable like the one we have in `universe`.
- Changed `pnpm --version` to **not** run inside worker (so it does not error).
- Removed `setup-node` for specific Node versions in favor of a Mise override.

## Test plan
- Corepack + Yarn resolution checks (local):
  - `mise exec [email protected].4 -- bash -lc 'corepack enable; yarn --version'` => `4.12.0`
  - same check from `scripts/` => `4.12.0`
- Worker runtime unit test (local):
  - `mise exec [email protected].4 -- bash -lc 'corepack enable; yarn workspace @expo/worker test:unit -- runtimeEnvironment.test.ts'`
- GitHub workflow runs:
  - success on Corepack worker workflow update: https://github.com/expo/eas-cli/actions/runs/22133707462
  - latest run for full set of changes: https://github.com/expo/eas-cli/actions/runs/22133932635
2026-02-19 10:17:57 +01:00
Stanisław Chmiela 37165428d1 [chore] Upgrade repository to Yarn 4 with Mise (#3406)
## Why
- Repository maintenance: standardize on Yarn 4 managed via Mise.
- Prepare for introducing minimum package-age constraints in dependency policy.

## How
- Migrated repo tooling to Yarn 4 with Mise.
- Updated CI and scripts for Yarn 4 behavior:
  - Lerna invoked via `yarn run -T lerna`.
  - Coverage args forwarded correctly (`-- --coverage`).
  - Removed unsupported Yarn 4 `--silent` usage.
- Added missing package-local script dependencies surfaced by Yarn 4 (`jest`, `oxlint`, `rimraf`).
- Increased `EasCommand` coverage test timeout from 30s to 60s. (Was consistently failing in CI.)

## Test Plan
- CI only: `Run tests` matrix (including Node 20 coverage) and other touched workflows.
- Release behavior will be verified during the normal release run.
2026-02-18 09:50:28 +01:00
Stanisław Chmiela 50b9be975e [build-tools] Add IPA info reader build function (#3405)
# Why

We need a build function that can read metadata directly from an IPA so later steps (for example App Store Connect upload flows) can consume canonical bundle values without requiring manual input.

# How

- Added a new build function `eas/read_ipa_info` in `packages/build-tools/src/steps/functions/readIpaInfo.ts`.
- The function accepts `ipa_path` and outputs:
  - `bundle_identifier`
  - `bundle_short_version`
  - `bundle_version`
- IPA inspection reads `Payload/*.app/Info.plist` from the zip and supports both plist formats commonly seen in IPAs:
  - binary plist (`bplist00`) via `bplist-parser`
  - XML plist via `plist`
- Added `UserFacingError` handling for parsing failures/invalid binary plist.
- Added the function to the EAS function registry in `packages/build-tools/src/steps/easFunctions.ts`.
- Added fixture-based tests using `SmallestAppExample.ipa` under `packages/build-tools/src/steps/functions/__tests__/fixtures/`.
- Added `node-stream-zip` and pinned `bplist-parser` to `0.3.2`.

# Test Plan

- Automated tests:
  - `yarn --cwd packages/build-tools test src/steps/functions/__tests__/readIpaInfo.test.ts`
- Typecheck:
  - `yarn --cwd packages/build-tools typecheck`

Coverage in `readIpaInfo.test.ts`:
- reads bundle identifier/short version/version from the fixture IPA
- fails when `Info.plist` is missing
- fails when required plist keys are missing
- verifies build-function outputs are populated correctly
2026-02-17 16:24:45 +01:00
Stanisław Chmiela 2655482f74 [worker] Logs improvements (#3397)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

For the new logs experience we need to identify every log with a unique ID.

Also, in the new logs experience we're going to be handling logs in plaintext, uncompressed, so size starts to matter.

# How

Decorating every log with a unique ID is surprisingly difficult with bunyan. It only runs serializers on the object passed in, I tried adding a `log_id` serializer `serializers: { log_id: () => uuidv7() }` and a non-empty value to build logger, but it was getting serialized in the moment of logger creation.

Instead, I'm adding `log_id` in the `Transform` that also masks secrets. The overall architecture looks like this now:

<img width="1742" height="1126" alt="Bez tytułu-2026-02-04-1436-3" src="https://github.com/user-attachments/assets/956e1b58-75ca-41c5-92ba-8229dd981ff6" />

Also removes `service: 'worker'` and `hostname` from logs. `service` was only being used [here](https://github.com/expo/universe/blob/3246198a3f913bf6fd0fd82e8e3df402cb59e82e/server/website/common/logs-utils.ts#L218) which I don't think is a thing right now.

# Test Plan

Added unit test that ensures `Transform` adds `logId`.

<img width="1257" height="1102" alt="Zrzut ekranu 2026-02-12 o 16 02 32" src="https://github.com/user-attachments/assets/4cfbbf43-744f-4af0-9faf-de0cab54f178" />
2026-02-16 09:31:09 +01:00
Stanisław Chmiela dc103acf06 [build-tools] Increase ASC build processing wait timeout (#3403)
## Summary
- extend the `upload_to_asc` build status polling window from 10 minutes to 30 minutes so slowly processed builds have time to reach TestFlight
- keep the rest of the polling logic and `wait_for_processing` default untouched
2026-02-16 09:02:00 +01:00
Mustafa Abban b87bc5bf8f [worker] unknown error log std error/output (#3402)
* [worker] unknown error log std error/output

* [worker] unknown error log std error/output

* check token
2026-02-13 14:19:04 -05:00
Byron Karlen 1604b7bc20 Use auth code flow (#3398) 2026-02-12 17:24:19 -08:00
Stanisław Chmiela 13a6356fca [chore] Fix post-release lint (#3396)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

After a new release is cut, `lerna.json` and `README.md` want to be reformatted by Oxfmt.

# How

Excluded them from being formatted.

# Test Plan

`yarn fmt` changes no files.
2026-02-12 15:59:27 +01:00
Expo CI 0823a9d675 update CHANGELOG.md 2026-02-12 11:18:30 +00:00
Expo CI 772d4dd654 v18.0.1 2026-02-12 11:14:44 +00:00
Stanisław Chmiela e8da383de4 [chore] Move to Oxlint (#3384)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

We're moving to Oxc in universe too. The toolset is faster than ESLint / Prettier.

# How

Asked Codex to migrate the repository. I don't think all rules got migrated, unfortunately, but also the lint are in a bit weird state (different rules in `eas-cli` vs the rest of the repo). I hope we can unify the rules soon.

# Test Plan

CI should pass.
2026-02-12 11:44:19 +01:00
Stanisław Chmiela d08f9cfdcb [chore] Remove .tsbuildinfo when deleting dist/build (#3394)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

Release failed because in `prepack` in some packages we delete `dist`, but don't delete `.tsbuildinfo` file.

# How

Made Codex fix all the package scripts. I _think_ we also could skip `yarn build` before publishing.

# Test Plan

Try to release and see if it works.
2026-02-12 11:32:37 +01:00
Expo CI ac6569aa58 v18.0.0 2026-02-11 21:21:31 +00:00
Stanisław Chmiela 960ce7dbf5 [infra] Stop preparing tarballs (#3361)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

Our GitHub released executable Oclif bundles of `eas-cli` are… not too popular. v16.28.0 was downloaded 154k times in the last 7 days from NPM. Since its release (2 months ago) it was downloaded from GitHub 82 times.

<img width="1247" height="491" alt="Zrzut ekranu 2026-01-30 o 15 15 35" src="https://github.com/user-attachments/assets/9c5a904a-e00e-43ee-9875-d1cea15b2108" />

# How

Asked Gemini to remove this. This should speed up release process significantly. This release process has been created 5 years ago (https://github.com/expo/eas-cli/pull/54).

Also removed size comparison workflow. It's not going to make comments like https://github.com/expo/eas-cli/pull/3361#issuecomment-3824036144.

Closes https://github.com/expo/eas-cli/pull/3346.

# Test Plan

CI should pass.
2026-02-11 21:13:47 +00:00
Mustafa Abban 029bbdd007 [worker] - log unknown build error logs in Datadog (#3386)
* log unknown build error logs in Datadog

* import order

* tag build phase, error code

* relay to www

* cleanup, stringify
2026-02-10 16:14:44 -05:00
Byron Karlen 7061a6743a fix changelog (#3389) 2026-02-09 16:24:10 -08:00
Byron Karlen 42940b7c4c [eas-cli] Add --browser option to eas login (#3312) 2026-02-06 09:51:37 -08:00
Stanisław Chmiela d29a88eafd [chore] Move all CI to Mise (#3379)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

I noticed I didn't migrate all workflows to Mise (because not all worklfows were using Volta lol).

# How

Codex added a shared Mise action (in other repos we have it set up like this too) and used it in all places where we require Node related to code (there's one workflow which does not use checkout, it remains on `setup-node` too). `test.yml` where we test against different Node versions still uses `setup-node`.

Note: this does downgrade Node in a few workflows from 22 to 20, but if 20 is what we decide to use for our development maybe CI should use it too?

# Test Plan

CI should pass.
2026-02-06 11:58:10 +01:00
Stanisław Chmiela 67087e9471 [chore] Fix pretarball-ci script (#3381)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

When oclif is building tarballs with executables it does so in a directory separate from the monorepo which requires us to change `eas-cli` dependencies from the monorepo to `file`.

# How

Asked Gemini to update the script to include all local dependencies of `eas-cli`.

Or… we can land https://github.com/expo/eas-cli/pull/3361 …

# Test Plan

Release should pass. Validated locally. (Hmm but on CI it failed? Will see.)

Ok, so size failed because it ran on `main` which is broken because it lacks this pull request.

<img width="944" height="1031" alt="Zrzut ekranu 2026-02-5 o 19 05 41" src="https://github.com/user-attachments/assets/1134493c-bbdb-4f08-85d6-07e59fa28750" />
2026-02-06 11:57:57 +01:00
Stanisław Chmiela 96dc437217 [build-tools] Move uploading IPAs to build-tools (#3383)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

I want to iterate on this faster.

# How

Asked Codex to move this to build functions.

# Test Plan

Unit tests were moved too. Land and test on staging.
2026-02-05 21:29:52 +01:00
Expo CI f68cbb48d7 v17.0.0 2026-02-05 14:58:59 +00:00
Stanisław Chmiela 50f1269a9c [eas-cli] Some polish to submit:upload-to-asc command (#3362)
# How

I like these flag names a bit better (AI preferred them too). Also made it so we don't wait for build processing by default.

# Test Plan

Tested manually.
2026-02-05 14:23:30 +01:00
Karol Rzemiński 3bc8a8e3e1 [chore] Upgrade tar to v7 (#3327)
# Why

After fixing [GHSA-8qq5-rm4j-mr97](https://github.com/advisories/GHSA-8qq5-rm4j-mr97) by setting resolution of `tar` to 7.5.3 EAS build started failing
<img width="776" height="144" alt="Zrzut ekranu 2026-01-20 o 20 17 57" src="https://github.com/user-attachments/assets/02f8d82e-6316-43c4-95e2-6434599456fd" />
Link to github issue: https://github.com/expo/eas-cli/issues/3320

# How

`tar` 7.x changed from default exports to named exports for better tree-shaking and ESM/CommonJS hybrid support
EAS CLI code still used the old import pattern which no longer exists

**Solution:**
Updated the TypeScript sources to import tar as a namespace instead of a default export
No functional changes besides fixing the broken tar calls, all call sites still use the same options and behavior as before

Additionally, updated `tar` from version 7.5.3 to 7.5.4 to address CVE-2026-23950

Closes https://github.com/expo/eas-cli/pull/3353, https://github.com/expo/eas-cli/pull/3360, https://github.com/expo/eas-cli/pull/3354.

# Test Plan

Run EAS build with tar 7.5.3 to reproduce
After this changes EAS build uploads tarball successfully
2026-02-05 13:53:32 +01:00
Stanisław Chmiela 510de5de4f [build-tools] Poll lockscreen state and retry unlock until it clears (#3377)
# Why

The Cuttlefish device may remain locked after an initial `KEYCODE_MENU` event which blocks reaching the home screen, so the unlock flow must verify the real lock state instead of blindly sending HOME. See https://exponent-internal.slack.com/archives/C02VC9BLMQW/p1769703880840729?thread_ts=1763747642.435979&cid=C02VC9BLMQW.

# How
- Updated `packages/build-tools/src/steps/functions/startCuttlefishDevice.ts` to poll `adb shell dumpsys window` and check `mDreamingLockscreen` before deciding the device is unlocked.
- Implemented a retry loop that sends `adb shell input keyevent 82` while the lockscreen remains active, waits 1s between attempts, and uses a 30 second deadline for the unlock attempts.
- When unlock is detected the code now sends `adb shell input keyevent 3` to return to the home screen and logs success, and it logs warnings on query failures or on timeout.

# Test Plan

- Ran `yarn typecheck` which completed successfully.
- Ran `yarn lint --fix` which completed successfully.

------
[Codex Task](https://chatgpt.com/codex/tasks/task_e_698374b75b88832890754e697e58e5ea)
2026-02-05 13:20:01 +01:00
Stanisław Chmiela 0deeb469d6 [chore] Use Mise for Yarn too (#3378)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

I knew https://github.com/expo/eas-cli/pull/3366 won't work! ([Failure](https://github.com/expo/eas-cli/actions/runs/21709140672/job/62608616635).)

Using corepack for Yarn breaks EAS Build because when we run `pnpm --version` when setting up the environment, it runs in default cwd which is the worker directory and if worker has `packageManager` different than PNPM, PNPM throws an error. This was originally found in February 2025 by @szdziedzic.

# How

Use Mise for Yarn installation (instead of corepack).

# Test Plan

Merge and see if system tests work.
2026-02-05 13:08:24 +01:00
Stanisław Chmiela 9b91bc37c9 [chore] Migrate from Volta to Mise (#3366)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

I think we migrated from Volta to Mise in… most if not all places?

# How

I think I let Codex do all the work, nothing too difficult. Grepped repo for "volta" leftover afterwards.

One thing I'm not sure of is whether worker is going to work. I remember trying to remove Volta in `turtle-v2` and it wasn't working for some reason? We'll see.

# Test Plan

Compared these changes with https://github.com/expo/universe/commit/acca8169746c5cb4d708add083f77b927341ae4a.
2026-02-05 12:08:12 +01:00
Stanisław Chmiela 952b275c90 [chore] Revamp testing (#3375)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

We moved more packages into this monorepo and the existing test setup didn’t scale well:
- the root Jest config only covered a subset of packages,
- PRs ran heavier “all tests” workflows,
- worker builds ran on PRs even though typecheck + tests were enough.

This PR aligns testing with the dependency graph, reduces unnecessary CI work, and standardizes Jest config handling.

# How

- Switched root testing to Lerna and updated docs.
- CI now runs affected+dependent tests on PRs and all packages on main.
- Coverage runs via Lerna with per-package reports (no merge) and is kept for PRs.
- Worker builds are skipped on PRs; worker workflow triggers include dependency paths on main.
- All Jest configs are now TypeScript with a shared base (`jest/jest.shared.config.ts`) and use `@jest/types` to avoid TS config parsing errors.
- CI stays unit-only (no integration tests).

# Test Plan

Not run locally. CI covers:
- `yarn lerna run test` (affected on PRs, all on main)
- `yarn lerna run test -- --coverage`
2026-02-04 21:34:56 +01:00
Colin MacKenzieandClaude Opus 4.5 9dd75445c3 Don't show build credits warning bar at 100% usage (#3371)
* [eas-cli] Don't show build credits warning bar at 100% usage

The warning bar is meant to alert users when they're approaching their
limit. At 100%, they've already hit it, so the bar is redundant.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* Update CHANGELOG for #3371

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* [eas-cli] Hide progress bar (not warning) at 100% usage

The warning should still show at 100% to inform users they've hit the
limit, but the progress bar is redundant when completely full.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
2026-02-03 15:15:17 -06:00
Stanisław Chmiela da6703706a [build-tools] Add repack_package (#3367)
https://github.com/expo/universe/pull/24634

# Test plan

No values defined

<img width="460" height="199" alt="Zrzut ekranu 2026-02-3 o 15 22 43" src="https://github.com/user-attachments/assets/4a45d554-af83-476e-a221-68dc29f84290" />


With overrides

<img width="694" height="356" alt="Zrzut ekranu 2026-02-3 o 15 11 38" src="https://github.com/user-attachments/assets/2ca1fa76-612f-4a3d-896e-67119fb1f676" />
2026-02-03 20:35:08 +01:00
Evan BaconandClaude Opus 4.5 673d8c3712 List available options in non-interactive mode errors (#3359)
* Improve error message for Apple team selection in non-interactive mode

Enhanced the error thrown when selecting an Apple team in non-interactive mode to include a list of available teams and guidance on using the --apple-team-id flag. This provides better context and instructions for users running the CLI in non-interactive environments.

* List available options in non-interactive mode errors across all selection prompts

Non-interactive mode errors now include the available options so agents and
scripts can discover valid values and retry with the correct flag. This applies
to selection prompts for Apple teams, Apple devices, builds, branches, channels,
update groups, runtimes, rollout actions, and environment variable type/visibility/environment.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* Fix test expectation and update changelog PR number

Update test for selectUpdateGroupOnBranchAsync to match the new error
message when no update groups are found in non-interactive mode. Update
changelog entry with PR #3359.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* Fix prettier formatting

Co-Authored-By: Claude Opus 4.5 <[email protected]>

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
2026-02-03 10:46:30 -08:00
Stanisław Chmiela c6463af3b9 [worker] Copy bin directory too (#3370)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

https://staging.expo.dev/accounts/expo-services/projects/worker-system-tests/builds/59628168-4fe6-4cef-981a-fb74527c47ed

# How

Added copying of missing `bin`.

# Test Plan

Merge and observe system tests.
2026-02-03 19:42:50 +01:00
Stanisław Chmiela f94dc6bd59 [worker] Copy all local packages required by @expo/worker (#3368)
`package.sh` currently does not copy workspace packages into the temporary build folder which makes worker pull packages from NPM.

Instead, we want to use local versions from workspace.

Added `copy_package` calls for all dependencies of `@expo/worker`. 

It was done in a similar way in `turtle-v2` — https://github.com/expo/turtle-v2/blob/245eaf23492f2dcd9204a4606bb95c109208d29e/src/services/worker/package.sh#L46-L47.

Ran `./package.sh` and confirmed `@expo/eas-build-job` and other packages are symlinked.
2026-02-03 17:06:53 +01:00
Stanisław Chmiela 51926d13db [chore] Update yarn.lock (#3365)
Missed outdated yarn.lock. Weird that `yarn --frozen-lockfile` didn't fail?
2026-02-03 15:59:50 +01:00
Stanisław Chmiela 789cf62bd6 [chore] Add eas-build packages (#3290)
## Consolidate eas-build packages into eas-cli monorepo

This PR completes the migration of packages from the `eas-build` repository into `eas-cli`, consolidating all EAS Build tooling into a single monorepo.

### Background

Previously, the EAS Build libraries lived in a separate `expo/eas-build` repository. This made development and releases more complex, requiring coordination between two repos. By consolidating into `eas-cli`, we can:

- Develop and test changes across CLI and build tools in a single PR
- Simplify the release process
- Reduce dependency version drift between packages

### Migrated Packages

The following packages have been moved from `eas-build`:

| Package | npm Name | License |
|---------|----------|---------|
| `build-tools` | `@expo/build-tools` | BUSL-1.1 |
| `eas-build-job` | `@expo/eas-build-job` | MIT |
| `steps` | `@expo/steps` | BUSL-1.1 |
| `logger` | `@expo/logger` | BUSL-1.1 |
| `downloader` | `@expo/downloader` | BUSL-1.1 |
| `turtle-spawn` | `@expo/turtle-spawn` | BUSL-1.1 |
| `template-file` | `@expo/template-file` | BUSL-1.1 |
| `local-build-plugin` | `eas-cli-local-build-plugin` | BUSL-1.1 |
| `create-eas-build-function` | `create-eas-build-function` | MIT |

### Changes

**Package Configuration:**
- Updated `repository` and `bugs` URLs in all migrated package.json files to point to `expo/eas-cli`
- Updated README repository links
- Aligned dependency versions across `eas-cli`, `eas-json`, and `worker` to use workspace packages

**Documentation:**
- Expanded `CLAUDE.md` with comprehensive documentation covering all packages, architectural patterns, and development workflows
- Added `AGENTS.md` for AI agent guidance

**Licensing:**
- Added `LICENSE-BUSL` for BUSL-1.1 licensed packages
- Added `LICENSE-eas-build-job` for MIT licensed packages

**Build Infrastructure:**
- Added `.gitignore` entries for build artifacts: `dist`, `*.tsbuildinfo` (needed for `tsconfig.json` and `tsconfig.build.json`), vim swap files
- Added `bin/eas-cli-local-build-plugin` symlink for local build testing

**Dependency Cleanup:**
- Removed stale npm registry entries from `yarn.lock` - workspace packages now resolve locally

### Testing

- [x] `yarn install` completes successfully
- [x] `yarn build` builds all packages
- [x] `yarn test` passes
- [x] Worker builds and runs correctly against workspace packages

### Follow-up

After this PR is merged:
- Archive or deprecate the `expo/eas-build` repository
- Update any CI/CD pipelines that referenced the old repo
- Update internal documentation pointing to eas-build
2026-02-03 14:30:15 +01:00
Vojtech Novak a476d44066 [eas-build] warn when creating a production build from Expo Go (#3073) 2026-02-02 08:51:26 +00:00
Stanisław Chmiela d8a87a6195 [chore] Require Node 20+ (#3343)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

Noticed we're not running tests on Node 18 anymore (since https://github.com/expo/eas-cli/commit/6bb09c4e99950a6b46b0b908a93e8fcd9b3002aa). I think we probably want to officially drop support for Node 18 too.

# How

Looked at https://github.com/expo/eas-cli/commit/eb4c5e073d02f38d7f90fbd6a1b2b5491dc40464, pinned Node to a newer version, updated types, adjusted `engines`.

# Test Plan

CI should pass.
2026-01-30 15:09:44 +01:00
Expo CI 846aacd320 update CHANGELOG.md 2026-01-30 10:20:52 +00:00
Expo CI 079aa5dc66 v16.32.0 2026-01-30 10:11:55 +00:00
Stanisław Chmiela fa8980759f [eas-cli] Add submit:upload-to-asc command (#3358)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

We want to move iOS submissions to Linux machines, which requires us to either:
- have Builds generate not only an IPA but also an Xcode asset file for use with Transporter
- reimplement upload ourselves with App Store Connect API.

# How

As much as I fear the unknown (does this support App Clips? do App Clips come in a different package files? what about macOS apps?) I wonder if, by any chance, this would… just work?

Planning to wire this to submissions in some opt-in way and ask for early feedback in Discord.

# Test Plan

Tested manually — uploaded a file as IPA. It obviously failed to process, but at least it uploaded all right.

```
Generating JWT...
Reading App information...
Uploading Build to "turtle-cli-example" (com.expo.turtle.cli.example)...
Creating Build Upload...
Build Upload initialized (ID: 6a3f6a72-d088-4ac8-9cdd-7d1fb83f7c8b). Preparing IPA upload...
IPA Upload initialized (ID: 0374ffc4-c51b-42df-9b2f-48e9a81a62eb). Uploading IPA...
  Uploading chunk 1/1 (offset: 0, length: 359)...
  Chunk 1/1 uploaded.
Committing upload...
Checking upload file status...
File upload (ID: 0374ffc4-c51b-42df-9b2f-48e9a81a62eb) complete!
Checking build upload status...
Waiting for build upload to finish... (status = PROCESSING)
Waiting for build upload to finish... (status = PROCESSING)
Waiting for build upload to finish... (status = PROCESSING)
Waiting for build upload to finish... (status = PROCESSING)
Waiting for build upload to finish... (status = PROCESSING)
Waiting for build upload to finish... (status = PROCESSING)
Waiting for build upload to finish... (status = PROCESSING)


Errors:
- Invalid package. The uploaded package is corrupt. Verify that you have sufficient disk space on your computer and are uploading only one package at a time. If you created the package manually, verify that it‘s a valid ZIP archive file. (90168)

Build upload (ID: 6a3f6a72-d088-4ac8-9cdd-7d1fb83f7c8b) failed.
    Error: submit:upload-to-asc command failed.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
```
2026-01-30 11:08:07 +01:00
Stanisław Chmiela 790085ea9e [worker] Bump build-tools (#3357)
https://github.com/expo/eas-build/pull/668
2026-01-29 11:31:46 -05:00
Will Schurman 🦦 9c7ac49f3e [eas-cli] Remove unnecessary method re-declaration (#3352) 2026-01-28 15:48:17 -08:00
Evan Bacon fa244d6410 Add eas account:usage command (#3334)
## Summary

Add a new `eas account:usage` command that displays account usage and billing information for the current billing cycle. The command is hidden for now, until we fully test it in production.

```
USAGE
  $ eas account:usage [ACCOUNT_NAME] [--json --non-interactive]

ARGUMENTS
  ACCOUNT_NAME  Account name to view usage for. If not provided, the account will be selected
                interactively (or defaults to the only account if there is just one)

FLAGS
  --json             Enable JSON output, non-JSON messages will be printed to stderr.
  --non-interactive  Run the command in non-interactive mode.

DESCRIPTION
  view account usage and billing estimates for the current cycle

```

- View EAS Build usage with iOS/Android platform breakdown
- View EAS Update usage (MAU and bandwidth)
- Display billing period dates and progressOptional account name argument
- Display upcoming invoice, similarly to the billing website
- If no account name passed in, and user has more than one account, a selection list is shown

- `--json` flag for JSON output (optimized for agents/automation)
- Supports `--non-interactive`
    - Forces JSON output
    - Requires account name if user has more than one account

## Example Output

```
✔ Usage data loaded for example-customer

Account: example-customer
Plan: Production
Concurrencies: 6 total (iOS: 6, Android: 6)
Billing Period: Jan 9, 2026 - Feb 9, 2026
Days: 18 elapsed / 31 total (14 remaining)

EAS Build
  Builds (plan): 10,000/10,000 builds
  ████████████████████ 100%
  Builds (overage): 781 builds ($1462.00)
  Breakdown by platform/worker:
    Android large: 86 builds
    iOS medium: 695 builds

EAS Update
  Unique Updaters (plan): 50,000/50,000 users
  ████████████████████ 100%
  Unique Updaters (overage): 1,021,888 users ($5109.44)
  Bandwidth (plan): 17.7 TiB/40.0 TiB
  █████████░░░░░░░░░░░ 44%

Billing
  Addons:
    Additional Concurrency: 4

  Upcoming invoice:
    EAS Build - Plan credit: $-100.00
    EAS Build - Build (Android: 64 large builds; iOS: 628 medium builds): $1384.00
    EAS Update - 1,094,102 monthly active users: $3857.30
    EAS Workflows - 5,347 jobs (3,970.4 minutes): $71.46
    1 × Expo Application Services - Production (at $99.00 / month): $99.00
    4 Concurrency × EAS Build - Additional Concurrency (at $50.00 / month): $200.00

  Invoice total: $5511.76

View detailed billing: https://expo.dev/accounts/example-customer/settings/billing)
```

## Test plan

- [x] Build passes (`yarn build`)
- [x] TypeScript type check passes (`yarn typecheck`)
- [x] Lint passes for new files
- [x] Unit tests pass (`yarn test packages/eas-cli/src/commands/account/__tests__/usage-test.ts`)
- [x] Manual testing with real account

Slack thread: https://exponent-internal.slack.com/archives/C017N0N99RA/p1769054123592549?thread_ts=1768439335.822519&cid=C017N0N99RA

Closes ENG-18827
2026-01-27 14:22:56 -08:00
Stanisław Chmiela db8a3770f8 [worker] Set DELIVER_ALTOOL_ADDITIONAL_UPLOAD_PARAMETERS in Xcode 26.0 (#3350)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

> Apple's altool in Xcode 26.0 has a bug where it incorrectly selects the wrong app when multiple apps share a common bundle ID prefix (e.g., `com.example`, `com.example.app1`, `com.example.app2`). This causes builds to be uploaded to the wrong app on App Store Connect.

> See: https://github.com/fastlane/fastlane/issues/29698

https://github.com/expo/universe/pull/23310 + https://exponent-internal.slack.com/archives/C06EFBQK3B7/p1765365928911459

# How

If `xcodebuild -version` matches "Xcode 26.0", we'll add `DELIVER_ALTOOL_ADDITIONAL_UPLOAD_PARAMETERS='--use-old-altool'`. This helps if the code does not re-override `$DELIVER_ALTOOL_ADDITIONAL_UPLOAD_PARAMETERS` further, in which case it should do `DELIVER_ALTOOL_ADDITIONAL_UPLOAD_PARAMETERS="overrides $DELIVER_ALTOOL_ADDITIONAL_UPLOAD_PARAMETERS"` or sth like that.

I hope this fixes Launch.

# Test Plan

Tested manually on my computer

```
> spawnSync("xcodebuild", ["-version"]).stdout.includes("Xcode")
true

> spawnSync("xcodebuild", ["-version"]).stdout.includes("Xcode 26.0")
false

(below did not throw)
> spawnSync("abc")
{
  error: Error: spawnSync abc ENOENT
      at Object.spawnSync (node:internal/child_process:1123:20)
      at spawnSync (node:child_process:877:24)
      at REPL12:1:1
      at ContextifyScript.runInThisContext (node:vm:137:12)
      at REPLServer.defaultEval (node:repl:598:22)
      at bound (node:domain:433:15)
      at REPLServer.runBound [as eval] (node:domain:444:12)
      at REPLServer.onLine (node:repl:927:10)
      at REPLServer.emit (node:events:536:35)
      at REPLServer.emit (node:domain:489:12) {
    errno: -2,
    code: 'ENOENT',
    syscall: 'spawnSync abc',
    path: 'abc',
    spawnargs: []
  },
  status: null,
  signal: null,
  output: null,
  pid: 0,
  stdout: null,
  stderr: null
}
```
2026-01-27 23:08:35 +01:00
Stanisław Chmiela be16b0032f [chore] Bring back tagging as it was before (#3349)
https://github.com/expo/eas-cli/commit/15b690c85763fb8428a4255a9ded7e7e9126d4e8 did not work. Maybe `NODE_AUTH_TOKEN` is a special variable or something.
2026-01-27 17:51:53 +01:00
Expo CI 3ef14b5eb7 v16.31.0 2026-01-27 14:09:37 +00:00
Stanisław Chmiela c777b25e92 Add no-index-store Xcode setting (#3348)
See https://stackoverflow.com/questions/71109718/what-is-the-compiler-index-store-enable-xcode-build-settings-default-value#71710412 and https://github.com/flutter/flutter/issues/37231.
2026-01-27 14:51:28 +01:00
Stanisław Chmiela 15b690c857 [chore] Use NPM_ACCESS_TOKEN in steps moving tags between eas-cli versions (#3347)
We still need `NPM_ACCESS_TOKEN` despite OIDC for publishing. https://github.com/expo/eas-cli/actions/runs/21363225445/job/61513769796

------
[Codex Task](https://chatgpt.com/codex/tasks/task_e_6977d0214f5c832898a83f54ba0a68fd)
2026-01-27 12:25:40 +01:00
Will Schurman 🦦 4d04463714 [eas-cli] Remove commented-out code in eas update command (#3344) 2026-01-26 22:08:30 -08:00
Expo CI 1c31702653 v16.30.0 2026-01-26 15:26:12 +00:00
Stanisław Chmiela b053e0926a [chore] Switch to publishing with OIDC (#3342)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

https://exponent-internal.slack.com/archives/C017N0N99RA/p1769306804484099

# How

Applied a few of the changes we've done to `eas-build`:
- https://github.com/expo/eas-build/commit/784e37c96300b20e3f812083443f4334a77fc17e
- https://github.com/expo/eas-build/commit/ae9dbea0f994d2c43fdf96a885778168a60fa4f7
- https://github.com/expo/eas-build/commit/ec824f1deddef6b5a1459329a3f9273bf9d48832
- https://github.com/expo/eas-build/commit/54ff4e9017beed4e2f98b0794e47612f8f7f6d19

# Test Plan

Merge and try releasing a new version.
2026-01-26 16:21:18 +01:00
Brent Vatne 6eeb15bdce [eas-cli] Fix --source-maps flag compatibility with expo CLI (#3341) 2026-01-25 17:32:09 -08:00
Brent Vatne 279eb1595c [eas-cli] Fix changelog format 2026-01-25 14:08:27 -08:00
Brent Vatne 256b02d3e9 [eas-cli] Fix lint error 2026-01-25 13:09:06 -08:00
Brent Vatne 9f4fef7626 Add missing CHANGELOG entry 2026-01-24 19:00:09 -08:00
Brent Vatne 8cc324e11e [eas-cli] Use --dump-sourcemaps if --source-maps is not provided, for compat 2026-01-24 18:41:22 -08:00
Expo CI f33b4c131d v16.29.0 2026-01-25 01:48:46 +00:00
Brent Vatne e3eb3ad5ea [eas-cli] Add --no-bytecode and --inline-source-maps flags to eas update (#3339) 2026-01-24 17:45:02 -08:00
Stanisław Chmiela 1292465c57 [worker] Bump eas-build packages (#3337)
https://github.com/expo/eas-build/pull/658
2026-01-23 21:54:19 +01:00
Quinlan Jung 🦥 00d2022680 [eas-cli] add warning to channel:delete (#3335)
* Ad[eas-cli] add warning to channel:delete

* Temporary Commit at 1/22/2026, 3:11:19 PM
2026-01-22 15:25:25 -08:00
Stanisław Chmiela 5aaaf1b224 [worker] Bump @expo/build-tools (#3336)
https://github.com/expo/eas-build/pull/667
2026-01-22 21:08:07 +01:00
Stanisław Chmiela aaf14b8bab Add fallback repos to init.gradle (#3329)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

It _looks_ like if cache is configured, it replaces all other repositories, meaning if downloading an artifact from the cache fails, the whole build immediately fails. Instead, I wish Gradle tried downloading the artifact from other repositories.

We expect it to work like this already — https://github.com/expo/turtle-v2/blob/1c44f6e6206cdc703ac9f09ef813f5a0ad93ecb8/internal-docs/maven-cache.md#L7, since the beginning (https://github.com/expo/turtle-v2/pull/139).

# How

Gradle documentation is not clear regarding whether `repositories` add to or replace lists. [Here](https://docs.gradle.org/current/userguide/declaring_repositories.html) it says "add", but I'm not sure the same thing applies to `init.gradle` or if it applies to all `repositories` sections — of dependencies, plugins and initscript.

I did two things to inspect what's happening.

### `./gradlew help --scan`

[Without our Maven cache enabled](https://scans.gradle.com/s/pbd3vg3le6o3u/dependencies/repositories). [With our Maven cache enabled](https://scans.gradle.com/s/vl2ayuzigam2k/dependencies/repositories).

If you go to "Repositories" sections you'll see the one with our Maven cache enabled has our Maven cache as repository PLUS the ones that are used when Maven cache is not enabled.

#### Without cache enabled

<img width="1392" height="775" alt="Zrzut ekranu 2026-01-21 o 22 14 47" src="https://github.com/user-attachments/assets/33919a95-4f26-427d-9b3c-dca16848ae25" />
<img width="1392" height="775" alt="Zrzut ekranu 2026-01-21 o 22 14 55" src="https://github.com/user-attachments/assets/b79b951b-321b-4b15-b417-59358c4c5329" />

#### With cache enabled

<img width="1392" height="775" alt="Zrzut ekranu 2026-01-21 o 22 15 57" src="https://github.com/user-attachments/assets/ebed5389-bd49-4a19-bf49-93d2f622d502" />
<img width="1392" height="775" alt="Zrzut ekranu 2026-01-21 o 22 16 09" src="https://github.com/user-attachments/assets/27f32964-b318-4f50-8e62-b9a0a50457df" />

### Gradle script printing repositories

Inside the VM added

```gradle
gradle.beforeSettings { settings ->
  settings.pluginManagement.repositories.each { r ->
    println "pluginManagement repo: ${r.name} -> ${r.url}"
  }
  settings.dependencyResolutionManagement.repositories.each { r ->
    println "settings repo: ${r.name} -> ${r.url}"
  }
}

gradle.projectsLoaded {
  gradle.allprojects { p ->
    p.buildscript.repositories.each { r ->
      println "buildscript repo (${p.path}): ${r.name} -> ${r.url}"
    }
    p.repositories.each { r ->
      println "project repo (${p.path}): ${r.name} -> ${r.url}"
    }
  }
}
```

With `init.gradle` containing _only_ our Maven repo, we see

```
buildscript repo (:): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:react-native-gradle-plugin): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:react-native-gradle-plugin): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:settings-plugin): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:settings-plugin): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:shared): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:shared): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:shared-testutil): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:shared-testutil): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:app): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:app): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-asset): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-asset): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-constants): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-constants): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-eas-client): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-eas-client): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-file-system): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-file-system): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-font): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-font): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-json-utils): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-json-utils): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-keep-awake): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-keep-awake): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-manifests): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-manifests): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-modules-core): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-modules-core): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-structured-headers): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-structured-headers): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-updates): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-updates): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-updates-interface): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-updates-interface): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
```

With fallbacks included (with this PR)

```
buildscript repo (:): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:): Google -> https://dl.google.com/dl/android/maven2/
project repo (:): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:react-native-gradle-plugin): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:react-native-gradle-plugin): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:react-native-gradle-plugin): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:react-native-gradle-plugin): Google -> https://dl.google.com/dl/android/maven2/
project repo (:react-native-gradle-plugin): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:react-native-gradle-plugin): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:react-native-gradle-plugin): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:settings-plugin): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:settings-plugin): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:settings-plugin): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:settings-plugin): Google -> https://dl.google.com/dl/android/maven2/
project repo (:settings-plugin): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:settings-plugin): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:settings-plugin): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:shared): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:shared): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:shared): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:shared): Google -> https://dl.google.com/dl/android/maven2/
project repo (:shared): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:shared): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:shared): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:shared-testutil): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:shared-testutil): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:shared-testutil): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:shared-testutil): Google -> https://dl.google.com/dl/android/maven2/
project repo (:shared-testutil): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:shared-testutil): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:shared-testutil): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:): Google -> https://dl.google.com/dl/android/maven2/
project repo (:): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:app): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:app): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:app): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:app): Google -> https://dl.google.com/dl/android/maven2/
project repo (:app): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:app): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:app): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:expo): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:expo): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:expo): Google -> https://dl.google.com/dl/android/maven2/
project repo (:expo): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:expo): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:expo-asset): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-asset): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:expo-asset): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:expo-asset): Google -> https://dl.google.com/dl/android/maven2/
project repo (:expo-asset): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-asset): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:expo-asset): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:expo-constants): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-constants): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:expo-constants): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:expo-constants): Google -> https://dl.google.com/dl/android/maven2/
project repo (:expo-constants): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-constants): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:expo-constants): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:expo-eas-client): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-eas-client): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:expo-eas-client): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:expo-eas-client): Google -> https://dl.google.com/dl/android/maven2/
project repo (:expo-eas-client): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-eas-client): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:expo-eas-client): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:expo-file-system): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-file-system): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:expo-file-system): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:expo-file-system): Google -> https://dl.google.com/dl/android/maven2/
project repo (:expo-file-system): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-file-system): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:expo-file-system): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:expo-font): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-font): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:expo-font): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:expo-font): Google -> https://dl.google.com/dl/android/maven2/
project repo (:expo-font): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-font): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:expo-font): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:expo-json-utils): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-json-utils): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:expo-json-utils): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:expo-json-utils): Google -> https://dl.google.com/dl/android/maven2/
project repo (:expo-json-utils): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-json-utils): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:expo-json-utils): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:expo-keep-awake): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-keep-awake): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:expo-keep-awake): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:expo-keep-awake): Google -> https://dl.google.com/dl/android/maven2/
project repo (:expo-keep-awake): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-keep-awake): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:expo-keep-awake): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:expo-manifests): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-manifests): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:expo-manifests): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:expo-manifests): Google -> https://dl.google.com/dl/android/maven2/
project repo (:expo-manifests): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-manifests): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:expo-manifests): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:expo-modules-core): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-modules-core): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:expo-modules-core): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:expo-modules-core): Google -> https://dl.google.com/dl/android/maven2/
project repo (:expo-modules-core): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-modules-core): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:expo-modules-core): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:expo-structured-headers): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-structured-headers): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:expo-structured-headers): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:expo-structured-headers): Google -> https://dl.google.com/dl/android/maven2/
project repo (:expo-structured-headers): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-structured-headers): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:expo-structured-headers): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:expo-updates): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-updates): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:expo-updates): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:expo-updates): Google -> https://dl.google.com/dl/android/maven2/
project repo (:expo-updates): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-updates): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:expo-updates): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:expo-updates-interface): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:expo-updates-interface): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:expo-updates-interface): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:expo-updates-interface): Google -> https://dl.google.com/dl/android/maven2/
project repo (:expo-updates-interface): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:expo-updates-interface): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:expo-updates-interface): Google -> https://dl.google.com/dl/android/maven2/
buildscript repo (:): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
buildscript repo (:): Gradle Central Plugin Repository -> https://plugins.gradle.org/m2
buildscript repo (:): MavenRepo -> https://repo.maven.apache.org/maven2/
buildscript repo (:): Google -> https://dl.google.com/dl/android/maven2/
project repo (:): maven -> http://maven.production.caches.eas-build.internal/artifactory/libs-release
project repo (:): MavenRepo -> https://repo.maven.apache.org/maven2/
project repo (:): Google -> https://dl.google.com/dl/android/maven2/
```

You can see how repository fallbacks get configured.

---

Codex also said `repositories` does different things depending on where is it being used — settings/plugins repositories get replaced and project/buildscript get appended. It would explain the outages — settings/plugins are resolved before projects, so a single failing repository there breaks the build either way. It feels like the best course of action now is… test this live?

A reason why Gradle Scan shows something different than the Gradle script might be that Gradle script tries to print stuff as it is configured as soon as possible (`beforeSettings` and stuff). I can imagine `build.gradle`s of projects being able to add repositories to the list considered (which Gradle Scan would show).

One more thing to consider here is `RepositoriesMode`. Default value seems to be [`PREFER_PROJECT`](https://docs.gradle.org/current/kotlin-dsl/gradle/org.gradle.api.initialization.resolve/-repositories-mode/-p-r-e-f-e-r_-p-r-o-j-e-c-t/index.html) which should be causing project to use repositories declared by the project, ignoring those declared in settings. So, theoretically, all projects should ignore our Maven cache, because `build.gradle` always includes a few repositories for dependencies? The other setting is [`PREFER_SETTINGS`](https://docs.gradle.org/current/kotlin-dsl/gradle/org.gradle.api.initialization.resolve/-repositories-mode/-p-r-e-f-e-r_-s-e-t-t-i-n-g-s/index.html) which **ignores** project repositories which I think we definitely don't want.

I even wonder now if with `PREFER_PROJECT` we even use our Maven cache for build dependencies if build comes with `repositories`… May we be using our Maven cache only for buildscript/plugins dependencies which the project does _not_ change?

# Test Plan

Planning to enable Maven cache in staging and merge this. Maven cache in staging doesn't work, which means if the fallbacks work, the build will pass either way.

We should also see no decrease in Maven cache usage in Datadog.
2026-01-22 16:10:31 +01:00
Stanisław Chmiela 65413d1640 [worker] Upgrade @expo/build-tools (#3330)
https://github.com/expo/eas-build/pull/666
2026-01-22 13:12:29 +01:00
Stanisław Chmiela 71ee9469aa [worker] Bump @expo/build-tools (#3326)
https://github.com/expo/eas-build/pull/665
2026-01-21 15:25:53 +01:00
Mustafa Abban 7223a19fc8 [worker] bump build-tools 1.0.267 (#3324) 2026-01-20 17:35:36 -05:00
Mustafa Abban 3f4e826315 [worker] bump build-tools (#3321)
* [worker] bump build-tools

* rem 1.7
2026-01-19 12:57:22 -05:00
Ash Wu ea7cc6b1ea [worker] bump build-tools, eas-build-job & steps to 1.0.265 (#3315)
Signed-off-by: Ash Wu <[email protected]>
2026-01-15 20:01:10 +08:00
Mustafa Abban b5281d64d1 [worker] bump build-tools (#3313)
* [worker] bump build-tools

* [worker] bump build-tools
2026-01-14 09:33:22 -05:00
Mustafa Abban e51fe11aca [worker] bump build-tools (#3306) 2026-01-12 15:43:11 -05:00
Evan Bacon 16767f3feb feat(cli): add app clip registration for multi-target (#3300)
* bump apple utils

* Add App Clip support to iOS bundle ID registration

Detects App Clips via entitlements and registers App Clip bundle identifiers with parent app relationships. Updates related interfaces and logic to handle App Clip-specific requirements during bundle ID creation.

* Update CHANGELOG.md

* Update ensureAppExists.ts
2026-01-12 12:39:11 -08:00
Stanisław ChmielaandClaude c42b1a004c [gha] Merge setup, build, and deploy into single job (#3303)
## Why

The build-and-deploy-worker workflow had three separate jobs (setup, build-workers, deploy) with complex inter-job dependencies and redundant conditional logic. I think if we put it all in one job it should make stuff faster.

## How

Consolidated all three jobs into a single `build-and-deploy` job with conditional steps:

1. **Check**: Verify if tarballs for the current SHA exist in GCS (when deploying)
2. **Build**: Execute EAS workflow if needed (PR testing or cache miss)
3. **Upload**: Upload tarballs to GCS with SHA-based naming
4. **Deploy**: Copy tarballs to environment-specific names (when deploying)

Key simplifications:
- **Assumption**: Main branch always specifies an environment (staging/production), so `environment == ''` only occurs on PR builds
- **SHA-based caching**: Check cache by SHA regardless of branch - if the code is identical (same SHA), reuse existing tarballs
- **Clearer conditions**: Each step has a single clear purpose without overlapping branch-specific logic

I also added back building Linux tarball on Linux not to mix CPU architectures accidentally.

## Test Plan

[Build on PR](https://github.com/expo/eas-cli/actions/runs/20920297113/job/60104039095?pr=3303). [Deployment from a branch](https://github.com/expo/eas-cli/actions/runs/20921082109/job/60106516235).

Co-authored-by: Claude <[email protected]>
2026-01-12 21:21:38 +01:00
Stanisław Chmiela 8a4bf6f068 [worker] Coalesce worker builds in EAS (#3296)
# Why
- Build Android and iOS worker tarballs together in a single EAS workflow run
- Avoid split builds across different machines and simplify uploads

# How
- Update the EAS workflow to build and upload both iOS and Android tarballs
- Update the GitHub Actions workflow to run the EAS workflow once and upload both artifacts
- Cancel the EAS workflow run when the GitHub Actions job is canceled

# Test plan
- Verified auto-cancel behavior: https://github.com/expo/eas-cli/actions/runs/20849238272/job/59900072125
2026-01-12 13:26:04 +01:00
Stanisław Chmiela 03e1c18d34 [infra] Cancel EAS runs when system-tests workflow is canceled (#3298)
# Why

- Canceling the GitHub Actions workflow should cancel the underlying EAS build/workflow runs to avoid wasting resources.

# How

- Start the iOS build and Android workflow with `--no-wait` and capture their IDs from JSON.
- Poll the EAS build/workflow statuses to determine success/failure.
- Persist IDs to `GITHUB_ENV` and cancel them in a `if: cancelled()` step.

# Test Plan

https://github.com/expo/eas-cli/actions/runs/20859556010/job/59935355509
2026-01-12 12:24:21 +01:00
Ash Wu 2bf8822b1d [worker] add yarn-link script for worker/eas-build local dev (#3302)
Signed-off-by: Ash Wu <[email protected]>
2026-01-12 18:54:40 +08:00
Evan Bacon 460f98da76 fix(metadata): Handle missing Apple metadata gracefully in tasks (#3299)
* Handle missing Apple metadata gracefully in tasks

Updated Apple metadata tasks to skip operations when app version or info is not available, instead of throwing errors. This improves support for cases where the app or version has not been set up yet, particularly for metadata:pull. Tests have been updated to reflect the new behavior.

* Update CHANGELOG.md
2026-01-10 12:47:43 -08:00
Stanisław Chmiela 3a7182bb82 [infra] Add system tests to worker deployment pipeline (#3286)
This PR adds system tests that run after staging deployment and gate production deployment.

## Changes
- Add `system-tests.yml` workflow (can be dispatched manually or called from worker.yml)
- Add EAS workflow fixture for Android build
- Gate production deployment on system tests passing
- Run iOS build (`eas build`) and Android build (`eas workflow:run`) concurrently

## Testing
Once merged, the workflow can be manually dispatched to test, or it will run automatically on the next push to main that touches worker files.
2026-01-09 11:46:38 +00:00
Stanisław Chmiela f963dc711b [worker] Upgrade koa to v3 in worker (#3294)
Closes https://github.com/expo/eas-cli/pull/3293
2026-01-09 11:31:21 +01:00
Stanisław Chmiela a059852bd9 [worker] Build worker in production (#3295)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

Gives us more concurrencies for building worker (which is not a huge problem, but may be nice). Also, IF we botched worker staging, we won't break the whole pipeline (system tests won't let worker deploy to production so worker in production will still work and let us rebuild worker).

# How

Changed app ID and token to expo.dev ones.

# Test Plan

CI ran ok.
2026-01-09 11:17:26 +01:00
Stanisław Chmiela 33e0b0901c Bump TypeScript to 5.5.4 and @types/node to 20.14.8 (#3291)
## Summary

Upgrades TypeScript and `@types/node` across the monorepo to align versions and prepare for future package consolidation.

### Changes

| Package | TypeScript | @types/node |
|---------|-----------|-------------|
| Root | 5.3.3 → 5.5.4 | 20.11.0 → 20.14.8 |
| eas-build-cache-provider | 5.3.3 → 5.5.4 | - |
| eas-cli | 5.3.3 → 5.5.4 | - |
| eas-json | 5.3.3 → 5.5.4 | - |
| worker | 5.4.5 → 5.5.4 | (unchanged at 20.14.8) |

### TypeScript 5.5 Highlights

- Inferred type predicates for cleaner code
- Better control flow narrowing for indexed accesses
- JSDoc `@import` tag support

### Breaking Changes Reviewed

- ✅ No enum members using `Infinity`, `-Infinity`, or `NaN`
- ✅ No issues with mapped type behavior changes
- ✅ No issues with stricter `lib.d.ts` definitions

### Testing

- ✅ `yarn install` - passes
- ✅ `yarn build` - all packages build successfully
- ✅ `yarn test` - all 1221 tests pass
2026-01-09 10:41:55 +01:00
Stanisław Chmiela 8ba877ab81 [worker] Simplify tests (#3285)
# Why

The worker integration tests were slow and required platform-specific CI runners (macOS for iOS, Linux for Android) because they attempted to build real React Native apps. This made the CI pipeline complex, slow, and resource-intensive.

# How

- **Replace full-build tests with fast mocked tests**: Instead of running actual Android/iOS builds, we now mock `@expo/build-tools` at the library boundary. This lets us test the worker's WebSocket communication, job dispatch, state sync, abort handling, and hanging worker detection without the overhead of real builds.

- **Add new `jobExecution.test.ts`**: A new integration test that verifies the worker accepts jobs, executes them (via mocked builders), and returns success/error responses correctly.

- **Update existing tests**: `abortBuild.test.ts`, `stateSync.test.ts`, and `possiblyHangingWorker.test.ts` now mock `@expo/build-tools` instead of the worker's internal `build.ts` module, allowing more of the real code path to execute.

- **Simplify CI**: Consolidated the separate `worker-android-checks` and `worker-ios-checks` jobs into a single `worker-checks` job running on `ubuntu-latest`. Removed dependencies on `direnv`, `setup-gcloud`, and platform-specific scripts.

- **Cleanup**: Deleted unused files including `__mocks__/build.ts`, macOS-specific Jest config, CI shell scripts, and the `direnv` internal action.

The build-build part is going to be tested in system tests running in CI on `main`.

# Test Plan

cd packages/worker
yarn test:integrationAll 19 tests pass in ~6 seconds (down from minutes with real builds).

```bash
cd packages/worker
yarn test:integration
```

All 19 tests pass in ~6 seconds (down from minutes with real builds).
2026-01-08 16:11:06 +01:00
Stanisław Chmiela 9f6e387888 [worker] Bump packages (#3289)
Lands https://github.com/expo/turtle-v2/commit/df16c54eb74583fd2ac6effbe0ccec9010ae6525.
2026-01-08 16:10:46 +01:00
Stanisław Chmiela c10691c339 [infra] Build worker on EAS instead of GHA's macos-latest (#3287)
## Why

The `worker.yml` GitHub Actions workflow was recently moved from a private repository to this public one. This means we lost access to private GitHub Actions runners and now have to wait in long queues for public `macos-latest` runners when building the iOS worker tarball.

## How

Instead of building the iOS worker tarball on GitHub Actions `macos-latest`, we now:

1. **Trigger an EAS Workflow** from GitHub Actions that builds the tarball on EAS's macOS runners
2. **Upload the tarball as an EAS artifact** using `eas/upload_artifact`
3. **Uploads the artifact to tarball bucket** from GitHub Actions

### Changes

- Added `packages/worker/app.json` - minimal Expo app config required for EAS
- Added `packages/worker/eas.json` - EAS configuration
- Added `packages/worker/.eas/workflows/build-worker.yml` - EAS Workflow that builds the iOS worker tarball and uploads it as an artifact
- Updated `.github/workflows/build-and-deploy-worker.yml`:
  - Split the matrix build into separate `build-android` and `build-ios` jobs
  - `build-ios` now runs on `ubuntu-latest` and triggers the EAS Workflow using `eas workflow:run --ref --wait --json`
  - Extracts the artifact download URL from the workflow output and streams it directly to GCS

## Test Plan

https://github.com/expo/eas-cli/actions/runs/20796579975 Don't mind the CI failure on the PR, I dispatched the workflow manually later.
2026-01-08 11:16:17 +01:00
Stanisław Chmiela 155b65c8fc [chore\ Add basic CLAUDE.mds (#3284)
Used Claude Code to generate CLAUDE.mds. I hope they help with AI "startup".
2026-01-07 15:15:40 +01:00
Stanisław Chmiela cdbc95295c [worker] Add worker from turtle-v2 (#3268)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

We want to streamline DX of the worker part of EAS. Part of it is colocating all code in one repository.

# How

Copied code over to `eas-cli`. Adjusted code — each commit represents a single piece of work towards adjusting code to this repository. The main changes are around removing dependency on `turtle-v2` internal libraries.

One thing to figure out still is deployment process. I've added `build-and-deploy-worker` which is capable of deploying to staging and production. The question is how to automate it. In `turtle-v2` we have system tests. Should we just copy them over to here? (Don't like that.) Should we streamline system tests now and instead of having this weird `turtle-v2-example` repo, create project on the go? Should we deploy worker manually for now?

I think I'd rather deploy manually for now?

# Test Plan

Deployed worker to staging. Ran system tests. They succeeded!! https://github.com/expo/turtle-v2/actions/runs/20042956545/job/57535011098
2026-01-07 11:44:26 +01:00
Quinlan Jung 🦥 414ccb92c0 [eas-cli] delete channel in background (#3278)
* [eas-cli] delete channel in background

* Temporary Commit at 1/5/2026, 3:02:00 PM
2026-01-05 15:09:32 -08:00
Douglas Lowder aabf519a9d eas init should fix and validate project name/slug before creating a project on the server (#3277)
# Why

The CLI does not validate the project name and slug before creating the project on the server, leading to unnecessary GraphQL errors.

# How

Copy the server's self-contained validation code to the CLI. Eventually, this code should move to `eas-build` repo for use by both CLI and server. Add code to run the same validation checks that the server GraphQL resolver uses, and add a method to strip invalid characters from the project name before suggesting it at the prompt.

# Test Plan

The server's validation unit tests have been added here as well.
2025-12-18 10:50:21 -08:00
Douglas Lowder 88d9627286 Allow exit from credentials command (#3269)
# Why

The command `eas credentials` does not provide a way to exit other than control-C, if no platform flag is passed in.

# How

Add an "Exit" choice to the platform prompt.

# Test Plan

Tested locally.
2025-12-03 14:44:21 -08:00
Will Schurman 🦦 59ce6d0b65 [ci] Test with node 24 (#3270)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

Node 24 is in LTS, it's time to officially support it and test for it.

# How

Add 24 to CI test matrix.

# Test Plan

CI
2025-12-02 14:48:13 -08:00
HarelSultan 8e64a9c73e feat: add --runtime-version and --platform filters to eas update:list (#3261) 2025-12-02 11:08:18 -08:00
Expo CI 4476c87962 update CHANGELOG.md 2025-11-20 19:58:01 +00:00
Expo CI 18ecd0844a v16.28.0 2025-11-20 19:39:20 +00:00
Colin MacKenzie cc3e7cdbe4 remove package json changes (#3263) 2025-11-20 13:36:15 -06:00
Colin MacKenzie aa7fecb9fe [eas-cli] Fix usage warning bug in eas:build (#3260)
* fix overages bug

* stub changelog

* update pr number

* fix test
2025-11-20 13:24:00 -06:00
Douglas Lowder c73a3e3a1e Update GraphQL from server, fix type error in test (#3262)
# Why

GraphQL test started failing today after a recent server change in updates GraphQL queries. Failed test run at https://github.com/expo/eas-cli/actions/runs/19529909252

# How

- Updated GraphQL generated code
- Fixed a TS type error in one test

# Test Plan

- CI should pass
2025-11-20 11:18:21 -08:00
Quinlan Jung 🦥 37ca2ebd55 [ENG-17987][eas-cli] delete branch in background (#3258)
* [eas-cli] delete branch in background

* Temporary Commit at 11/18/2025, 4:28:38 PM
2025-11-18 16:33:40 -08:00
Colin MacKenzie dead11964e [eas-cli] Sort organization accounts first in eas:new (#3256)
* sort accounts

* update changelog

* fix failing test
2025-11-17 13:44:50 -06:00
Expo CI b66d31d7d7 update CHANGELOG.md 2025-11-13 02:21:00 +00:00
Expo CI ed8170047b v16.27.0 2025-11-13 02:14:02 +00:00
Quinlan Jung 🦥 0e4056ba82 [eas-cli] kick off update patch generation if enabled (#3250)
* [eas-cli] kick off update patch generation if enabled

* Temporary Commit at 11/5/2025, 3:09:27 PM

* Temporary Commit at 11/5/2025, 3:19:31 PM

* Temporary Commit at 11/5/2025, 3:25:14 PM

* Temporary Commit at 11/11/2025, 2:06:29 PM

* Temporary Commit at 11/11/2025, 2:51:21 PM

* Temporary Commit at 11/11/2025, 2:53:51 PM

* Temporary Commit at 11/11/2025, 4:27:57 PM

* Temporary Commit at 11/12/2025, 5:31:14 PM
2025-11-12 18:05:53 -08:00
Kudo Chien 6182ea5a1d [eas-cli] remove plugins evaluation from workflow:run (#3254) 2025-11-12 01:46:16 +08:00
Douglas Lowder c9e7b649ad Fix workflow:status when run ID passed in (#3253)
# Why

In `workflow:status`, the argument name for the passed-in workflow run ID is incorrect. Instead of showing the status of that run, it shows the dialog to select from recent runs.

# How

Fixed the argument name.

# Test Plan

Works when testing locally.
2025-11-10 01:55:11 -08:00
Quinlan Jung 🦥 7a6b796242 [eas-cli] fix yarn watch (#3251)
* [eas-cli] fix yarn watch

* Temporary Commit at 11/6/2025, 12:22:09 PM
2025-11-06 12:37:12 -08:00
Kadi Kraman 804eb9a326 [eas-cli] Fix updating file env vars with env:update --non-interactive (#3249) 2025-11-05 12:12:40 +00:00
Expo CI 8da14f0101 update CHANGELOG.md 2025-10-29 19:26:44 +00:00
Expo CI dc62d22816 v16.26.0 2025-10-29 19:17:21 +00:00
Colin MacKenzie 258c625611 [eas-cli] eas:new - remove hidden tag; update output and project README (#3243)
* fix typo in the readme, change output formatting

* remove hidden flag

* update readme

* remove dev code
2025-10-29 14:10:30 -05:00
Douglas Lowder e84340774d New command workflow:status (#3242)
# Why

Shows the status of a workflow run that has already started.

If the run is in progress, and the `--wait` flag is set, the command will show periodic updates on running jobs, the current job steps, and recent log lines, similarly to `eas workflow:run --wait`.

Otherwise, the command immediately returns with information on the run result, exactly as `eas workflow:run --wait` will show at the end of a run. If the run failed, the failed jobs, failed steps, and last few log lines are shown.

```bash
USAGE
  $ eas workflow:status [WORKFLOW_RUN_ID] [--non-interactive] [--wait] [--json]

ARGUMENTS
  WORKFLOW_RUN_ID  A workflow run ID.

FLAGS
  --json             Enable JSON output, non-JSON messages will be printed to stderr.
  --non-interactive  Run the command in non-interactive mode.
  --[no-]wait        Wait for workflow run to complete. Defaults to false.

DESCRIPTION
  show the status of an existing workflow run. If no run ID is provided, you will be prompted to select from recent
  workflow runs for the current project.

FLAG DESCRIPTIONS
  --[no-]wait  Wait for workflow run to complete. Defaults to false.

    Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
```

# How

Leverages utility code already written for `eas workflow:run` and `eas workflow:logs`.

# Test Plan

- Tested on running and completed workflow runs.
- CI should pass.
2025-10-29 12:00:07 -07:00
Expo CI fbc23f7a7d update CHANGELOG.md 2025-10-28 15:05:45 +00:00
Expo CI 97255ec90d v16.25.1 2025-10-28 13:14:43 +00:00
Phil Pluckthun cfccc87b82 [eas-cli] Remove preview log output from EAS Hosting commands (#3241)
* Remove preview labels from EAS Hosting commands

* Add CHANGELOG entry
2025-10-27 22:25:44 +00:00
Expo CI 09b4f8367e update CHANGELOG.md 2025-10-27 16:55:04 +00:00
Expo CI 45481f9758 v16.25.0 2025-10-27 15:57:32 +00:00
Colin MacKenzie 1119008317 [eas-cli] Display usage warnings on eas:build (#3236)
* first pass

* cleanup

* clean up query

* show warning when builds are close to threshold

* cleaned up query

* tweak test

* more test tweaks
2025-10-27 10:34:40 -05:00
Douglas Lowder 123a0c1b7e Remove outdated update-local-plugin from release workflow (#3240)
# Why

The manual release workflow is broken after #3237, since the local plugin version is no longer hardcoded, but tied to the `@expo/eas-build-job` dependency.

# How

Remove this step from the workflow.

# Test Plan

Workflow should succeed in dry run mode.
2025-10-27 05:51:52 -07:00
Douglas Lowder 2f21fa62b1 New workflow to move latest-eas-build tag (#3233)
# Why

Provide a way to move the "latest-eas-build" version tag added in the EAS CLI release process. After https://github.com/expo/universe/pull/22530, this is the EAS CLI version that will be used by EAS workflows.

# Test Plan

Test with different version inputs, ensure failures are handled correctly.
2025-10-27 04:32:34 -07:00
Colin MacKenzieandDouglas Lowder 8ad449c6b7 [eas-cli] New command - incorporate more user feedback (#3235)
* incorporated feedback

* more feedback

* add changelog entry

* prompt for name

* update agents.md

* fix test

* add PR to changelog

* Update CHANGELOG.md

Co-authored-by: Douglas Lowder <[email protected]>

* remove dev warning

---------

Co-authored-by: Douglas Lowder <[email protected]>
2025-10-24 10:08:43 -05:00
Douglas Lowder fe8ec50806 Add missing env for local builds with latest plugin (#3237)
# Why

Currently, the CLI is using an old version of `eas-cli-local-build-plugin` for builds with `eas build --local`. This is not ideal, as this means that local builds are not using the most recent `build-tools` and other packages.

# How

- Modify `local.ts` so that the version of the plugin matches the version of `@expo/eas-build-job` used by the CLI
- Add some missing environment variables that are required in the most recent releases of `@expo/build-tools`

# Test Plan

- Testing with local builds on my dev machine
- CI should pass
2025-10-23 14:08:22 -07:00
Douglas Lowder 86cefeb259 workflow:run can use --wait and --json together (#3239)
# Why

Customers would like to wait for a workflow run to complete, and get JSON output.

# How

Remove the restriction on --wait and --json flags together, and ensure that when --json is enabled, all messages other than the final JSON result will go to stderr.

# Test Plan

- Tested locally by running workflows
- CI should pass
2025-10-23 12:00:41 -07:00
Kadi Kraman 034e0a1e7f [eas-json] Add default values for android track and releaseStatus (#3229) 2025-10-20 17:55:27 +01:00
Expo CI a6b776915f update CHANGELOG.md 2025-10-19 21:51:50 +00:00
Expo CI 4ae52d0e3d v16.24.1 2025-10-19 21:45:41 +00:00
Douglas Lowder f62720ef5a fix (regression): revert ora dependency change (#3231) 2025-10-19 14:42:20 -07:00
Expo CI af09aaedb3 update CHANGELOG.md 2025-10-19 20:05:21 +00:00
Expo CI 184616e874 v16.24.0 2025-10-19 19:59:27 +00:00
Douglas Lowder 6bb09c4e99 feat: workflow:run --wait should show status updates (#3227)
# Why

If a user executes `eas workflow:run --wait`, it would be desirable to show status updates for the individual jobs and steps in the workflow while the workflow run is in progress.

# How

Added code that leverages some of the same utilities used by `workflow:logs`. 

During the run, the logs for the various workflow jobs are retrieved and parsed, and the progress of each job is shown in the text of a spinner, showing the current step and the last few lines of the step's logs.

If the workflow run fails, the output will show jobs that failed, along with the failed step for each, and any logs for the failed steps.

Other changes:

- Bumped the "ora" package to the latest version, to pick up bug fixes
- Removed deprecated Node 18 from test runs
- Refactored the code in `workflow:run` that parses inputs, moving that code to a file in `commandUtils`
- Modified the Zod type for inputs to align with server side changes in https://github.com/expo/universe/pull/21950

# Test Plan

- Testing with different workflows
- CI should pass
2025-10-19 12:52:46 -07:00
Douglas Lowder 81add86c61 fix: workflow:logs should show build and repack logs (#3226)
# Why

Build and repack jobs do not have logs at the same URL as other workflow jobs, so no logs were showing for these job types.

# How

- Update BuildFragment to include the log file URL
- Check for build jobs, and add the correct queries and fetch calls for those
- Update and fix tests
- Bump `@expo/eas-build-job` to 1.0.239 to fix TS error in environment type

# Test Plan

- Tested with various workflow runs, including ones with custom, build, repack, and update jobs.
- CI should pass.
2025-10-16 14:05:26 -07:00
Expo CI 4bb1c784d8 update CHANGELOG.md 2025-10-16 15:42:51 +00:00
Expo CI 322082c28b v16.23.1 2025-10-16 15:32:16 +00:00
Colin MacKenzieandDouglas Lowder 5db1d21aa5 [eas-cli] Incorporating feedback on eas new command (#3205)
* incorporating feedback

* small cleanup

* moved some files around

* moved tests

* remove unused line

* tweak one line

* unit tests for all the helper functions

* more tests pass

* last batch of tests for now

* update changelog

* fix changelog

* move src/new to src/commandUtils/new

* pr feedback

* update changelog

* update path in copy-new-templates command

* fix test import

* Update packages/eas-cli/package.json

Co-authored-by: Douglas Lowder <[email protected]>

* feat: add --package-manager flag to project:new command

- Add --package-manager (-p) flag with options: npm, yarn, pnpm, bun
- Default to npm when flag not provided
- Remove promptForPackageManagerAsync call
- Add corresponding tests

* feat: add --no-install flag to skip dependency installation

- Add --no-install flag to skip installing dependencies
- Update README with installation instructions when --no-install is used
- Pass skipInstall flag through to updateReadmeAsync
- Add corresponding tests

* feat: add hideOutput option to suppress dependency installation logs

- Add hideOutput parameter to runCommandAsync to use stdio: 'ignore'
- Update installDependenciesAsync to hide npm/yarn/pnpm/bun output
- Show only spinner during installation instead of verbose logs
- Apply hideOutput to all dependency installation steps
- Update tests to reflect flag definition changes

* revert: remove --no-install flag

Remove the --no-install flag and related skipInstall functionality as requested

* feat: remove prompts and use smart defaults for project name and directory

- Remove project name prompt, use smart fallback logic:
  - Default: new-expo-project
  - If exists: new-expo-project-USERNAME-DATE
  - If exists: new-expo-project-SHORTID (using nanoid)

- Remove target directory prompt, use cwd/projectName as default

- Refactor naming to reflect generation vs prompting:
  - Rename prompts.ts → configs.ts
  - Combine generateDefaultProjectNameAsync + promptForProjectNameAsync → generateProjectNameAsync
  - Rename promptForTargetDirectoryAsync → generateDirectoryAsync
  - Rename promptForConfigsAsync → generateConfigsAsync
  - Rename promptedConfigs → initialConfigs

- Add nanoid and fs-extra dependencies for smart defaults
- Update all tests to reflect new behavior (11 tests in configs.test.ts passing)
- All existing tests continue to pass

* refactor: simplify CLI args for project:new command

- Remove --name flag, always use smart generation logic
- Change --directory flag to positional 'path' argument
  - Defaults to current working directory
  - If no slashes: treated as subdirectory of cwd
  - If relative path with slashes: resolved from cwd
  - If absolute path: used as-is

- Update generateProjectNameAsync to remove optional name parameter
- Update generateDirectoryAsync to handle path logic
- Add 4 new tests for path handling scenarios
- Remove 1 test for name arg (no longer supported)
- All 17 tests passing

* more feedback

* one more big refactor

* more cleanup

* one last round of cleanup

* one last round of cleanup

* fix changelog

* fix tests

---------

Co-authored-by: Douglas Lowder <[email protected]>
2025-10-16 10:20:22 -05:00
Expo CI 0db5585b20 update CHANGELOG.md 2025-10-14 19:13:02 +00:00
Expo CI 10ec54345a v16.23.0 2025-10-14 15:53:39 +00:00
Kadi Kraman 27b4710264 [eas-cli] [eas-json] support custom environment variable environments (#3220) 2025-10-14 13:56:42 +01:00
Kadi Kraman 4132e92da5 Add tests for eas env:push --force (#3223) 2025-10-14 11:31:36 +01:00
Giorgio FellipeandKadi Kraman 264340a717 Add --force parameter to env:push (#3135)
Co-authored-by: Kadi Kraman <[email protected]>
2025-10-14 10:40:22 +01:00
Expo CI 1d09c2c97d update CHANGELOG.md 2025-10-13 20:55:14 +00:00
Expo CI 72cff1df1a v16.22.0 2025-10-13 20:47:55 +00:00
Douglas Lowder 6912e453ef Improvements to workflow:create (#3194)
# Why

The existing `workflow:create` command simply creates a very bare-bones workflow, that is not very useful to developers who need to know how to fully use workflows for managing builds, updates, and other EAS operations.

# How

- Define four different workflow templates, initialized as JSON objects.
- After the user selects which type of workflow (build, update, deploy, custom), sanity checks are made, EAS build and EAS update are configured if needed, and any missing build profiles are created in `eas.json`.
- Once the user has made their selections, the final workflow YAML is dynamically generated and written to disk with an explanatory header.
- After writing the workflow, next steps are provided for any further configuration that may be needed, especially if build credentials or app store credentials are needed.

In future, we can easily add more templates that use other workflow job types (fingerprint, repack, etc.).

# Test Plan

- Tested against different projects
- CI should pass
2025-10-13 13:42:00 -07:00
Expo CI 839cbac47b update CHANGELOG.md 2025-10-13 15:45:46 +00:00
Expo CI 5ba16dbc67 v16.21.0 2025-10-13 15:31:06 +00:00
Phil Pluckthun 896b456dd3 [eas-cli] Pass _expo/.routes.json for SSG exports as routes.json to Hosting deployments (#3211)
* Derive `routes.json` payload from static export `_expo/.routes.json`

* Add CHANGELOG entry
2025-10-10 15:22:32 +01:00
Kadi Kraman f2c0202f06 Add tests for the env commands (#3217) 2025-10-09 15:18:43 +01:00
Expo CI 497bac1cbf update CHANGELOG.md 2025-10-08 05:36:29 +00:00
Expo CI 5b9734b60b v16.20.4 2025-10-08 05:25:57 +00:00
Kadi Kraman 3ac70d8977 Switch to lowercase in exec and pull (#3216) 2025-10-08 06:23:06 +01:00
Douglas Lowder 32adc788d0 Support workflow:run from a Git ref (#3203)
# Why

Add a `--ref` flag so that `eas workflow:run` can initiate a workflow run from an existing ref in the project's GitHub repository.

- Avoids the need to upload the entire project from a local machine
- Ensures that the workflow starts from a known reference point in the project's source control, with no unexpected local changes

# How

- If the ref flag is set, the run command will ensure that the ref is valid, and that the workflow can be used for that ref (using the new mutation for this from https://github.com/expo/universe/pull/22132)
- If the ref flag is set, the run command starts the run from that ref (using https://github.com/expo/universe/pull/20161)
- If the user chooses ref = HEAD, `git rev-parse` is called to convert that to a commit
- The flow is designed to work the same for running from GitHub ref as running from local, so inputs and waiting for results are supported 

# Test Plan

- Tested in an existing project with workflows with and without inputs
- CI should pass
2025-10-07 15:06:24 -07:00
Expo CI e465ed57cb update CHANGELOG.md 2025-10-07 20:04:20 +00:00
Expo CI df819fc232 v16.20.3 2025-10-07 18:42:28 +00:00
Kadi Kraman 539e201972 Lower instead of uppercase environment (#3213) 2025-10-07 19:39:06 +01:00
Expo CI c14b3e179b update CHANGELOG.md 2025-10-07 15:23:38 +00:00
Expo CI f093197dea v16.20.2 2025-10-07 13:29:54 +00:00
Kadi Kraman 13834039a8 [eas-cli] Use local EnvironmentVariableEnvironment enum (#3208) 2025-10-07 14:18:07 +01:00
Expo CI 4d3023f7f1 update CHANGELOG.md 2025-10-03 15:51:00 +00:00
Expo CI eb7d3b5ecd v16.20.1 2025-10-03 15:14:03 +00:00
Stanisław Chmiela 8c721759bd [chore] Remove FingerprintSource (#3200)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

In https://github.com/expo/eas-build/pull/604 I removed `fingerprintSource` from metadata passed to a build (1). It was previously used to inform the build process of, well, the fingerprint source so it can optionally diff current fingerprint and fingerprint calculated by the `eas build` command.

In `eas build`, the fingerprint would get calculated and uploaded. Then it was included in the build mutation as `{ type: GCS, bucketKey }` (2). Worker expected that somewhere in the process of starting a job, something (`www` or `turtle-v2`) would swap out the persisted `fingerprintSource` to `{ type: URL, url }` (because worker can't download arbitrary GCS objects).

In https://github.com/expo/eas-build/pull/587 I changed the flow so that now worker does not expect any changes to the job or metadata and instead fetches the fingerprint sources straight from `www`.

This made `fingerprintSource` in `metadata` obsolete (see (1)). However, we still need to be able to send fingerprint sources information to `www` (see (2)).

# How

This pull request:
- bumps `@expo/eas-build-job` (removing `FingerprintSource` type)
- `FingerprintSource` gets replaced with `FingerprintSourceInput` generated by GraphQL schema
- this lets us remove `transformFingerprintSource` (it's being passed around in its final form)

# Test Plan

Set up my staging test app to use runtime version policy = fingerprint, installed and configured `expo-updates`, [ran a build](https://staging.expo.dev/accounts/sjchmiela/projects/staging-app/builds/7a44f4c3-e448-4657-a8f3-71e24a2a29c8).

I had a few `console.log`s in place:

<img width="943" height="779" alt="Zrzut ekranu 2025-09-30 o 17 39 14" src="https://github.com/user-attachments/assets/2b117a17-8c0a-4445-bf56-27f93a590f87" />

and the build has fingerprint:

<img width="1256" height="161" alt="Zrzut ekranu 2025-09-30 o 17 39 33" src="https://github.com/user-attachments/assets/b6133402-9910-42d2-b351-836ae9c10c95" />

Before I installed node dependencies, fingerprint was being set to `file:fingerprint` (probably from https://github.com/expo/expo/pull/26901, see https://github.com/expo/eas-cli/blob/b447194c8521270ba8b551296c4329c3edb7aa1c/packages/eas-cli/src/project/resolveRuntimeVersionAsync.ts#L88).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Replaces internal FingerprintSource with GraphQL FingerprintSourceInput across build/update flows, removes transformFingerprintSource, adjusts metadata wiring, and bumps related packages.
> 
> - **Build & GraphQL**:
>   - Replace `Metadata.fingerprintSource` usage with GraphQL `FingerprintSourceInput`; update builders to accept `BuildMetadataInput` and pass pre-transformed metadata.
>   - Remove `transformFingerprintSource` and associated imports/usages; simplify `transformMetadata` to accept `{ metadata, fingerprintSource }`.
>   - Update types for fingerprint source across build paths (`computeAndMaybeUpload*`, `sendBuildRequestAsync`, Android/iOS build senders).
> - **Update/Publish**:
>   - Stop transforming fingerprint source in `update publish`; pass through GraphQL `FingerprintSourceInput`.
>   - Adjust runtime fingerprint computation/assembly to use `FingerprintSourceInput`.
> - **Fingerprint upload**:
>   - `maybeUploadFingerprintAsync` now returns `FingerprintSourceInput`; use GraphQL enum casing (`Gcs`).
>   - Mutation `createFingerprintAsync` accepts `FingerprintSourceInput`.
> - **Dependencies**:
>   - Bump `@expo/eas-build-job` and `@expo/steps`; update yarn.lock accordingly.
> 
> <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5a61fdee68f3b0bbda043b8f3160c4c2f1ea3a71. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2025-10-03 13:06:42 +02:00
Vojtech Novak c6a741bb8c [eas-cli] skip syncing capabilities if they don't contain 'enabled' attribute (#3201) 2025-10-01 23:56:44 +02:00
Expo CI af5b76fe9f update CHANGELOG.md 2025-09-30 15:55:00 +00:00
Expo CI aa428b7571 v16.20.0 2025-09-30 15:39:44 +00:00
Colin MacKenzie ba68f40922 [eas-cli] Create new command to configure a new project (#3196)
* incorporate pr feedback

* playing around with adding build

* create expo project

* clean up getAccountChoices

* update unit test

* removed some unused code

* wip

* working on test files

* fix previews, add more end comments

* small tweaks to tests

* update changelog
2025-09-30 10:32:12 -05:00
Stanisław Chmiela 1380b023ba [eas-cli] Deprecate isGlobal GraphQL input property of environment variables (#3190)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

See https://github.com/expo/universe/pull/22108.

# How

- Updated GraphQL code.
- Adjusted code to the updated GraphQL code (no `isGlobal`).
- Updated types in `EnvironmentVariableMutation` so they don't re-implement what GraphQL is generating for us.
- Updated types in `env:push`.

# Test Plan

Adjusted tests.
2025-09-25 18:42:44 +02:00
Colin MacKenzie adfc79abd4 [eas-cli] Scaffolding out the new command (#3177)
* scaffolding out new command

* remove a newline

* incorporate pr feedback

* add unit test for package manager functions

* get rid of unnecessary lockfile git add

* remove unused test
2025-09-23 10:36:23 -05:00
Douglas Lowder 92e6ecd03c Improvements to workflow:validate (#3165)
# Why

Improve the validation of workflow files:

- Add client-side checks for valid syntax, and compatibility with the user's EAS build profiles
- Make validation usable with future improvements to `workflow:create`

# How

- New `validation.ts` in `commandUtils` with the various validation checks separated cleanly into different functions
- `workflow:validate` command refactored to use the new utility and wrap errors
- The new code reads the workflow schema from `<ExpoAPIBaseURL>/v2/workflows/schema`
- For testing purposes only, a local copy of the schema can be used by setting EXPO_TESTING_WORKFLOW_SCHEMA_PATH
- An existing check in `workflowFile.ts` has been modified to remove the requirement for a Github repository, as that is no longer required to run workflows

# Test Plan

- Tested against different workflow templates, and against workflows in a real project.
- Tested against the www server from the main branch running locally, to verify recent server side changes (e.g. https://github.com/expo/universe/pull/21950)
2025-09-19 02:45:58 -07:00
Expo CI 05546f0bfd update CHANGELOG.md 2025-09-17 06:43:24 +00:00
Expo CI 6c5c99b5d1 v16.19.3 2025-09-17 06:36:33 +00:00
Brent Vatne f27ea08839 [cli] Temporarily remove warnings when no compatible build found, need to refine the message (#3182) 2025-09-16 23:21:34 -07:00
Expo CI 15f47aea81 update CHANGELOG.md 2025-09-15 19:24:30 +00:00
Expo CI fd23f031b6 v16.19.2 2025-09-15 19:16:48 +00:00
Douglas Lowder a74c434718 Add artifact URLs to workflow:view (#3175)
# Why

To allow CLI users to get artifacts of builds or workflow jobs directly using the CLI, and make the URLs available in JSON output also (for easier use by automation scripts).

# How

- Add artifact info to WorkflowJobFragment
- Add this info to the results of `workflow:view`
- For workflow jobs that are of type=BUILD, use the existing build query to get the build artifact URLs for the build and show those as well
- Make some improvements to the formatting of workflow job outputs

# Test Plan

- Tested against different EAS projects
- CI should pass
2025-09-15 11:42:09 -07:00
Expo CI 29950a7629 update CHANGELOG.md 2025-09-12 11:54:08 +00:00
Expo CI 0fd33a95f5 v16.19.1 2025-09-12 11:48:48 +00:00
Stanisław Chmiela 843ed0d0aa [eas-cli] Upgrade Zod to v4 (#3173)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

Unify packages versions across the Expo package ecosystem (Zod, eas-build-job)

# How

Copied Zod definitions from `www` to `run.ts` where we are parsing workflow to get inputs definitions.

# Test Plan

There are, fortunately, tests for this part of the code.
2025-09-12 13:38:49 +02:00
Expo CI bf2cde45c6 update CHANGELOG.md 2025-09-12 10:35:59 +00:00
Expo CI ecab803cfa v16.19.0 2025-09-12 10:29:03 +00:00
Stanisław Chmiela 570ff126a0 [eas-cli] Allow freeform Android release track (#3163)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[exponent-internal.slack.com/archives/C011ZMXQ6SZ/p1755530483378679](https://exponent-internal.slack.com/archives/C011ZMXQ6SZ/p1755530483378679)

# How

Updated GraphQL code which removed `SubmissionAndroidTrack` enum (it's a scalar now). Adjusted code to pass track around as string.

Removed `AndroidReleaseTrack` from `eas-json`. Updated `eas.schema.json` to allow any string as `track`.

Technically, to support `track: string` we'll need to update `eas-json` in `www`. I think it's ok to merge this, publish releases and then update `eas-json` in `www`. Alternatively we could publish `eas-json` from this branch as prerelease, bump in `www` and then merge this. This would be more _correct_, but I think it's ok to have this 1 hour of unannounced discrepancy between `www` and `eas-cli` (users not being able to use custom tracks _yet_). 

# Test Plan


Tested along with all the pull requests in the mix.

<img width="372" height="90" alt="Zrzut ekranu 2025-09-9 o 17 03 38" src="https://github.com/user-attachments/assets/0b260ad2-2379-4597-aa1c-dae8bf86446d" />
<img width="243" height="72" alt="Zrzut ekranu 2025-09-9 o 16 59 37" src="https://github.com/user-attachments/assets/4eef353d-ac7e-4b16-9413-a304a574b685" />
<img width="449" height="110" alt="Zrzut ekranu 2025-09-9 o 17 00 40" src="https://github.com/user-attachments/assets/58a44b7a-16bb-46b4-8d98-f233c196406a" />
<img width="366" height="85" alt="Zrzut ekranu 2025-09-9 o 17 00 35" src="https://github.com/user-attachments/assets/71013cd6-5f5e-4e38-951c-008642fbee8c" />
2025-09-12 12:08:56 +02:00
Expo CI e24b21bd1e update CHANGELOG.md 2025-09-10 11:09:48 +00:00
Expo CI 20da7f8778 v16.18.1 2025-09-10 10:54:43 +00:00
Stanisław Chmiela 14e002274f [eas-cli] Support unknown Android release tracks in submit:internal (#3170)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

Preparation for merging the rest of `track`-related pull requests.

Currently, the only place fetching `track` from `www` for display is `submit:internal` command. There, we fetch the submission config to know what parameters to supply to `fastlane supply` in the Submit workflow job.

Currently, we're using a mapping of `{ PRODUCTION: production, INTERNAL: internal, ... }` to transform the GraphQL value (enum key) to config value ("enum value").

# How

Since we're planning to make `androidConfig.track` be a plain string (e.g. `internal`), we should fall back to the actual value if the mapping does not produce a match.

This currently should not change anything (`track` is guaranteed to be a GraphQL enum value). This is a preparation for landing of https://github.com/expo/universe/pull/21827 which (somewhat breakily, but we expect to be the only consumers of our GraphQL API) changes `track` to be any string.

This `track` value is being used [here](https://github.com/expo/universe/blob/2f143aa8d51821c7e1c306768787c46b18bdd544/server/www/src/data/entities/workflow/job/WorkflowSubmitJob.ts#L399-L403).

# Test Plan

This is currently a no-op. Tested along with all the pull requests in the mix.

<img width="372" height="90" alt="Zrzut ekranu 2025-09-9 o 17 03 38" src="https://github.com/user-attachments/assets/0b260ad2-2379-4597-aa1c-dae8bf86446d" />
<img width="243" height="72" alt="Zrzut ekranu 2025-09-9 o 16 59 37" src="https://github.com/user-attachments/assets/4eef353d-ac7e-4b16-9413-a304a574b685" />
<img width="449" height="110" alt="Zrzut ekranu 2025-09-9 o 17 00 40" src="https://github.com/user-attachments/assets/58a44b7a-16bb-46b4-8d98-f233c196406a" />
<img width="366" height="85" alt="Zrzut ekranu 2025-09-9 o 17 00 35" src="https://github.com/user-attachments/assets/71013cd6-5f5e-4e38-951c-008642fbee8c" />
2025-09-10 12:37:37 +02:00
Douglas Lowder 3b8dc6b7aa CLI debug log should respect DEBUG=eas:* (#3162)
# Why

It would be great if we could use the environment setting `DEBUG=eas:*` to enable debug logs when running the EAS CLI, similarly to the way `DEBUG=expo:*` is used in the Expo CLI.

This is a first step towards implementing that. Ideally, different commands and utility modules would set different tags for the debug log messages, but this will at least allow the debug logs to appear for existing `Log.debug()` calls.

# How

Modify `src/log.ts` to write debug logs if DEBUG is set to a regex that matches `eas:log:debug`.

# Test Plan

CI should pass.
2025-09-05 11:02:56 -07:00
Douglas Lowder 445a48a8dd Sync GraphQL schema and generated code with server schema (#3161)
# Why

Sync up the schema to reflect recent additions to the server GraphQL schema

# How

`yarn --cwd packages/eas-cli verify-graphql-code`, then inspect changes in schema and generated code.

# Test Plan

CI should pass
2025-09-05 11:02:45 -07:00
Douglas Lowder f6436319ed Add better description to update:rollback (#3154)
# Why

Make it easier for customers to use `eas update:rollback` in CI.

# How

Add description text on the underlying commands that do support the non-interactive flag.
2025-08-28 12:52:15 -07:00
Expo CI bd1e32622d update CHANGELOG.md 2025-08-27 18:02:36 +00:00
Expo CI 74dcb40caa v16.18.0 2025-08-27 17:44:02 +00:00
Jon Samp f094146dcd exposes channel:delete as command (#3150) 2025-08-25 09:33:49 -05:00
Vojtech Novak 3784745382 chore: include available profiles in 'Missing build profile' error (#3141)
* chore: include available profiles in 'Missing build profile' error

* address review
2025-08-13 13:44:21 +02:00
Ash Wu 72be7e80bf [eas-cli] fix broken docs links (#3140)
* [eas-cli] fix broken docs links

Signed-off-by: Ash Wu <[email protected]>

* fix eslint

Signed-off-by: Ash Wu <[email protected]>

---------

Signed-off-by: Ash Wu <[email protected]>
2025-08-13 12:05:36 +02:00
Douglas Lowder cf1b8cb5eb Show fingerprints in build:view and build:list commands (#3137) 2025-08-07 09:12:02 -07:00
Stanisław Chmiela b6df8d3a57 [chore] Update codemention.yml (#3136) 2025-08-07 17:36:18 +02:00
Kadi Kraman b96a262cd4 [eas-cli] Only make EXPO_PUBLIC_ env vars plain text (#3121) 2025-07-31 12:40:40 +01:00
Douglas Lowder 3df84f28aa Use correct token in release trigger workflow (#3125) 2025-07-30 18:19:29 -07:00
Expo CI 554f411fae update CHANGELOG.md 2025-07-31 00:44:50 +00:00
Expo CI e6ce9837e6 v16.17.4 2025-07-31 00:39:29 +00:00
Douglas Lowder 30d8a0ce99 Adjust release workflow so Lerna detects version changes (#3124)
# Why

The existing release trigger workflow does not obtain the GItHub history and tags, so that Lerna cannot correctly determine which package versions to bump.

# How

Set `fetch-depth: 0` in the checkout action, and do an explicit fetch of all tags.

# Test Plan

- After merge, a dry run of the release trigger should correctly bump only changed packages.
2025-07-30 16:45:53 -07:00
Douglas Lowder a9c5907230 Fix release script (#3123)
# Why

The new release GitHub workflow has an error because the release script needs to be run without prompts:

```
Checking if eas-cli-local-build-plugin upgrade is available
eas-cli-local-build-plugin is already on the latest version - 1.0.171
lerna notice cli v8.2.3
lerna info ci enabled
lerna info current version 16.17.3
lerna info Assuming all packages changed
Changes:
 - eas-build-cache-provider: 16.4.2 => 16.17.4
 - eas-cli: 16.17.3 => 16.17.4
 - @expo/eas-json: 16.13.2 => 16.17.4
error Command failed with exit code 130.
? Are you sure you want to create these versions? (ynH) info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
Error: Process completed with exit code 130.
```
# How

- Added the `--yes` flag to the script commands
- Instead of failing when run locally, the script now executes the release flow in "dry run" mode, without committing, tagging, or pushing changes.
- Add workflow input for dry run mode (true by default, must be manually set to false to actually release)
- Get full Git history before running release script, as required for correct Lerna version check

# Test Plan

- Test locally
- Run the workflow in dry run mode after merging
- Attempt a release after this PR is merged
2025-07-30 16:07:20 -07:00
Kadi Kraman ad0f94aca9 [ci] Build eas-json before typecheck (#3122)
* Build eas-json

* Ensure check gets run when the script changes
2025-07-30 12:38:07 -07:00
Douglas Lowder bf66b79126 New workflow to trigger releases (#3119)
# Why

The current release script requires that the user executing the script have permission to push changes to main without any PR requirement or checks.

# How

To allow us to add PR requirements and checks, I have added a manual GitHub workflow so that the existing script is executed by the expo-bot user, which is exempted from PR requirements.

I have also made a few adjustments to the `release.yml` workflow (remove incorrect release name, update Node version to 22).

# Test Plan

- CI should pass
- Try a release after this PR is merged
2025-07-30 09:27:58 -07:00
Will Schurman 🦦 dcc5d62800 [eas-cli] Fix lint (#3116) 2025-07-28 10:40:53 -07:00
Will Schurman 🦦 878ed19f0c Update expoSsoLauncher.ts 2025-07-28 10:35:15 -07:00
Nitzan Raz 0e17e415b7 [TINY] SSO Login: print browser url
Summary:
I'm working in a weird setup where the browser won't launch from cli.
This required me to visit the url manually from my browser.
To get the url, I had to add a `console.log` to the cli's code directly.
Figured this change will make it easier for similar users to login, without exposing sensitive data

Test Plan:
Tested manually on the compiled code, not the best
2025-07-27 17:47:39 +03:00
Expo CI d4c5a4e1e4 update CHANGELOG.md 2025-07-23 22:12:02 +00:00
Douglas Lowder 3776c95f00 v16.17.3 2025-07-23 15:08:48 -07:00
1091 changed files with 150606 additions and 21058 deletions

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "eas-cli"
[setup]
script = '''
mise trust
mise install
yarn
yarn typecheck
'''
+7 -1
View File
@@ -1,8 +1,14 @@
**/build/**
**/dist/**
**/coverage/**
**/fixtures/**
!packages/eas-cli/src/commands/build/**
!packages/eas-cli/src/build/**
packages/eas-cli/src/graphql/generated.ts
packages/eas-cli/schema/**
packages/eas-cli/graphql.schema.json
packages/build-tools/schema.graphql
node_modules
ts-declarations
ts-declarations
-44
View File
@@ -1,44 +0,0 @@
module.exports = {
root: true,
extends: ['universe/node'],
plugins: ['async-protect', 'node'],
rules: {
'no-console': 'warn',
'no-constant-condition': ['warn', { checkLoops: false }],
'sort-imports': [
'warn',
{
ignoreDeclarationSort: true,
},
],
curly: 'warn',
'import/no-cycle': 'error',
'import/no-extraneous-dependencies': [
'error',
{ devDependencies: ['**/__tests__/**/*', '**/__mocks__/**/*'] },
],
'import/no-relative-packages': 'error',
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'lodash',
message: "Don't use lodash, it's heavy!",
},
],
},
],
'no-underscore-dangle': ['error', { allow: ['__typename'] }],
'async-protect/async-suffix': 'error',
'node/no-sync': 'error',
},
overrides: [
{
files: ['**/__tests__/**/*.ts', '**/__mocks__/**/*.ts'],
rules: {
'async-protect/async-suffix': 'off',
},
},
],
};
+11
View File
@@ -0,0 +1,11 @@
name: setup-mise
description: Install tools via mise
runs:
using: "composite"
steps:
- uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3.5.1
with:
install: true
cache: true
experimental: true
+7 -1
View File
@@ -3,7 +3,7 @@ commentConfiguration:
epilogue: 'Generated by [CodeMention](https://github.com/expo/expo/actions/workflows/codemention.yaml)'
rules:
- patterns: ['**/*']
mentions: ['sjchmiela']
mentions: ['douglowder']
- patterns: ['packages/eas-cli/src/update/**']
mentions: ['wschurman', 'quinlanj']
- patterns: ['packages/eas-cli/src/commands/update/**']
@@ -22,3 +22,9 @@ rules:
mentions: ['kitten', 'kadikraman']
- patterns: ['packages/eas-cli/src/commands/deploy/**']
mentions: ['kitten', 'kadikraman']
- patterns: ['packages/eas-cli/src/commands/build/**']
mentions: ['sjchmiela']
- patterns: ['packages/eas-cli/src/build/**']
mentions: ['sjchmiela']
- patterns: ['packages/eas-cli/src/commands/workflow/**']
mentions: ['sjchmiela']
+16
View File
@@ -0,0 +1,16 @@
version: 2
updates:
# Keep SHA-pinned GitHub Actions fresh. Without this, pins never receive
# upstream security patches because tag movements no longer reach us.
- package-ecosystem: github-actions
directories:
- /
- /.github/actions/setup-mise
- /.github/internal-actions/setup-gcloud
- /.github/internal-actions/notify-slack-on-fail-or-recover
schedule:
interval: weekly
groups:
actions:
patterns:
- '*'
@@ -0,0 +1,19 @@
name: build-android
on:
workflow_dispatch: {}
jobs:
build:
name: Build Android
type: build
runs_on: linux-large
params:
platform: android
profile: system-test
custom_job:
name: Custom Job
runs_on: linux-medium-nested-virtualization
steps:
- name: Echo
run: echo "This is a custom job step"
- uses: eas/start_android_emulator
@@ -12,7 +12,7 @@ runs:
using: 'composite'
steps:
- name: Get previous workflow run status
uses: actions/github-script@v6
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
id: run-status
with:
script: |
@@ -28,7 +28,7 @@ runs:
- name: Send Slack Success Notification
if: fromJSON(steps.run-status.outputs.result).previous == 'failure' && fromJSON(steps.run-status.outputs.result).current == 'success'
uses: rtCamp/action-slack-notify@v2
uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 # v2
env:
SLACK_CHANNEL: ${{ inputs.channel }}
SLACK_COLOR: good
@@ -41,7 +41,7 @@ runs:
- name: Send Slack Failure Notification
if: fromJSON(steps.run-status.outputs.result).current == 'failure'
uses: rtCamp/action-slack-notify@v2
uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 # v2
env:
SLACK_CHANNEL: ${{ inputs.channel }}
SLACK_COLOR: danger
@@ -0,0 +1,16 @@
name: setup-gcloud
runs:
using: "composite"
steps:
- name: Auth gcloud
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
with:
workload_identity_provider: 'projects/321830142373/locations/global/workloadIdentityPools/github/providers/expo'
project_id: exponentjs
- name: Setup gcloud
uses: google-github-actions/setup-gcloud@e427ad8a34f8676edf47cf7d7925499adf3eb74f # v2
with:
version: 548.0.0
project_id: exponentjs
@@ -0,0 +1,205 @@
name: build-and-deploy-worker
# Assumes main branch always specifies an environment (staging/production).
# If environment is empty: PR testing build (no upload, no deploy)
# If environment is set: Build (unless cached on main), upload, and deploy
on:
workflow_dispatch:
inputs:
environment:
description: '"staging", "production" or empty'
type: string
required: true
workflow_call:
inputs:
environment:
description: '"staging", "production" or empty'
type: string
required: true
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: ./.github/actions/setup-mise
- uses: ./.github/internal-actions/setup-gcloud
- name: Check if worker already exists
id: check_worker_exists
# Check if tarballs for this SHA already exist
# If they do, we can skip building regardless of branch
if: inputs.environment != ''
run: |
for platform in android ios; do
GCP_TARBALL="worker-$platform-sha1-$GITHUB_SHA.tar.gz"
if ! gsutil stat gs://eas-build-worker-tarballs/$GCP_TARBALL; then
echo "worker_exists=false" >> "$GITHUB_OUTPUT"
exit 0
fi
done
echo "worker_exists=true" >> "$GITHUB_OUTPUT"
- name: Start EAS Workflow to build worker tarballs
id: eas_workflow
# Build if: PR testing (no env), or deploying but tarballs don't exist
if: inputs.environment == '' || steps.check_worker_exists.outputs.worker_exists == 'false'
working-directory: packages/worker
env:
EXPO_TOKEN: ${{ secrets.EXPO_DEV_EXPO_GITHUB_ROBOT_ACCESS_TOKEN }}
run: |
set -euo pipefail
# Only upload artifacts from EAS if GHA will upload them to GCS
# This avoids unnecessary artifact uploads for PR builds
SHOULD_UPLOAD_ARTIFACT="${{ inputs.environment != '' && steps.check_worker_exists.outputs.worker_exists == 'false' }}"
# Run workflow and capture output (includes run ID)
WORKFLOW_OUTPUT=$(npx -y eas-cli@latest workflow:run build-worker.yml \
--ref ${{ github.sha }} \
--input upload_artifact=$SHOULD_UPLOAD_ARTIFACT \
--non-interactive \
--no-wait \
--json)
WORKFLOW_RUN_ID=$(echo "$WORKFLOW_OUTPUT" | jq -r '.id')
if [ -z "$WORKFLOW_RUN_ID" ] || [ "$WORKFLOW_RUN_ID" = "null" ]; then
echo "Error: Could not determine workflow run ID"
echo "Workflow output:"
echo "$WORKFLOW_OUTPUT" | jq .
exit 1
fi
echo "workflow_run_id=$WORKFLOW_RUN_ID" >> "$GITHUB_OUTPUT"
- name: Wait for EAS workflow and capture artifacts
id: eas_artifacts
if: inputs.environment == '' || steps.check_worker_exists.outputs.worker_exists == 'false'
working-directory: packages/worker
env:
EXPO_TOKEN: ${{ secrets.EXPO_DEV_EXPO_GITHUB_ROBOT_ACCESS_TOKEN }}
run: |
set -euo pipefail
WORKFLOW_RUN_ID="${{ steps.eas_workflow.outputs.workflow_run_id }}"
if [ -z "$WORKFLOW_RUN_ID" ]; then
echo "Error: Missing workflow run ID"
exit 1
fi
SHOULD_UPLOAD_ARTIFACT="${{ inputs.environment != '' && steps.check_worker_exists.outputs.worker_exists == 'false' }}"
while true; do
VIEW_OUTPUT=$(npx -y eas-cli@latest workflow:view "$WORKFLOW_RUN_ID" \
--non-interactive \
--json)
STATUS=$(echo "$VIEW_OUTPUT" | jq -r '.status')
case "$STATUS" in
SUCCESS)
break
;;
FAILURE)
echo "EAS workflow failed."
echo "$VIEW_OUTPUT" | jq .
exit 1
;;
CANCELED)
echo "EAS workflow canceled."
echo "$VIEW_OUTPUT" | jq .
exit 1
;;
ACTION_REQUIRED)
echo "EAS workflow requires action."
echo "$VIEW_OUTPUT" | jq .
exit 1
;;
NEW|IN_PROGRESS)
sleep 7
;;
*)
echo "Unexpected workflow status: $STATUS"
echo "$VIEW_OUTPUT" | jq .
exit 1
;;
esac
done
# Skip artifact extraction if we didn't request uploads
if [ "$SHOULD_UPLOAD_ARTIFACT" == "false" ]; then
echo "Skipping artifact download - upload was disabled for this run"
exit 0
fi
# Extract artifact download URLs from workflow output
# Artifacts are in jobs[].artifacts[] for custom jobs
DOWNLOAD_IOS_URL=$(echo "$VIEW_OUTPUT" | jq -r '
.jobs[]
| .artifacts[]?
| select(.name == "worker-ios-tarball")
| .downloadUrl
' | head -1)
DOWNLOAD_ANDROID_URL=$(echo "$VIEW_OUTPUT" | jq -r '
.jobs[]
| .artifacts[]?
| select(.name == "worker-android-tarball")
| .downloadUrl
' | head -1)
if [ -z "$DOWNLOAD_IOS_URL" ] || [ "$DOWNLOAD_IOS_URL" = "null" ]; then
echo "Error: Could not find iOS artifact download URL"
echo "$VIEW_OUTPUT" | jq .
exit 1
fi
if [ -z "$DOWNLOAD_ANDROID_URL" ] || [ "$DOWNLOAD_ANDROID_URL" = "null" ]; then
echo "Error: Could not find Android artifact download URL"
echo "$VIEW_OUTPUT" | jq .
exit 1
fi
# Mask the download URLs
echo "::add-mask::$DOWNLOAD_IOS_URL"
echo "::add-mask::$DOWNLOAD_ANDROID_URL"
echo "download_ios_url=$DOWNLOAD_IOS_URL" >> "$GITHUB_OUTPUT"
echo "download_android_url=$DOWNLOAD_ANDROID_URL" >> "$GITHUB_OUTPUT"
- name: Cancel EAS workflow on GitHub cancellation
if: cancelled()
working-directory: packages/worker
env:
EXPO_TOKEN: ${{ secrets.EXPO_DEV_EXPO_GITHUB_ROBOT_ACCESS_TOKEN }}
run: |
set -euo pipefail
WORKFLOW_RUN_ID="${{ steps.eas_workflow.outputs.workflow_run_id }}"
if [ -n "$WORKFLOW_RUN_ID" ]; then
npx -y eas-cli@latest workflow:cancel "$WORKFLOW_RUN_ID" --non-interactive
fi
- name: Upload worker tarballs to GCP
# Upload when deploying if we built something new
# Don't upload PR builds (no environment) or when using cached tarballs
if: inputs.environment != '' && steps.check_worker_exists.outputs.worker_exists == 'false'
run: |
IOS_GCP_TARBALL="worker-ios-sha1-$GITHUB_SHA.tar.gz"
ANDROID_GCP_TARBALL="worker-android-sha1-$GITHUB_SHA.tar.gz"
curl -L "${{ steps.eas_artifacts.outputs.download_ios_url }}" | gsutil cp - gs://eas-build-worker-tarballs/$IOS_GCP_TARBALL
curl -L "${{ steps.eas_artifacts.outputs.download_android_url }}" | gsutil cp - gs://eas-build-worker-tarballs/$ANDROID_GCP_TARBALL
- name: Promote worker to ${{ inputs.environment }}
if: inputs.environment != ''
run: |
for platform in android ios; do
GCP_TARBALL="worker-$platform-sha1-$GITHUB_SHA.tar.gz"
WORKER_STAGING_TARBALL="worker-$platform-${{ inputs.environment }}.tar.gz"
gsutil cp gs://eas-build-worker-tarballs/$GCP_TARBALL gs://eas-build-worker-tarballs/$WORKER_STAGING_TARBALL
done
+4 -3
View File
@@ -1,6 +1,7 @@
name: CHANGELOG.md PR check
on:
workflow_dispatch:
pull_request:
types:
- opened
@@ -17,7 +18,7 @@ jobs:
name: Check CHANGELOG.md updated
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2
- name: Check if CHANGELOG.md was updated
id: changelog-updated
uses: tj-actions/changed-files@b1ba699b304f2083b602164e06a89b868c84f076
@@ -25,11 +26,11 @@ jobs:
files: CHANGELOG.md
- name: Fail if CHANGELOG.md was not updated and the "no changelog" label is absent
if: steps.changelog-updated.outputs.any_changed == 'false' && !contains(github.event.pull_request.labels.*.name, 'no changelog')
uses: actions/github-script@v6
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
with:
script: |
core.setFailed('Please add a changelog entry!')
- uses: mshick/add-pr-comment@v2.1.0
- uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2
if: always()
with:
message-id: changelog-entry-check
+7 -1
View File
@@ -1,5 +1,8 @@
name: codemention
on:
# pull_request_target is required so fork PRs get a write token to post the
# mention comment. Safe because there is no checkout — the action only reads
# PR metadata via the GitHub API; no fork code is fetched or executed.
pull_request_target:
types: [opened, synchronize, ready_for_review]
jobs:
@@ -9,6 +12,9 @@ jobs:
contents: read
pull-requests: write
steps:
- uses: tobyhs/[email protected]
# Pinned to a commit SHA (not the v1.4.0 tag) because this runs under
# pull_request_target with a write-scoped token — a re-pointed tag from
# a compromised upstream account would give the attacker repo write access.
- uses: tobyhs/codemention@bb6bfb2c3ff1e6fee7ee37006bbee6d114057225 # v1.4.0
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
+5 -5
View File
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-20.04
if: "${{ contains(github.event.label.name, 'incomplete issue: missing or invalid repro') }}"
steps:
- uses: actions/github-script@v3
- uses: actions/github-script@ffc2c79a5b2490bd33e0a41c1de74b877714d736 # v3
with:
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
script: |
@@ -52,7 +52,7 @@ jobs:
runs-on: ubuntu-20.04
if: "${{ contains(github.event.label.name, 'incomplete issue: missing info') }}"
steps:
- uses: actions/github-script@v3
- uses: actions/github-script@ffc2c79a5b2490bd33e0a41c1de74b877714d736 # v3
with:
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
script: |
@@ -81,7 +81,7 @@ jobs:
runs-on: ubuntu-20.04
if: github.event.label.name == 'issue accepted'
steps:
- uses: actions/github-script@v3
- uses: actions/github-script@ffc2c79a5b2490bd33e0a41c1de74b877714d736 # v3
with:
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
script: |
@@ -98,7 +98,7 @@ jobs:
runs-on: ubuntu-20.04
if: "${{ contains(github.event.label.name, 'invalid issue: question') }}"
steps:
- uses: actions/github-script@v3
- uses: actions/github-script@ffc2c79a5b2490bd33e0a41c1de74b877714d736 # v3
with:
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
script: |
@@ -126,7 +126,7 @@ jobs:
runs-on: ubuntu-20.04
if: "${{ contains(github.event.label.name, 'invalid issue: feature request') }}"
steps:
- uses: actions/github-script@v3
- uses: actions/github-script@ffc2c79a5b2490bd33e0a41c1de74b877714d736 # v3
with:
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
script: |
+64
View File
@@ -0,0 +1,64 @@
on:
workflow_dispatch:
inputs:
staging_only:
type: boolean
required: true
default: false
description: "Only move the latest-eas-build-staging tag, not the latest-eas-build tag"
version:
type: string
required: true
description: "Version number to move the tag to"
dry_run:
type: boolean
required: true
default: true
description: "Run the workflow in dry run mode, without making any NPM changes"
name: Move NPM tags used by EAS builds and workflows
jobs:
move-tag:
name: Move NPM tags used by EAS builds and workflows
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.EXPO_BOT_PAT }}
NPM_ACCESS_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
INPUT_VERSION: ${{ github.event.inputs.version }}
INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }}
INPUT_STAGING_ONLY: ${{ github.event.inputs.staging_only }}
steps:
- uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 # v2
with:
registry-url: "https://registry.npmjs.org/"
scope: "expo"
node-version: 22
- name: Resolve version (fails if the version does not exist)
run: |
set -o pipefail
npm pack --dry-run eas-cli@$INPUT_VERSION
- name: Add latest-eas-build-staging tag
run: |
cmd="npm dist-tag add eas-cli@$INPUT_VERSION latest-eas-build-staging"
if [[ "$INPUT_DRY_RUN" == "true" ]]; then
echo "Running in dry run mode, skipping version check"
echo "cmd = $cmd"
exit 0
fi
echo //registry.npmjs.org/:_authToken=$NPM_ACCESS_TOKEN > ~/.npmrc
$cmd
- name: Add latest-eas-build tag
run: |
cmd="npm dist-tag add eas-cli@$INPUT_VERSION latest-eas-build"
if [[ "$INPUT_DRY_RUN" == "true" ]]; then
echo "Running in dry run mode, skipping version check"
echo "cmd = $cmd"
exit 0
fi
if [[ "$INPUT_STAGING_ONLY" == "true" ]]; then
echo "Only moving the staging tag, skipping the production tag"
exit 0
fi
echo //registry.npmjs.org/:_authToken=$NPM_ACCESS_TOKEN > ~/.npmrc
$cmd
+25 -155
View File
@@ -1,7 +1,7 @@
on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
name: Release workflow
@@ -9,197 +9,67 @@ jobs:
release:
name: Create a release
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- id: create_release
uses: actions/create-release@v1
uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: 'eas-cli@${{ env.EAS_CLI_VERSION }}'
release_name: ${{ github.ref }}
draft: true
build-linux:
name: Build for Linux
runs-on: ubuntu-latest
needs: release
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 20
- run: yarn install --frozen-lockfile --check-files
- run: yarn build
- name: Ensure GraphQL schema and generated code is up-to-date
working-directory: ${{ github.workspace }}/packages/eas-cli
run: |
yarn verify-graphql-code
- name: Build tarballs
working-directory: ${{ github.workspace }}/packages/eas-cli
run: |
yarn pretarball-ci
yarn oclif pack:tarballs --no-xz --targets linux-x64,linux-arm
- id: x64
run: echo "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- id: arm
run: echo "filename=$(ls eas-*-arm.tar.gz)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- name: Upload linux-x64 tarball
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
asset_name: ${{ steps.x64.outputs.filename }}
asset_content_type: application/gzip
- name: Upload linux-arm tarball
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/${{ steps.arm.outputs.filename }}
asset_name: ${{ steps.arm.outputs.filename }}
asset_content_type: application/gzip
build-mac:
name: Build for macOS
# Use macOS to get the correct platform specific dependencies
runs-on: macos-latest
needs: release
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 20
- run: brew install python-setuptools
env:
HOMEBREW_NO_AUTO_UPDATE: 1
- run: yarn install --frozen-lockfile --check-files
- run: yarn build
- name: Build tarballs
working-directory: ${{ github.workspace }}/packages/eas-cli
run: |
yarn pretarball-ci
yarn oclif pack:tarballs --no-xz --targets darwin-x64,darwin-arm64
- id: x64
run: echo "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- id: arm64
run: echo "filename=$(ls eas-*-arm64.tar.gz)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- name: Upload darwin-x64 tarball
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
asset_name: ${{ steps.x64.outputs.filename }}
asset_content_type: application/gzip
- name: Upload darwin-arm64 tarball
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/${{ steps.arm64.outputs.filename }}
asset_name: ${{ steps.arm64.outputs.filename }}
asset_content_type: application/gzip
build-windows:
name: Build for Windows
runs-on: ubuntu-latest
needs: release
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 20
- run: yarn install --frozen-lockfile --check-files
- run: sudo apt-get install nsis
- run: yarn build
- run: |
yarn pretarball-ci
yarn oclif pack:win
working-directory: ${{ github.workspace }}/packages/eas-cli
- id: x64
run: echo "filename=$(ls eas-*-x64.exe)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
- id: x86
run: echo "filename=$(ls eas-*-x86.exe)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
- name: Upload x64 installer
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/win32/${{ steps.x64.outputs.filename }}
asset_name: ${{ steps.x64.outputs.filename }}
asset_content_type: application/gzip
- name: Upload x86 installer
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/win32/${{ steps.x86.outputs.filename }}
asset_name: ${{ steps.x86.outputs.filename }}
asset_content_type: application/gzip
publish-to-npm:
name: Publish to npm
runs-on: ubuntu-latest
needs: [build-linux, build-mac, build-windows]
needs: release
permissions:
id-token: write
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
registry-url: 'https://registry.npmjs.org/'
scope: 'expo'
node-version: 20
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: ./.github/actions/setup-mise
- name: Install dependencies
run: yarn install --frozen-lockfile --check-files
run: yarn install --immutable
- name: Build
run: yarn build
- name: Publish packages to npm
run: yarn lerna publish from-package --yes --no-verify-access
run: yarn run -T lerna publish from-package --yes --no-verify-access
- name: Add latest-eas-build-staging tag
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
- name: Add latest-eas-build-staging tag
run: |
VERSION=$(cat lerna.json | jq -r .version)
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
npm dist-tag add eas-cli@$VERSION latest-eas-build-staging
- name: Add latest-eas-build tag
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
- name: Add latest-eas-build tag
run: |
VERSION=$(cat lerna.json | jq -r .version)
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
npm dist-tag add eas-cli@$VERSION latest-eas-build
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
release-changelog:
name: Update changelog and publish release
runs-on: ubuntu-latest
needs: publish-to-npm
permissions:
contents: write
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/setup-node@v2
with:
node-version: 20
- uses: ./.github/actions/setup-mise
- name: Install dependencies
working-directory: ./scripts
run: yarn install --frozen-lockfile --check-files
run: yarn install --immutable
- name: Resolve version
run: echo "EAS_CLI_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Update changelog
working-directory: ./scripts
run: yarn --silent release-changelog $EAS_CLI_VERSION > /tmp/current-changelog
run: yarn release-changelog $EAS_CLI_VERSION > /tmp/current-changelog
- name: Publish release
run: gh release edit v$EAS_CLI_VERSION --draft=false -F /tmp/current-changelog
env:
@@ -211,7 +81,7 @@ jobs:
git config --global user.email "[email protected]"
git config --global user.name "Expo CI"
git add CHANGELOG.md
git commit -m "update CHANGELOG.md"
git commit --allow-empty -m "update CHANGELOG.md"
git push
- name: Prepare changelog for Slack
run: |
@@ -221,13 +91,13 @@ jobs:
cat /tmp/changelog.slack.md >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Slack Notification
uses: rtCamp/action-slack-notify@v2
uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 # v2
env:
SLACK_CHANNEL: eas-cli
SLACK_COLOR: ${{ job.status }}
SLACK_ICON: https://github.com/expo-bot.png?size=48
SLACK_TITLE: 'Released eas-cli@${{ env.EAS_CLI_VERSION }}'
SLACK_TITLE: "Released eas-cli@${{ env.EAS_CLI_VERSION }}"
SLACK_MESSAGE: ${{ env.SLACK_CHANGELOG }}
SLACK_USERNAME: 'EAS CLI CI'
SLACK_USERNAME: "EAS CLI CI"
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_FOOTER: ''
SLACK_FOOTER: ""
-23
View File
@@ -1,23 +0,0 @@
name: Size
on:
pull_request:
types: [synchronize, opened]
concurrency:
group: size-${{ github.ref }}
cancel-in-progress: true
jobs:
compare:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 20
- uses: preactjs/compressed-size-action@v2
with:
build-script: 'build:tarballs:linux'
repo-token: '${{ secrets.GITHUB_TOKEN }}'
pattern: './packages/eas-cli/dist/*.tar.gz'
compression: 'none'
strip-hash: "v\\d+.\\d+.\\d+-\\w+-"
clean-script: 'clean'
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
close-issues:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4
- uses: actions/stale@a20b814fb01b71def3bd6f56e7494d667ddf28da # v4
with:
ascending: false
operations-per-run: 300
+4 -9
View File
@@ -5,9 +5,6 @@ on:
branches: [main]
pull_request:
branches: [main]
paths:
- .github/workflows/test-graphql.yml
- packages/eas-cli/src/**
schedule:
- cron: '0 8 * * *' # 8:00 AM UTC time every day
@@ -20,11 +17,9 @@ jobs:
name: Ensure GraphQL schema and generated code is up-to-date
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 22
- run: yarn install --frozen-lockfile
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2
- name: Setup tools
uses: ./.github/actions/setup-mise
- run: yarn install --immutable
- run: yarn verify-graphql-code
working-directory: packages/eas-cli
+4 -6
View File
@@ -21,10 +21,8 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 20
- run: yarn install --frozen-lockfile --check-files
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2
- name: Setup tools
uses: ./.github/actions/setup-mise
- run: yarn install --immutable
- run: yarn typecheck
+38 -15
View File
@@ -15,40 +15,63 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: ['18', '20', '22']
node: ['20', '22', '24']
include:
- node: '20'
coverage: true
name: Test with Node ${{ matrix.node }}
steps:
- uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- run: yarn install --frozen-lockfile --check-files
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2
- name: Setup tools
uses: ./.github/actions/setup-mise
env:
MISE_NODE_VERSION: ${{ matrix.node }}
- name: Install dependencies
run: yarn install --immutable
env:
YARN_ENABLE_HARDENED_MODE: ${{ matrix.coverage == true && '1' || '0' }}
- run: yarn typecheck
- run: |
git config --global user.email "[email protected]"
git config --global user.name "Test"
- run: yarn test
if: ${{ !matrix.coverage }}
- name: Fetch base commit for PR diffing
if: ${{ github.event_name == 'pull_request' }}
# Lerna needs the base commit to compute affected packages.
run: git fetch --no-tags --depth=1 origin ${{ github.event.pull_request.base.sha }}
- name: Run tests (affected)
# PRs: only run tests for changed packages + dependents.
if: ${{ !matrix.coverage && github.event_name == 'pull_request' }}
run: yarn run -T lerna run test --since ${{ github.event.pull_request.base.sha }} --include-dependents
env:
NODE_OPTIONS: --max-old-space-size=8192
- run: yarn test --coverage
- name: Run tests (all)
# Pushes to main: run all package tests.
if: ${{ !matrix.coverage && github.event_name == 'push' }}
run: yarn run -T lerna run test
env:
NODE_OPTIONS: --max-old-space-size=8192
- name: Run tests (coverage)
# Coverage on PRs and main; per-package reports are uploaded to Codecov.
if: ${{ matrix.coverage }}
run: yarn run -T lerna run test -- --coverage
env:
# Oclif seems to cause OOM errors when testing with coverage enabled.
# Remove this whenever we solved those OOM errors.
NODE_OPTIONS: --max-old-space-size=8192
- run: yarn lint --max-warnings=0
- run: yarn install --frozen-lockfile --check-files
- run: yarn lint
- run: yarn fmt:check
- name: Install scripts dependencies
if: ${{ matrix.coverage }}
run: yarn install --immutable
working-directory: ./scripts
- uses: codecov/codecov-action@v1
env:
YARN_ENABLE_HARDENED_MODE: ${{ matrix.coverage == true && '1' || '0' }}
- uses: codecov/codecov-action@29386c70ef20e286228c72b668a06fd0e8399192 # v1
if: ${{ matrix.coverage }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
- run: yarn lint-changelog
- if: ${{ matrix.coverage }}
run: yarn lint-changelog
notify-slack:
runs-on: ubuntu-latest
@@ -56,7 +79,7 @@ jobs:
name: Notify Slack
if: ${{ github.ref == 'refs/heads/main' && always() }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2
- name: Notify Slack
uses: ./.github/internal-actions/notify-slack-on-fail-or-recover
with:
+46
View File
@@ -0,0 +1,46 @@
on:
workflow_dispatch:
inputs:
version:
type: string
required: false
description: 'Version number for the release'
dry_run:
type: boolean
required: true
default: true
description: 'Run the release in dry run mode, without pushing any changes'
name: Manually trigger a release
jobs:
release:
name: Trigger a release
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.EXPO_BOT_PAT }}
INPUT_VERSION: ${{ github.event.inputs.version }}
INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: main
fetch-depth: 0
token: ${{ secrets.EXPO_BOT_PAT }}
- uses: ./.github/actions/setup-mise
- run: |
# Lerna will not detect changes correctly without having all tags
git fetch --depth=1 origin +refs/tags/*:refs/tags/*
# Checkout the main branch
git checkout main
git config --global user.email "[email protected]"
git config --global user.name "Expo CI"
yarn install --immutable
yarn build
if [[ "$INPUT_VERSION" == "" ]]; then
echo "Releasing with default version"
yarn release
else
echo "Releasing with version $INPUT_VERSION"
yarn release $INPUT_VERSION
fi
@@ -1,81 +0,0 @@
name: Open a PR bumping eas-build modules
on: workflow_dispatch
jobs:
open-pr:
name: Open a PR bumping eas-build modules
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.EXPO_BOT_PAT }}
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 20
- run: yarn install --frozen-lockfile
- name: Configure git
run: |
git config --global user.email "[email protected]"
git config --global user.name "Expo Bot"
- name: Upgrade eas-build modules
run: |
for DIR in packages/eas-cli packages/eas-json; do
pushd $DIR >/dev/null 2>&1
# packages are ordered according to output of `lerna list --toposort`
# https://github.com/expo/turtle-v2/pull/568
for PACKAGE in "@expo/downloader" "@expo/eas-build-job" "@expo/logger" "@expo/template-file" "@expo/steps" "@expo/turtle-spawn" "@expo/build-tools"; do
if grep -q $PACKAGE package.json; then
echo "[$DIR] yarn --exact add $PACKAGE"
yarn add --exact $PACKAGE
fi
done
popd >/dev/null 2>&1
done
- name: Set a branch name
id: bump-branch-name
run: |
HASH="$(echo $RANDOM | md5sum | head -c 10)"
BRANCH_NAME="@expo-bot/eas-build-bump-$HASH"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Create a branch, commit, and push
run: |
BRANCH_NAME="${{ steps.bump-branch-name.outputs.BRANCH_NAME }}"
git checkout -b $BRANCH_NAME
git add .
git commit -m "Upgrade eas-build dependencies"
git push origin $BRANCH_NAME
- name: Open a PR to main
uses: repo-sync/pull-request@v2
id: open-pr
with:
source_branch: '${{ steps.bump-branch-name.outputs.BRANCH_NAME }}'
destination_branch: 'main'
pr_title: 'Upgrade eas-build dependencies'
pr_body: ':arrow_up: Upgrades [`eas-build`](https://github.com/expo/eas-build) dependencies.'
pr_reviewer: 'sjchmiela'
github_token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/github-script@v3
with:
github-token: ${{ secrets.EXPO_BOT_PAT }}
script: |
github.issues.createComment({
issue_number: ${{ steps.open-pr.outputs.pr_number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: "/changelog-entry chore Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies."
})
+160
View File
@@ -0,0 +1,160 @@
name: system-tests
on:
workflow_dispatch: {}
workflow_call: {}
jobs:
system-tests:
runs-on: ubuntu-latest
env:
EXPO_TOKEN: ${{ secrets.STAGING_EXPO_DEV_EXPO_SERVICES_GITHUB_ROBOT_ACCESS_TOKEN }}
EXPO_STAGING: "1"
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: ./.github/actions/setup-mise
- name: Create test app
run: npx -y create-expo-app@latest ${{ runner.temp }}/test-app --template tabs
- name: Link to EAS project
working-directory: ${{ runner.temp }}/test-app
run: npx -y eas-cli@latest init --id 6ae8028d-4e6d-4912-8eb2-87d4e6122880 --force --non-interactive
- name: Add bundle identifiers to app.json
working-directory: ${{ runner.temp }}/test-app
run: |
jq '.expo.ios.bundleIdentifier = "dev.expo.workersystemtests" | .expo.android.package = "dev.expo.workersystemtests"' app.json > app.json.tmp
mv app.json.tmp app.json
- name: Configure EAS Build
working-directory: ${{ runner.temp }}/test-app
run: npx -y eas-cli@latest build:configure --platform all
- name: Add system-test profile to eas.json
working-directory: ${{ runner.temp }}/test-app
run: |
jq '.build["system-test"] = { "ios": { "simulator": true }, "android": { "withoutCredentials": true } }' eas.json > eas.json.tmp
mv eas.json.tmp eas.json
- name: Setup EAS workflow
working-directory: ${{ runner.temp }}/test-app
run: |
mkdir -p .eas/workflows
cp ${{ github.workspace }}/.github/fixtures/eas-workflow-build-android.yml .eas/workflows/build-android.yml
- name: Run iOS build and Android workflow concurrently
working-directory: ${{ runner.temp }}/test-app
run: |
set -euo pipefail
BUILD_JSON=$(npx -y eas-cli@latest build --platform ios --profile system-test --non-interactive --no-wait --json)
BUILD_ID=$(echo "$BUILD_JSON" | jq -r 'if type=="array" then .[0].id // .[0].build.id // .[0].buildId else .id // .build.id // .buildId end // empty')
if [ -z "$BUILD_ID" ]; then
echo "Error: could not determine build ID"
echo "$BUILD_JSON" | jq .
exit 1
fi
echo "BUILD_ID=$BUILD_ID" >> "$GITHUB_ENV"
echo "Resolved BUILD_ID=$BUILD_ID"
WORKFLOW_JSON=$(npx -y eas-cli@latest workflow:run build-android.yml --non-interactive --no-wait --json)
WORKFLOW_RUN_ID=$(echo "$WORKFLOW_JSON" | jq -r '.id // .workflowRun.id // .workflowRunId // empty')
if [ -z "$WORKFLOW_RUN_ID" ]; then
echo "Error: could not determine workflow run ID"
echo "$WORKFLOW_JSON" | jq .
exit 1
fi
echo "WORKFLOW_RUN_ID=$WORKFLOW_RUN_ID" >> "$GITHUB_ENV"
echo "Resolved WORKFLOW_RUN_ID=$WORKFLOW_RUN_ID"
is_terminal_build_status() {
case "$1" in
FINISHED|ERRORED|CANCELED) return 0 ;;
*) return 1 ;;
esac
}
is_terminal_workflow_status() {
case "$1" in
SUCCESS|FAILURE|CANCELED|ACTION_REQUIRED) return 0 ;;
*) return 1 ;;
esac
}
BUILD_STATUS=""
WORKFLOW_STATUS=""
TIMEOUT_SECONDS=1800
START_TIME=$(date +%s)
while true; do
ELAPSED=$(( $(date +%s) - START_TIME ))
if [ "$ELAPSED" -ge "$TIMEOUT_SECONDS" ]; then
echo "Timeout: polling exceeded 30 minutes"
exit 1
fi
BUILD_STATUS=$(npx -y eas-cli@latest build:view "$BUILD_ID" --json | jq -r '.status // empty')
WORKFLOW_STATUS=$(npx -y eas-cli@latest workflow:view "$WORKFLOW_RUN_ID" --non-interactive --json | jq -r '.status // empty')
echo "iOS build status: $BUILD_STATUS"
echo "Android workflow status: $WORKFLOW_STATUS"
if is_terminal_build_status "$BUILD_STATUS" && is_terminal_workflow_status "$WORKFLOW_STATUS"; then
break
fi
sleep 7
done
EXIT_CODE=0
if [ "$BUILD_STATUS" != "FINISHED" ]; then
echo "Build did not finish successfully: $BUILD_STATUS"
EXIT_CODE=1
fi
if [ "$WORKFLOW_STATUS" != "SUCCESS" ]; then
echo "Workflow did not finish successfully: $WORKFLOW_STATUS"
EXIT_CODE=1
fi
exit "$EXIT_CODE"
- name: Cancel EAS runs on workflow cancellation
if: always() && (cancelled() || failure())
working-directory: ${{ runner.temp }}/test-app
run: |
set -euo pipefail
if [ -n "${BUILD_ID:-}" ]; then
echo "Canceling EAS build: $BUILD_ID"
BUILD_CANCEL_OUTPUT=$(npx -y eas-cli@latest build:cancel "$BUILD_ID" --non-interactive 2>&1 || true)
if [ -n "$BUILD_CANCEL_OUTPUT" ]; then
echo "$BUILD_CANCEL_OUTPUT"
else
echo "build:cancel produced no output"
fi
BUILD_STATUS=$(npx -y eas-cli@latest build:view "$BUILD_ID" --json 2>/dev/null | jq -r '.status // empty' || true)
if [ -n "$BUILD_STATUS" ]; then
echo "Post-cancel build status: $BUILD_STATUS"
else
echo "Post-cancel build status unavailable"
fi
fi
if [ -n "${WORKFLOW_RUN_ID:-}" ]; then
echo "Canceling EAS workflow run: $WORKFLOW_RUN_ID"
WORKFLOW_CANCEL_OUTPUT=$(npx -y eas-cli@latest workflow:cancel "$WORKFLOW_RUN_ID" --non-interactive 2>&1 || true)
if [ -n "$WORKFLOW_CANCEL_OUTPUT" ]; then
echo "$WORKFLOW_CANCEL_OUTPUT"
else
echo "workflow:cancel produced no output"
fi
WORKFLOW_STATUS=$(npx -y eas-cli@latest workflow:view "$WORKFLOW_RUN_ID" --non-interactive --json 2>/dev/null | jq -r '.status // empty' || true)
if [ -n "$WORKFLOW_STATUS" ]; then
echo "Post-cancel workflow status: $WORKFLOW_STATUS"
else
echo "Post-cancel workflow status unavailable"
fi
fi
+80
View File
@@ -0,0 +1,80 @@
name: worker
on:
push:
branches: [main]
paths:
- "packages/worker/**"
- "packages/build-tools/**"
- "packages/eas-build-job/**"
- "packages/steps/**"
- "packages/logger/**"
- "packages/downloader/**"
- "packages/template-file/**"
- "packages/turtle-spawn/**"
- .github/workflows/worker.yml
- .github/workflows/build-and-deploy-worker.yml
- yarn.lock
concurrency:
group: worker-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
worker-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: ./.github/actions/setup-mise
- run: yarn install --immutable
- run: yarn typecheck
- name: Run unit tests
working-directory: packages/worker
run: yarn test:unit
- name: Run integration tests
working-directory: packages/worker
run: yarn test:integration
worker-build:
needs:
- worker-checks
uses: ./.github/workflows/build-and-deploy-worker.yml
secrets: inherit
with:
environment: ${{ github.ref == 'refs/heads/main' && 'staging' || '' }}
system-tests-staging:
if: github.ref == 'refs/heads/main' && !failure() && !cancelled()
needs:
- worker-build
- worker-checks
uses: ./.github/workflows/worker-system-tests.yml
secrets: inherit
deploy-worker-production:
if: github.ref == 'refs/heads/main' && !failure() && !cancelled()
needs:
- system-tests-staging
uses: ./.github/workflows/build-and-deploy-worker.yml
secrets: inherit
with:
environment: production
notify-slack:
runs-on: ubuntu-latest
needs:
- deploy-worker-production
if: github.ref == 'refs/heads/main' && always() && !cancelled()
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Notify Slack
uses: ./.github/internal-actions/notify-slack-on-fail-or-recover
with:
channel: turtle-bots
slack_webhook: ${{ secrets.SLACK_EAS_WORKFLOWS_WEBHOOK }}
+7
View File
@@ -3,17 +3,24 @@ packages/*/build
packages/eas-cli/dist
packages/eas-cli/tmp
scripts/build
dist
# TypeScript
**/*.tsbuildinfo
# Code editors
.idea
.history
.vscode
*.swp
*.swo
# macOS
.DS_Store
# Node
node_modules
**/.yarn/install-state.gz
*-debug.log
*-error.log
+22
View File
@@ -0,0 +1,22 @@
{
"printWidth": 100,
"tabWidth": 2,
"singleQuote": true,
"bracketSameLine": true,
"trailingComma": "es5",
"arrowParens": "avoid",
"endOfLine": "lf",
"ignorePatterns": [
".github",
"lerna.json",
"**/build/**",
"**/dist/**",
"**/coverage/**",
"**/fixtures/**",
"packages/eas-cli/README.md",
"packages/eas-cli/schema/**",
"packages/eas-cli/graphql.schema.json",
"packages/eas-cli/src/graphql/generated.ts",
"packages/build-tools/schema.graphql"
]
}
+77
View File
@@ -0,0 +1,77 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"categories": {
"correctness": "off",
"suspicious": "off",
"pedantic": "off",
"perf": "off",
"restriction": "off",
"style": "off",
"nursery": "off"
},
"ignorePatterns": [
"**/build/**",
"**/dist/**",
"**/coverage/**",
"**/fixtures/**",
"**/__fixtures__/**",
".github/**",
"packages/eas-cli/schema/**",
"packages/eas-cli/graphql.schema.json",
"packages/build-tools/schema.graphql"
],
"plugins": ["import", "node", "typescript"],
"jsPlugins": [
{
"name": "async-protect",
"specifier": "eslint-plugin-async-protect"
}
],
"rules": {
"no-console": "warn",
"no-constant-condition": [
"warn",
{
"checkLoops": false
}
],
"sort-imports": [
"warn",
{
"ignoreDeclarationSort": true
}
],
"curly": "warn",
"default": "off",
"named": "off",
"namespace": "off",
"no-cycle": "error",
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "lodash",
"message": "Don't use lodash, it's heavy!"
}
]
}
],
"no-underscore-dangle": [
"error",
{
"allow": ["__typename"]
}
],
"async-protect/async-suffix": "error",
"node/no-sync": "error"
},
"overrides": [
{
"files": ["**/__tests__/**/*.ts", "**/__mocks__/**/*.ts"],
"rules": {
"async-protect/async-suffix": "off"
}
}
]
}
-9
View File
@@ -1,9 +0,0 @@
{
"printWidth": 100,
"tabWidth": 2,
"singleQuote": true,
"bracketSameLine": true,
"trailingComma": "es5",
"arrowParens": "avoid",
"endOfLine": "auto"
}
+1 -2
View File
@@ -1,5 +1,4 @@
{
"eslint.enable": true,
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
@@ -7,4 +6,4 @@
"**/src/**/build/**": false,
"**/src/build/**": false,
}
}
}
-1
View File
@@ -1 +0,0 @@
save-prefix ""
+9
View File
@@ -0,0 +1,9 @@
defaultSemverRangePrefix: ''
nodeLinker: node-modules
npmMinimalAgeGate: 2d
npmPreapprovedPackages:
- '@expo/*'
- 'eslint-plugin-universe'
- 'oxlint-config-universe'
+6
View File
@@ -0,0 +1,6 @@
# AGENTS.md
When working with this repository follow instructions from CLAUDE.md.
- [./CLAUDE.md](./CLAUDE.md)
- Before committing, run the formatter on modified files.
+467 -1
View File
@@ -12,6 +12,473 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores
## [19.0.1](https://github.com/expo/eas-cli/releases/tag/v19.0.1) - 2026-05-19
### 🐛 Bug fixes
- [eas-cli] Disable dotenv loading when resolving managed iOS entitlements. ([#3752](https://github.com/expo/eas-cli/pull/3752) by [@sjchmiela](https://github.com/sjchmiela))
- [eas-cli] Restore the bundled config fallback for managed iOS entitlements introspection. ([#3753](https://github.com/expo/eas-cli/pull/3753) by [@sjchmiela](https://github.com/sjchmiela))
## [19.0.0](https://github.com/expo/eas-cli/releases/tag/v19.0.0) - 2026-05-19
### 🛠 Breaking changes
- [eas-cli] Make browser-based authentication the default for `eas login`. Use `--no-browser` for CLI-based login. ([#3746](https://github.com/expo/eas-cli/pull/3746) by [@byronkarlen](https://github.com/byronkarlen))
### 🎉 New features
- [eas-cli] Allow command `observe:routes` to filter results to specific route names. ([#3744](https://github.com/expo/eas-cli/pull/3744) by [@douglowder](https://github.com/douglowder))
### 🐛 Bug fixes
- [eas-cli] Check that `expo` is resolvable before assuming it's installed ([#3748](https://github.com/expo/eas-cli/pull/3748) by [@kitten](https://github.com/kitten))
### 🧹 Chores
- [worker] Report build phase durations stats. ([#3732](https://github.com/expo/eas-cli/pull/3732) by [@gwdp](https://github.com/gwdp))
## [18.13.1](https://github.com/expo/eas-cli/releases/tag/v18.13.1) - 2026-05-18
### 🎉 New features
- [eas-cli] Send the installed `expo` package version in build metadata. ([#3725](https://github.com/expo/eas-cli/pull/3725) by [@sjchmiela](https://github.com/sjchmiela))
### 🐛 Bug fixes
- [eas-cli] Prevent `npx` invocations that can be unreliable and fail when retrieving entitlements or project configs ([#3282](https://github.com/expo/eas-cli/pull/3282) by [@kitten](https://github.com/kitten))
## [18.13.0](https://github.com/expo/eas-cli/releases/tag/v18.13.0) - 2026-05-14
### 🎉 New features
- [eas-cli] New command `observe:routes` for navigation metrics. ([#3730](https://github.com/expo/eas-cli/pull/3730) by [@douglowder](https://github.com/douglowder))
### 🧹 Chores
- [eas-cli] Refactor observe commands to extract common flag definitions. ([#3731](https://github.com/expo/eas-cli/pull/3731) by [@douglowder](https://github.com/douglowder))
## [18.12.3](https://github.com/expo/eas-cli/releases/tag/v18.12.3) - 2026-05-13
## [18.12.2](https://github.com/expo/eas-cli/releases/tag/v18.12.2) - 2026-05-12
### 🐛 Bug fixes
- [eas-cli] Fix `eas go` to use server-side project setup and support multiple Expo Go SDK versions. ([#3657](https://github.com/expo/eas-cli/pull/3657) by [@gwdp](https://github.com/gwdp))
### 🧹 Chores
- [ci] Audit GitHub Actions workflows: harden `pull_request_target` usage, pin all external actions to commit SHAs, and add Dependabot to refresh them. ([#3718](https://github.com/expo/eas-cli/pull/3718) by [@brentvatne](https://github.com/brentvatne))
## [18.12.1](https://github.com/expo/eas-cli/releases/tag/v18.12.1) - 2026-05-12
## [18.12.0](https://github.com/expo/eas-cli/releases/tag/v18.12.0) - 2026-05-12
### 🎉 New features
- [eas-cli] Automatically save the Convex deployment URL as an EAS environment variable when connecting a Convex project. ([#3685](https://github.com/expo/eas-cli/pull/3685) by [@fiberjw](https://github.com/fiberjw))
### 🐛 Bug fixes
- [build-tools] Update the minimum Expo version required for iOS precompiled modules. ([#3677](https://github.com/expo/eas-cli/pull/3677) by [@sjchmiela](https://github.com/sjchmiela))
- [eas-cli] recommend that users read the docs after connecting Convex ([#3683](https://github.com/expo/eas-cli/pull/3683) by [@fiberjw](https://github.com/fiberjw))
## [18.11.0](https://github.com/expo/eas-cli/releases/tag/v18.11.0) - 2026-05-05
### 🎉 New features
- [eas-cli] Add `--simulator` to `eas build:dev` and `eas build:run` to select which iOS simulator to install and run builds on. ([#3637](https://github.com/expo/eas-cli/pull/3637) by [@mmichels-brex](https://github.com/mmichels-brex))
### 🐛 Bug fixes
- [eas-cli] Fix Convex team invite output after skipped or unnecessary invitations. ([#3672](https://github.com/expo/eas-cli/pull/3672) by [@fiberjw](https://github.com/fiberjw))
- Make directories marked as read-only writable when creating a tar, to prevent the cross-filesystem issues when extracting the tar. ([#3489](https://github.com/expo/eas-cli/pull/3489) by [@sswrk](https://github.com/sswrk))
## [18.10.0](https://github.com/expo/eas-cli/releases/tag/v18.10.0) - 2026-05-04
### 🎉 New features
- [eas-cli] Add `eas integrations:convex` commands to manage Convex integrations for EAS projects. ([#3575](https://github.com/expo/eas-cli/pull/3575) by [@fiberjw](https://github.com/fiberjw))
- [eas-cli] New command `observe:logs` for custom events. ([#3638](https://github.com/expo/eas-cli/pull/3638) by [@douglowder](https://github.com/douglowder))
### 🐛 Bug fixes
- [eas-cli] Create portable project archives on all platforms to normalize cross-platform tar metadata and permissions. ([#3234](https://github.com/expo/eas-cli/pull/3234) by [@sjchmiela](https://github.com/sjchmiela))
- [eas-cli] Remove hardcoded `builderEnvironment.image` override in `eas build:resign`. ([#3661](https://github.com/expo/eas-cli/pull/3661) by [@hSATAC](https://github.com/hSATAC))
- [eas-cli] Fix `eas update --json` intermittently failing with JSON parse errors during "Computing project fingerprints" by passing `silent: true` to `@expo/fingerprint` to suppress subprocess stdout pollution. ([#3659](https://github.com/expo/eas-cli/pull/3659) by [@Mookiies](https://github.com/Mookiies))
## [18.9.1](https://github.com/expo/eas-cli/releases/tag/v18.9.1) - 2026-04-30
## [18.9.0](https://github.com/expo/eas-cli/releases/tag/v18.9.0) - 2026-04-30
### 🎉 New features
- [eas-cli] Add `eas integrations:asc` commands to manage App Store Connect integrations for EAS projects. ([#3558](https://github.com/expo/eas-cli/pull/3558) by [@sswrk](https://github.com/sswrk))
- [eas-cli] Allow `eas build:download` to accept a build ID. ([#3655](https://github.com/expo/eas-cli/pull/3655) by [@douglowder](https://github.com/douglowder))
- [eas-cli] Add `--id` alias for `--build-id` flag in `eas build:download`. ([#3656](https://github.com/expo/eas-cli/pull/3656) by [@brentvatne](https://github.com/brentvatne))
### 🐛 Bug fixes
- [steps] Fix workflow job context interpolation when a string contains multiple `${{ }}` expressions on the same line (for example a changelog built from two context values). ([#3644](https://github.com/expo/eas-cli/pull/3644) by [@gwdp](https://github.com/gwdp))
## [18.8.1](https://github.com/expo/eas-cli/releases/tag/v18.8.1) - 2026-04-23
## [18.8.0](https://github.com/expo/eas-cli/releases/tag/v18.8.0) - 2026-04-22
### 🎉 New features
- [eas-cli] Support custom params in `observe:events` results. ([#3620](https://github.com/expo/eas-cli/pull/3620) by [@douglowder](https://github.com/douglowder))
- [build-tools] Add `eas/deploy` function for EAS Hosting web deployments. ([#3598](https://github.com/expo/eas-cli/pull/3598) by [@gwdp](https://github.com/gwdp))
- [build-tools] Add `eas/export` function for Expo web exports. ([#3598](https://github.com/expo/eas-cli/pull/3598) by [@gwdp](https://github.com/gwdp))
- [eas-cli] Add `eas update:insights <groupId>` command to display launch, crash, unique-user, and payload-size metrics for an update group. ([#3614](https://github.com/expo/eas-cli/pull/3614) by [@kadikraman](https://github.com/kadikraman))
- [eas-cli] Add `--insights` flag to `eas update:view <groupId>` to append the same metrics below the existing output. ([#3614](https://github.com/expo/eas-cli/pull/3614) by [@kadikraman](https://github.com/kadikraman))
- [eas-cli] Add `eas channel:insights --channel <name> --runtime-version <version>` command to display adoption and crash metrics for a channel + runtime version. ([#3614](https://github.com/expo/eas-cli/pull/3614) by [@kadikraman](https://github.com/kadikraman))
### 🐛 Bug fixes
- [eas-cli] Fix `metadata:push` not deleting video previews from App Store Connect when removed from config. ([#3603](https://github.com/expo/eas-cli/pull/3603) by [@EvanBacon](https://github.com/EvanBacon))
- [eas-cli] Warn and skip unknown preview types in `metadata:push` with a helpful suggestion when the `APP_` screenshot prefix is mistakenly used. ([#3603](https://github.com/expo/eas-cli/pull/3603) by [@EvanBacon](https://github.com/EvanBacon))
- [eas-cli] Delete all existing previews in a preview set before uploading to avoid Apple's "Too many app previews" error. ([#3603](https://github.com/expo/eas-cli/pull/3603) by [@EvanBacon](https://github.com/EvanBacon))
- [eas-cli] Fixes for `observe` commands, including an issue for apps with many update IDs. ([#3609](https://github.com/expo/eas-cli/pull/3609) by [@douglowder](https://github.com/douglowder))
- [eas-cli] Add existing capability identifiers. ([#3615](https://github.com/expo/eas-cli/pull/3615) by [@jakex7](https://github.com/jakex7))
## [18.7.0](https://github.com/expo/eas-cli/releases/tag/v18.7.0) - 2026-04-14
### 🎉 New features
- [build-tools] Add `eas/read_package_json` and `eas/read_app_config` functions ([#3585](https://github.com/expo/eas-cli/pull/3585) by [@gwdp](https://github.com/gwdp))
- [eas-cli] Add `--skip-bundler` to `eas build:dev` to install and run a development build without starting the local Expo/Metro server. ([#3604](https://github.com/expo/eas-cli/pull/3604) by [@mmichels-brex](https://github.com/mmichels-brex))
## [18.6.0](https://github.com/expo/eas-cli/releases/tag/v18.6.0) - 2026-04-10
### 🎉 New features
- [eas-cli] Add missing Apple metadata attributes for age ratings and content descriptions. ([#3584](https://github.com/expo/eas-cli/pull/3584) by [@EvanBacon](https://github.com/EvanBacon))
- [eas-cli] Add App Clip metadata support to `metadata:push` and `metadata:pull` (default experience action, per-locale subtitle and header image, App Store review invocation URLs). ([#3590](https://github.com/expo/eas-cli/pull/3590) by [@EvanBacon](https://github.com/EvanBacon))
- [build-tools] Add `working_directory` input to `eas/build` function group for custom builds. ([#3582](https://github.com/expo/eas-cli/pull/3582) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- [steps] Coerce numeric env values to strings in workflow step configuration. ([#3583](https://github.com/expo/eas-cli/pull/3583) by [@szdziedzic](https://github.com/szdziedzic))
- [build-tools][eas-cli] Detect iOS Development provisioning profiles and set correct code signing identity instead of treating them as Ad Hoc. ([#3496](https://github.com/expo/eas-cli/pull/3496) by [@qwertey6](https://github.com/qwertey6))
- [build-tools] Prevent detecting Yarn Modern as Classic based on lockfile ([#3572](https://github.com/expo/eas-cli/pull/3572) by [@kitten](https://github.com/kitten))
- [build-tools] Early stop `eas/start_android_emulator` when Android emulator host setup is invalid. ([#3580](https://github.com/expo/eas-cli/pull/3580) by [@gwdp](https://github.com/gwdp))
- [eas-cli] Bump `@expo/apple-utils` to `2.1.18` to fix `metadata:push` failing on `ageRatingDeclarations` due to the removed `gamblingAndContests` attribute. ([#3588](https://github.com/expo/eas-cli/pull/3588) by [@EvanBacon](https://github.com/EvanBacon))
- [eas-cli] Bump `@expo/apple-utils` to `2.1.19` to fix image and video uploads via `metadata:push` getting stuck in `AWAITING_UPLOAD` state. The asset client was inheriting Bearer token injection from the App Store Connect API client, which caused S3 presigned URL uploads to be silently mishandled by Apple's CDN. Fixes screenshots, previews, and App Clip header image uploads. ([#3590](https://github.com/expo/eas-cli/pull/3590) by [@EvanBacon](https://github.com/EvanBacon))
- [eas-cli] `metadata:pull` now preserves screenshot, video preview, and App Clip header image entries with placeholder paths when the asset is in an unrendered state, so users can recover broken records by replacing the file or removing the entry instead of having entries silently dropped from `store.config.json`. ([#3590](https://github.com/expo/eas-cli/pull/3590) by [@EvanBacon](https://github.com/EvanBacon))
- [eas-cli] Surface hosting deployment's asset upload errors sooner ([#3600](https://github.com/expo/eas-cli/pull/3600) by [@kitten](https://github.com/kitten))
## [18.5.0](https://github.com/expo/eas-cli/releases/tag/v18.5.0) - 2026-04-02
### 🎉 New features
- [eas-cli] Add screenshots and previews support to `metadata:push` and `metadata:pull`. ([#3301](https://github.com/expo/eas-cli/pull/3301) by [@EvanBacon](https://github.com/EvanBacon))
- [eas-cli] Add `--non-interactive` flag to `metadata:push` and `metadata:pull` commands with ASC API Key auth support. ([#3548](https://github.com/expo/eas-cli/pull/3548) by [@EvanBacon](https://github.com/EvanBacon))
- [eas-cli] Add `observe:metrics`, `observe:events`, and `observe:versions` commands. ([#3401](https://github.com/expo/eas-cli/pull/3401) by [@ubax](https://github.com/ubax)), [@douglowder](https://github.com/douglowder)
### 🐛 Bug fixes
- [eas-cli] Fix workflow:logs for builds using built-in EAS build steps. ([#3523](https://github.com/expo/eas-cli/pull/3523) by [@douglowder](https://github.com/douglowder))
- [build-tools][worker] Read Expo app config with `expo config` CLI invocation before falling back to `@expo/config` ([#3536](https://github.com/expo/eas-cli/pull/3536) by [@kitten](https://github.com/kitten))
- Fix `hasIgnoredIosProjectAsync()` always returning `false` for ignored iOS projects. ([#3562](https://github.com/expo/eas-cli/pull/3562) by [@sjchmiela](https://github.com/sjchmiela))
### 🧹 Chores
- Bump apple-utils. ([#3545](https://github.com/expo/eas-cli/pull/3545) by [@quinlanj](https://github.com/quinlanj))
- [eas-cli][build-tools][worker] Bump `@expo/config` and `@expo/config-plugins`. ([#3527](https://github.com/expo/eas-cli/pull/3527) by [@quinlanj](https://github.com/quinlanj))
## [18.4.0](https://github.com/expo/eas-cli/releases/tag/v18.4.0) - 2026-03-16
### 🎉 New features
- Auto-detect non-interactive mode in CI and non-TTY environments. ([#3486](https://github.com/expo/eas-cli/pull/3486) by [@EvanBacon](https://github.com/EvanBacon))
- Add `skip-build-if-not-found` flag to `eas build:dev`. ([#3495](https://github.com/expo/eas-cli/pull/3495) by [@mmichels-brex](https://github.com/mmichels-brex))
## [18.3.0](https://github.com/expo/eas-cli/releases/tag/v18.3.0) - 2026-03-10
### 🎉 New features
- Show user email in `eas account:view` output. ([#3485](https://github.com/expo/eas-cli/pull/3485) by [@brentvatne](https://github.com/brentvatne))
## [18.2.0](https://github.com/expo/eas-cli/releases/tag/v18.2.0) - 2026-03-09
### 🎉 New features
- Auto-enable `--non-interactive` when `--json` is passed, so users no longer need to specify both flags. ([#3476](https://github.com/expo/eas-cli/pull/3476) by [@krystofwoldrich](https://github.com/krystofwoldrich))
- Auto-generate Android keystore in non-interactive mode instead of throwing an error. ([#3477](https://github.com/expo/eas-cli/pull/3477) by [@EvanBacon](https://github.com/EvanBacon))
- Add `--source-maps` flag to prevent uploading server sourcemaps ([#3478](https://github.com/expo/eas-cli/pull/3478) by [@hassankhan](https://github.com/hassankhan))
### 🐛 Bug fixes
- Check all certificates in provisioning profile during verification instead of only the first one. ([#3484](https://github.com/expo/eas-cli/pull/3484) by [@qwertey6](https://github.com/qwertey6))
- Provide an override for the new --environment flag requirement in the update command. ([#3442](https://github.com/expo/eas-cli/pull/3442) by [@douglowder](https://github.com/douglowder))
- Add missing `--include=dev` for `npm install` commands ([#3459](https://github.com/expo/eas-cli/pull/3459) by [@kitten](https://github.com/kitten))
- Add missing `--production false` for `yarn install` commands for Yarn Classic ([#3459](https://github.com/expo/eas-cli/pull/3459) by [@kitten](https://github.com/kitten))
### 🧹 Chores
- Upgrade `oclif` to v4. ([#3472](https://github.com/expo/eas-cli/pull/3472) by [@sjchmiela](https://github.com/sjchmiela))
## [18.1.0](https://github.com/expo/eas-cli/releases/tag/v18.1.0) - 2026-03-04
### 🎉 New features
- Support YAML anchors and aliases when parsing workflow files. ([#3435](https://github.com/expo/eas-cli/pull/3435) by [@sjchmiela](https://github.com/sjchmiela))
### 🐛 Bug fixes
- Remove --environment flag requirement for update:configure. ([#3440](https://github.com/expo/eas-cli/pull/3440) by [@douglowder](https://github.com/douglowder))
- Fix login spinner interfering with prompts in `eas go` command when not logged in. ([#3451](https://github.com/expo/eas-cli/pull/3451) by [@byronkarlen](https://github.com/byronkarlen))
## [18.0.6](https://github.com/expo/eas-cli/releases/tag/v18.0.6) - 2026-02-27
## [18.0.5](https://github.com/expo/eas-cli/releases/tag/v18.0.5) - 2026-02-25
### 🛠 Breaking changes
- Require --environment flag for update commands when SDK >= 55. ([#3398](https://github.com/expo/eas-cli/pull/3398) by [@douglowder](https://github.com/douglowder))
## [18.0.4](https://github.com/expo/eas-cli/releases/tag/v18.0.4) - 2026-02-23
### 🐛 Bug fixes
- Revert authorization code flow with PKCE for browser-based login. ([#3422](https://github.com/expo/eas-cli/pull/3422) by [@byronkarlen](https://github.com/byronkarlen))
## [18.0.3](https://github.com/expo/eas-cli/releases/tag/v18.0.3) - 2026-02-20
### 🎉 New features
- Use authorization code flow with PKCE for browser-based login. ([#3398](https://github.com/expo/eas-cli/pull/3398) by [@byronkarlen](https://github.com/byronkarlen))
## [18.0.1](https://github.com/expo/eas-cli/releases/tag/v18.0.1) - 2026-02-12
### 🛠 Breaking changes
- Drop support for Node 18 and Node 19. Require Node 20+. ([#3343](https://github.com/expo/eas-cli/pull/3343) by [@sjchmiela](https://github.com/sjchmiela))
### 🎉 New features
- Add `--browser` flag to `eas login` for browser-based authentication. ([#3312](https://github.com/expo/eas-cli/pull/3312) by [@byronkarlen](https://github.com/byronkarlen))
### 🐛 Bug fixes
- Hide progress bar in build credits warning when usage reaches 100%. ([#3371](https://github.com/expo/eas-cli/pull/3371) by [@mackenco](https://github.com/mackenco))
### 🧹 Chores
- Upgrade `tar` to v7. ([#3327](https://github.com/expo/eas-cli/pull/3327) by [@KarolRzeminski](https://github.com/KarolRzeminski))
## [16.32.0](https://github.com/expo/eas-cli/releases/tag/v16.32.0) - 2026-01-30
### 🎉 New features
- List available options in non-interactive mode errors instead of generic "unable to select" messages, making CLI usable by agents and scripts. ([#3359](https://github.com/expo/eas-cli/pull/3359) by [@EvanBacon](https://github.com/EvanBacon))
- Hidden beta version of new `eas account:usage` command for viewing account usage and billing estimates. ([#3334](https://github.com/expo/eas-cli/pull/3334) by [@douglowder](https://github.com/douglowder), [@EvanBacon](https://github.com/EvanBacon))
- Add `--no-bytecode` and `--source-maps [mode]` flags to `eas update`. ([#3339](https://github.com/expo/eas-cli/pull/3339) by [@brentvatne](https://github.com/brentvatne))
- Add App Clip bundle identifier registration support for multi-target iOS builds. ([#3300](https://github.com/expo/eas-cli/pull/3300) by [@evanbacon](https://github.com/evanbacon))
- Warn in `eas build` when creating a production build from an app that uses Expo Go for development ([#3073](https://github.com/expo/eas-cli/pull/3073) by [@vonovak](https://github.com/vonovak))
- Add `--runtime-version` and `--platform` filters to `eas update:list`. ([#3261](https://github.com/expo/eas-cli/pull/3261) by [@HarelSultan](https://github.com/HarelSultan))
### 🐛 Bug fixes
- Fix `--source-maps` flag compatibility with older SDKs that only support it as a boolean flag. ([#3341](https://github.com/expo/eas-cli/pull/3341) by [@brentvatne](https://github.com/brentvatne))
- Use `--dump-sourcemaps` as fallback when `--source-maps` is not provided to `eas update`, for backwards compatibility. ([8cc324e1](https://github.com/expo/eas-cli/commit/8cc324e1) by [@brentvatne](https://github.com/brentvatne))
- Fix `metadata:pull` failing for apps with only a live version by falling back to live app version and info. ([#3299](https://github.com/expo/eas-cli/pull/3299) by [@EvanBacon](https://github.com/EvanBacon))
- eas init should fix and validate project name and slug. ([#3277](https://github.com/expo/eas-cli/pull/3277) by [@douglowder](https://github.com/douglowder))
### 🧹 Chores
- Test with node 24. ([#3270](https://github.com/expo/eas-cli/pull/3270) by [@wschurman](https://github.com/wschurman))
- Delete channel in background. ([#3278](https://github.com/expo/eas-cli/pull/3278) by [@quinlanj](https://github.com/quinlanj))
- Add warning to channel:delete. ([#3335](https://github.com/expo/eas-cli/pull/3335) by [@quinlanj](https://github.com/quinlanj))
## [16.28.0](https://github.com/expo/eas-cli/releases/tag/v16.28.0) - 2025-11-20
### 🎉 New features
- `eas:new` sorts organization accounts first in account selection. ([#3256](https://github.com/expo/eas-cli/pull/3256) by [@mackenco](https://github.com/mackenco))
### 🐛 Bug fixes
- Fix `eas:build` bug where overages are checked before config is ready. ([#3260](https://github.com/expo/eas-cli/pull/3260) by [@mackenco](https://github.com/mackenco))
### 🧹 Chores
- Delete branch in background. ([#3258](https://github.com/expo/eas-cli/pull/3258) by [@quinlanj](https://github.com/quinlanj))
## [16.27.0](https://github.com/expo/eas-cli/releases/tag/v16.27.0) - 2025-11-13
### 🎉 New features
- Kick off update patch generation if enabled - this is feature is in preview, don't use it for prod yet please. ([#3250](https://github.com/expo/eas-cli/pull/3250) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Fix workflow:status when run ID passed in. ([#3253](https://github.com/expo/eas-cli/pull/3253) by [@douglowder](https://github.com/douglowder))
- Fix updating file env vars with `env:update --non-interactive`. ([#3249](https://github.com/expo/eas-cli/pull/3249) by [@kadikraman](https://github.com/kadikraman))
### 🧹 Chores
- Fix yarn watch. ([#3251](https://github.com/expo/eas-cli/pull/3251) by [@quinlanj](https://github.com/quinlanj))
- Remove the **node_modules** installation requirement for `workflow:run` command. ([#3254](https://github.com/expo/eas-cli/pull/3254) by [@kudo](https://github.com/kudo))
## [16.26.0](https://github.com/expo/eas-cli/releases/tag/v16.26.0) - 2025-10-29
### 🎉 New features
- New command `workflow:status`. ([#3242](https://github.com/expo/eas-cli/pull/3242) by [@douglowder](https://github.com/douglowder))
### 🧹 Chores
- `eas:new` cleanup (remove `hidden` tag, fix README error, output cleanup). ([#3243](https://github.com/expo/eas-cli/pull/3243) by [@mackenco](https://github.com/mackenco))
## [16.25.1](https://github.com/expo/eas-cli/releases/tag/v16.25.1) - 2025-10-28
### 🧹 Chores
- Remove preview label from EAS Hosting commands ([#3241](https://github.com/expo/eas-cli/pull/3241) by [@kitten](https://github.com/kitten))
## [16.25.0](https://github.com/expo/eas-cli/releases/tag/v16.25.0) - 2025-10-27
### 🎉 New features
- `eas:new` command changes (project name prompt, output cleanup, additional project files, additional prompts). ([#3235](https://github.com/expo/eas-cli/pull/3235) by [@mackenco](https://github.com/mackenco))
- `eas:build` command displays a warning when account is near it's build limit. ([#3236](https://github.com/expo/eas-cli/pull/3236) by [@mackenco](https://github.com/mackenco))
### 🐛 Bug fixes
- workflow:run can use --wait and --json together. ([#3239](https://github.com/expo/eas-cli/pull/3239) by [@douglowder](https://github.com/douglowder))
- Add missing env for local builds with latest plugin. ([#3237](https://github.com/expo/eas-cli/pull/3237) by [@douglowder](https://github.com/douglowder))
### 🧹 Chores
- Document the default values for `track` and `releaseStatus`. ([#3229](https://github.com/expo/eas-cli/pull/3229) by [@kadikraman](https://github.com/kadikraman))
## [16.24.1](https://github.com/expo/eas-cli/releases/tag/v16.24.1) - 2025-10-19
### 🐛 Bug fixes
- Revert change in ora version. ([#3231](https://github.com/expo/eas-cli/pull/3231) by [@douglowder](https://github.com/douglowder))
## [16.24.0](https://github.com/expo/eas-cli/releases/tag/v16.24.0) - 2025-10-19
### 🎉 New features
- workflow:run --wait shows periodic job and step status updates. ([#3227](https://github.com/expo/eas-cli/pull/3227) by [@douglowder](https://github.com/douglowder))
### 🐛 Bug fixes
- workflow:logs should show logs from build and repack jobs. ([#3226](https://github.com/expo/eas-cli/pull/3226) by [@douglowder](https://github.com/douglowder))
## [16.23.1](https://github.com/expo/eas-cli/releases/tag/v16.23.1) - 2025-10-16
### 🐛 Bug fixes
- Multiple bugfixes and small features for the `eas:new` command: handle project name and directory generation more smoothly; support `bun` as a package manager; add `--package-manager` flag with a default to `npm`; ensure workflow files are in the correct location after build; update the generated project's README with the provided package manager; ensure `app.json`'s `scheme` is valid; silence lots of noisy output. ([#3205](https://github.com/expo/eas-cli/pull/3205) by [@mackenco](https://github.com/mackenco))
## [16.23.0](https://github.com/expo/eas-cli/releases/tag/v16.23.0) - 2025-10-14
### 🎉 New features
- Support custom environment variable environments ([#3220](https://github.com/expo/eas-cli/pull/3220) by [@kadikraman](https://github.com/kadikraman))
- Add --force flag to eas env:push command to bypass confirmation prompts. ([#3135](https://github.com/expo/eas-cli/pull/3135) by [@giorgiofellipe](https://github.com/giorgiofellipe))
## [16.22.0](https://github.com/expo/eas-cli/releases/tag/v16.22.0) - 2025-10-13
### 🎉 New features
- Improvements to workflow:create. ([#3194](https://github.com/expo/eas-cli/pull/3194) by [@douglowder](https://github.com/douglowder))
## [16.21.0](https://github.com/expo/eas-cli/releases/tag/v16.21.0) - 2025-10-13
### 🎉 New features
- Support `_expo/.routes.json` metadata file exported by `@expo/cli` for headers configuration on static deployments ([#3211](https://github.com/expo/eas-cli/pull/3211) by [@kitten](https://github.com/kitten))
### 🧹 Chores
- Add thorough tests for the env commands ([#3217](https://github.com/expo/eas-cli/pull/3217) by [@kadikraman](https://github.com/kadikraman))
## [16.20.4](https://github.com/expo/eas-cli/releases/tag/v16.20.4) - 2025-10-08
### 🎉 New features
- Allow workflow:run to run a workflow from a GitHub ref. ([#3203](https://github.com/expo/eas-cli/pull/3203) by [@douglowder](https://github.com/douglowder))
### 🐛 Bug fixes
- Fix --environment flag in eas exec and pull ([#3216](https://github.com/expo/eas-cli/pull/3216) by [@kadikraman](https://github.com/kadikraman))
## [16.20.3](https://github.com/expo/eas-cli/releases/tag/v16.20.3) - 2025-10-07
### 🐛 Bug fixes
- Fix --environment flag in eas env commands ([#3213](https://github.com/expo/eas-cli/pull/3213) by [@kadikraman](https://github.com/kadikraman))
## [16.20.2](https://github.com/expo/eas-cli/releases/tag/v16.20.2) - 2025-10-07
### 🧹 Chores
- Create an `EnvironmentVariableEnvironment` enum and use it instead of the graphQL Enum ([#3208](https://github.com/expo/eas-cli/pull/3208) by [@kadikraman](https://github.com/kadikraman))
## [16.20.1](https://github.com/expo/eas-cli/releases/tag/v16.20.1) - 2025-10-03
### 🐛 Bug fixes
- skip syncing capabilities with settings if they don't contain the `enabled` attribute ([#3201](https://github.com/expo/eas-cli/pull/3201) by [@vonovak](https://github.com/vonovak))
## [16.20.0](https://github.com/expo/eas-cli/releases/tag/v16.20.0) - 2025-09-30
### 🎉 New features
- Improvements to workflow:validate. ([#3165](https://github.com/expo/eas-cli/pull/3165) by [@douglowder](https://github.com/douglowder))
- Add `eas:new` command behind `hidden` flag. ([#3196](https://github.com/expo/eas-cli/pull/3196) by [@mackenco](https://github.com/mackenco))
## [16.19.3](https://github.com/expo/eas-cli/releases/tag/v16.19.3) - 2025-09-17
### 🧹 Chores
- Temporarily remove warnings when no compatible build found, need to refine the message. ([#3182](https://github.com/expo/eas-cli/pull/3182) by [@brentvatne](https://github.com/brentvatne))
## [16.19.2](https://github.com/expo/eas-cli/releases/tag/v16.19.2) - 2025-09-15
### 🎉 New features
- Add artifact URLs to workflow:view. ([#3175](https://github.com/expo/eas-cli/pull/3175) by [@douglowder](https://github.com/douglowder))
## [16.19.1](https://github.com/expo/eas-cli/releases/tag/v16.19.1) - 2025-09-12
## [16.19.0](https://github.com/expo/eas-cli/releases/tag/v16.19.0) - 2025-09-12
### 🎉 New features
- Allow any string as Android release `track`, not just the `production`, `internal`, `alpha`, `beta`. ([#3163](https://github.com/expo/eas-cli/pull/3163) by [@sjchmiela](https://github.com/sjchmiela))
## [16.18.1](https://github.com/expo/eas-cli/releases/tag/v16.18.1) - 2025-09-10
### 🎉 New features
- CLI debug log should respect DEBUG=`eas:*`. ([#3162](https://github.com/expo/eas-cli/pull/3162) by [@douglowder](https://github.com/douglowder))
- Better description for update:rollback. ([#3154](https://github.com/expo/eas-cli/pull/3154) by [@douglowder](https://github.com/douglowder))
## [16.18.0](https://github.com/expo/eas-cli/releases/tag/v16.18.0) - 2025-08-27
### 🎉 New features
- Show fingerprints in build:view and build:list commands. ([#3137](https://github.com/expo/eas-cli/pull/3137) by [@douglowder](https://github.com/douglowder))
### 🐛 Bug fixes
- Make `EXPO_PUBLIC_` env vars plain text, rest sensitive ([#3121](https://github.com/expo/eas-cli/pull/3121) by [@kadikraman](https://github.com/kadikraman))
## [16.17.4](https://github.com/expo/eas-cli/releases/tag/v16.17.4) - 2025-07-31
### 🧹 Chores
- Introduce new release process and workflow. ([#3119](https://github.com/expo/eas-cli/pull/3119) by [@douglowder](https://github.com/douglowder))
- SSO Login: print browser url ([#3113](https://github.com/expo/eas-cli/pull/3113) by [@BackSlasher](https://github.com/BackSlasher))
## [16.17.2](https://github.com/expo/eas-cli/releases/tag/v16.17.2) - 2025-07-23
## [16.17.1](https://github.com/expo/eas-cli/releases/tag/v16.17.1) - 2025-07-23
## [16.17.0](https://github.com/expo/eas-cli/releases/tag/v16.17.0) - 2025-07-18
@@ -1387,7 +1854,6 @@ This is the log of notable changes to EAS CLI and related packages.
- Add rollback disambiguation command. ([#2004](https://github.com/expo/eas-cli/pull/2004) by [@wschurman](https://github.com/wschurman))
- Detect devices that fail to be provisioned, list them to the user and show the explanation message with the link to the devices page to check actual status. ([#2011](https://github.com/expo/eas-cli/pull/2011) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
- Add info to EAS Update asset upload process about asset counts and limits. ([#2013](https://github.com/expo/eas-cli/pull/2013) by [@wschurman](https://github.com/wschurman))
- .nvmrc support for setting node version. ([#1954](https://github.com/expo/eas-cli/pull/1954) by [@khamilowicz](https://github.com/khamilowicz))
### 🐛 Bug fixes
+355
View File
@@ -0,0 +1,355 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Monorepo Structure
This is a **Lerna-based monorepo** containing the EAS CLI and all supporting build libraries.
### CLI & Configuration Packages
1. **[packages/eas-cli](./packages/eas-cli/CLAUDE.md)** - The main CLI tool (published as `eas-cli`)
- 95+ commands organized by domain (build, submit, update, channel, credentials, etc.)
- Built on oclif (Open CLI Framework)
- Entry point: `packages/eas-cli/bin/run`
2. **[packages/eas-json](./packages/eas-json/CLAUDE.md)** - EAS configuration parser (published as `@expo/eas-json`)
- Validates `eas.json` configuration files using Joi schemas
- Manages build, submit, and deploy profiles
- Supports JSON5 via golden-fleece
3. **[packages/eas-build-cache-provider](./packages/eas-build-cache-provider/CLAUDE.md)** - Build cache plugin (published as `eas-build-cache-provider`)
- Optimizes build caching for Expo CLI
### Build Execution Packages
4. **[packages/eas-build-job](./packages/eas-build-job)** - Build job definitions (published as `@expo/eas-build-job`)
- Defines all data structures for build operations (Job, BuildPhase, BuildMode, Platform, Workflow)
- Provides type definitions and validation schemas (Zod, Joi)
- Contains BuildPhase enum that defines the traditional build pipeline stages
- Key exports: `Job`, `BuildPhase`, `BuildMode`, `BuildTrigger`, `Workflow`, `ArchiveSource`
- **Foundation that all other build packages depend on**
5. **[packages/build-tools](./packages/build-tools)** - Build execution engine (published as `@expo/build-tools`)
- Orchestrates all build operations through `BuildContext<T extends Job>`
- Contains platform-specific builders: `androidBuilder()`, `iosBuilder()`, `runCustomBuildAsync()`
- Manages build phases, artifact uploading, caching, credentials
- Provides functions for custom builds
- Integrates with GraphQL API
6. **[packages/steps](./packages/steps)** - Custom build workflow engine (published as `@expo/steps`)
- Framework for defining and executing custom build steps
- Key abstractions:
- `BuildWorkflow`: Orchestrates sequential step execution
- `BuildStep`: Individual executable unit with inputs/outputs
- `BuildStepGlobalContext`: Manages shared state and interpolation
- `BuildStepContext`: Per-step execution context
- Supports conditional execution with `if` expressions (using jsep)
- Template interpolation: `${{ steps.step-id.outputs.outputName }}`
- Parses build configs from YAML/JSON
7. **[packages/local-build-plugin](./packages/local-build-plugin)** - Local build execution (published as `eas-cli-local-build-plugin`)
- Allows running EAS builds locally on developer machines
- Entry point: `packages/local-build-plugin/src/main.ts`
- Sets `EAS_BUILD_RUNNER=local-build-plugin` environment variable
- Reuses all build-tools logic for consistency with cloud builds
8. **[packages/worker](./packages/worker/CLAUDE.md)** - Turtle Worker service (private, not published)
- Runs on EAS build VMs/pods
- WebSocket server for communication with Turtle Launcher
- Wraps `@expo/build-tools` to execute actual React Native builds
### Supporting Packages
9. **[packages/logger](./packages/logger)** - Bunyan-based structured logging (published as `@expo/logger`)
- Used by all build packages
10. **[packages/downloader](./packages/downloader)** - HTTP file downloading with retry logic (published as `@expo/downloader`)
11. **[packages/turtle-spawn](./packages/turtle-spawn)** - Child process spawning with error handling (published as `@expo/turtle-spawn`)
12. **[packages/template-file](./packages/template-file)** - Lodash-based template string interpolation (published as `@expo/template-file`)
13. **[packages/create-eas-build-function](./packages/create-eas-build-function)** - CLI scaffolding tool for custom build functions (published as `create-eas-build-function`)
14. **[packages/expo-cocoapods-proxy](./packages/expo-cocoapods-proxy)** - Ruby gem for CocoaPods proxy (published as `expo-cocoapods-proxy` gem)
> **Note**: Some packages have their own CLAUDE.md with package-specific guidance. Click the links above to view them.
## Common Development Commands
### Setup
```bash
yarn install # Install all dependencies
yarn build # Build all packages in the monorepo
```
### Development
```bash
yarn start # Watch mode with parallel builds across all packages
yarn watch # Alias for start
# Development with relaxed TypeScript rules (allows unused variables)
yarn start-allow-unused
```
### Testing
```bash
yarn run -T lerna run test # Run all tests across packages
yarn run -T lerna run test -- --watch # Watch mode across packages (heavier)
#
# If you use `yarn test`, remember to pass args through two separators:
# yarn test -- -- --watch
# Run tests for a specific package
cd packages/eas-cli && yarn test
cd packages/eas-json && yarn test
cd packages/worker && yarn test
cd packages/build-tools && yarn test
cd packages/steps && yarn test
```
### Type Checking & Linting
```bash
yarn typecheck # TypeScript type check all packages
yarn lint # Oxlint across all packages
yarn fmt:check # Oxfmt format check
```
### Running EAS CLI Locally
```bash
# From repository root:
yarn eas <command> # Run via root package script
# Or use the binary directly:
packages/eas-cli/bin/run <command>
# Recommended: Create an alias for development
alias easd="$(pwd)/packages/eas-cli/bin/run"
easd build --help
```
### Local Build Testing
Set up environment variables for testing local builds:
```bash
export EAS_LOCAL_BUILD_PLUGIN_PATH=$HOME/expo/eas-cli/bin/eas-cli-local-build-plugin
export EAS_LOCAL_BUILD_WORKINGDIR=$HOME/expo/eas-build-workingdir
export EAS_LOCAL_BUILD_SKIP_CLEANUP=1
export EAS_LOCAL_BUILD_ARTIFACTS_DIR=$HOME/expo/eas-build-workingdir/results
# Then run build with --local flag in eas-cli
eas build --local
```
## Build System
Each package has independent TypeScript compilation:
- `eas-cli`: `src/` → `build/`
- `eas-json`: `src/` → `build/`
- `eas-build-cache-provider`: `src/` → `build/`
- `worker`: `src/` → `dist/`
- `build-tools`: `src/` → `dist/`
- `eas-build-job`: `src/` → `dist/`
- `steps`: `src/` → `dist/` (CommonJS only)
- `local-build-plugin`: `src/` → `dist/`
- Other packages: `src/` → `dist/`
TypeScript configs:
- `tsconfig.json` - Base configuration (extends @tsconfig/node20)
- `tsconfig.build.json` - Production builds
- `tsconfig.allowUnused.json` - Development with relaxed rules
## Key Architectural Patterns
### Build Phases
Most traditional build operations are wrapped in phases for tracking:
```typescript
await ctx.runBuildPhase(BuildPhase.INSTALL_DEPENDENCIES, async () => {
// Phase logic here
});
```
Phases can be marked as skipped, warning, or failed for granular reporting.
### Context Objects
- **BuildContext** (`build-tools`): For traditional builds, wraps Job, manages phases/artifacts/caching
- **CustomBuildContext** (`build-tools`): Implements `ExternalBuildContextProvider`, bridges BuildContext to steps framework, used in custom builds and generic jobs
- **BuildStepGlobalContext** (`steps`): Manages step outputs, interpolation, shared state
- **BuildStepContext** (`steps`): Per-step context with working directory and logger
### Custom Build Steps
Steps are defined with:
- `id`: Unique identifier
- `name`: Display name
- `run`: Command or function reference
- `if`: Optional condition (`${{ always() }}`, `${{ success() }}`, etc.)
- `inputs`: Key-value inputs to the step
- `outputs`: Named outputs accessible to later steps
Built-in step functions are in `packages/build-tools/src/steps/functions/`
### Conditional Execution
Uses jsep for expression evaluation:
```yaml
if: ${{ steps.previous_step.outputs.success == 'true' && env.ENVIRONMENT == 'production' }}
```
### Artifact Management
Artifacts tracked as `ArtifactToUpload`:
- Managed artifacts (APK, IPA, AAB) with specific handling
- Generic artifacts for any file
- Upload via `ctx.uploadArtifact()` or `upload-artifact` step
## Package Interdependencies
```
eas-cli → @expo/eas-json
→ @expo/build-tools → @expo/eas-build-job
→ @expo/steps → @expo/eas-build-job
→ @expo/logger
→ @expo/turtle-spawn
→ @expo/downloader
→ @expo/template-file
local-build-plugin → @expo/build-tools → @expo/eas-build-job
→ @expo/turtle-spawn
worker → @expo/build-tools
```
Most packages depend on `@expo/eas-build-job` as the source of truth for types.
## Testing Architecture
- **Framework**: Jest with per-package configuration
- **Shared base config**: `jest/jest.shared.config.ts`
- **Test locations**: `__tests__/` directories alongside source files
- **Mocking**:
- File system: `memfs`
- HTTP: `nock`
- Utilities: `ts-mockito`, `mockdate`
Run a single test file:
```bash
cd packages/eas-cli && yarn test <path-to-test-file>
# Example:
cd packages/eas-cli && yarn test src/project/__tests__/projectUtils-test.ts
```
## Common Development Scenarios
### Adding a Built-in Step Function
1. Create file in `packages/build-tools/src/steps/functions/yourFunction.ts`
2. Export `createYourFunctionBuildFunction()` following existing patterns
3. Add to `getEasFunctions()` in `packages/build-tools/src/steps/functions/easFunctions.ts`
4. Function receives `BuildStepContext` and input/output maps
### Adding Error Detection
1. Add pattern detection in `packages/build-tools/src/buildErrors/detectError.ts`
2. Implement resolver for better error messages
3. Helps users understand and fix build failures
### Working with Platform-Specific Builders
- **Android builder** (`packages/build-tools/src/builders/android.ts`): Gradle-based, handles APK/AAB generation, also see `functionGroups/build.ts`
- **iOS builder** (`packages/build-tools/src/builders/ios.ts`): Fastlane/Xcode-based, handles IPA generation, also see `functionGroups/build.ts`
- Both use `runBuilderWithHooksAsync()` which runs build result hooks (on-success, on-error, on-complete) from package.json
### Introducing Breaking Changes to Job API (@expo/eas-build-job)
If you want to introduce breaking changes to the `@expo/eas-build-job` package, contact one of the CODEOWNERS to coordinate changes with EAS build servers and GraphQL API. Describe what changes you want to make and why. After everything is deployed to production, you can introduce a PR that relies on the new implementation.
## Environment Variables
```bash
# API Configuration
export EXPO_STAGING=1 # Use staging API (https://staging-api.expo.dev)
export EXPO_LOCAL=1 # Use local API (http://127.0.0.1:3000)
# Development
export EAS_NO_VCS=1 # Disable version control checks
export EXPO_DEBUG=1 # Enable debug logging
```
## Code Style & Conventions
### Validation
All changes should be validated before committing. At minimum run package-level tests (e.g. `jest-unit`)
for touched code, then run type checks/linting and format the repo:
```bash
yarn fmt # Apply Oxfmt formatting
yarn typecheck # Validate TypeScript types
yarn lint # Run Oxlint
yarn fmt:check # Run Oxfmt format check
```
### Logging
Use the `Log` module from `@expo/logger`:
```typescript
import { Log } from '@expo/logger';
Log.log('Info message');
Log.warn('Warning message');
Log.error('Error message');
```
### Pull Request conventions
When creating pull requests, make sure to adhere to [PULL_REQUEST_TEMPLATE](./.github/PULL_REQUEST_TEMPLATE).
### Commit Messages
- When possible, prepend commit messages with `[PACKAGE-BEING-CHANGED]`, e.g. `[steps] Add new step function`
- Do not use prefixes like `chore:` and `feat:`
- Commit messages should be concise. Only complex changes should be longer than one line
- Commit changes in logical groups
## Release Process
1. Update `CHANGELOG.md` in the appropriate package
2. Version bump is automated based on changelog:
- Breaking changes → MAJOR
- New features → MINOR
- Otherwise → PATCH
3. GitHub Actions workflow handles release automation
4. Notifications sent to Slack #eas-cli channel
## Licensing
This repository contains packages under different licenses:
- **MIT License**: `eas-cli`, `@expo/eas-json`, `@expo/eas-build-job`, `eas-build-cache-provider`
- **BUSL-1.1 (Business Source License)**: `@expo/build-tools`, `@expo/steps`, `@expo/logger`, `@expo/downloader`, `@expo/turtle-spawn`, `@expo/template-file`, `eas-cli-local-build-plugin`
See `LICENSE` (MIT) and `LICENSE-BUSL` (BUSL-1.1) for details.
## Important Notes
- **Node Version**: Requires Node.js (managed via Mise)
- **Package Manager**: Uses Yarn 4.12.0
- **Compilation Target**: CommonJS with Node resolution
+16 -7
View File
@@ -1,12 +1,21 @@
# Development
Set up an alias for the EAS CLI so you can try it in projects all around your computer. The project is compiled on the fly so you don't need to run a build (watch) command.
## Getting Started
> The only exception is when you just cloned the repository or there have been changes in `packages/eas-json`. In that case, you'll have to run `yarn build` in the root.
If this is your first time running `eas-cli` locally, make sure you install and build the packages. In the project root, run:
```sh
yarn install
yarn build
```
From there, set up an alias for the EAS CLI so you can try it in projects all around your computer. The project is compiled on the fly so you don't need to run a build (watch) command.
> The only exception is if there have been changes in `packages/eas-json`. In that case, you'll have to run `yarn build` in the root.
Open your `.zshrc` or other config file and add:
```
```sh
alias easd="/PATH/TO/eas-cli/bin/run"
```
@@ -14,7 +23,7 @@ Then use it with `easd` like `easd build`.
If you're making changes to `packages/eas-json` or prefer/need to work with production code, start the build command in watch mode with:
```
```sh
yarn start
```
@@ -33,7 +42,7 @@ If it is easier for you to work with the code locally with compiler options set
you can consider using:
```
```sh
yarn start-allow-unused
```
@@ -56,11 +65,11 @@ For development against staging API:
## Working on local builds (`eas build --local`)
See https://github.com/expo/eas-build/blob/main/DEVELOPMENT.md for how to set up your environment when making changes to [`eas-cli-local-build-plugin`](https://github.com/expo/eas-build/tree/main/packages/local-build-plugin) and/or [`build-tools`](https://github.com/expo/eas-build/tree/main/packages/build-tools).
See [`CLAUDE.md`](./CLAUDE.md) for how to set up your environment when making changes to [`eas-cli-local-build-plugin`](./packages/local-build-plugin) and/or [`build-tools`](./packages/build-tools).
## Testing
Run `yarn test` either in the repository root or in a package directory that you're working on.
From the repository root, run `yarn run -T lerna run test`. For a single package, run `yarn test` in that package directory.
## Releasing
+100
View File
@@ -0,0 +1,100 @@
Business Source License 1.1
Parameters
Licensor: 650 Industries, Inc.
Licensed Work: EAS Build
The Licensed Work is (c) 2021 650 Industries, Inc.
Additional Use Grant: You may make use of the Licensed Work, provided that you do
not use the Licensed Work for commercial offerings such as
a CI/CD service or application build service that allows
third parties (other than your employees and contractors)
to access the functionality of and directly benefit from the
functionality of the Licensed Work.
Change Date: 2028-04-01
Change License: MIT
For information about alternative licensing arrangements for the Software,
please visit: https://expo.dev/pricing/
Notice
The Business Source License (this document, or the "License") is not an Open
Source license. However, the Licensed Work will eventually be made available
under an Open Source License, as stated in this License.
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
"Business Source License" is a trademark of MariaDB Corporation Ab.
-----------------------------------------------------------------------------
Business Source License 1.1
Terms
The Licensor hereby grants you the right to copy, modify, create derivative
works, redistribute, and make non-production use of the Licensed Work. The
Licensor may make an Additional Use Grant, above, permitting limited
production use.
Effective on the Change Date, or the fourth anniversary of the first publicly
available distribution of a specific version of the Licensed Work under this
License, whichever comes first, the Licensor hereby grants you rights under
the terms of the Change License, and the rights granted in the paragraph
above terminate.
If your use of the Licensed Work does not comply with the requirements
currently in effect as described in this License, you must purchase a
commercial license from the Licensor, its affiliated entities, or authorized
resellers, or you must refrain from using the Licensed Work.
All copies of the original and modified Licensed Work, and derivative works
of the Licensed Work, are subject to this License. This License applies
separately for each version of the Licensed Work and the Change Date may vary
for each version of the Licensed Work released by Licensor.
You must conspicuously display this License on each original or modified copy
of the Licensed Work. If you receive the Licensed Work in original or
modified form from a third party, the terms and conditions set forth in this
License apply to your use of that work.
Any use of the Licensed Work in violation of this License will automatically
terminate your rights under this License for the current and all other
versions of the Licensed Work.
This License does not grant you any right in any trademark or logo of
Licensor or its affiliates (provided that you may use a trademark or logo of
Licensor as expressly required by this License).
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
TITLE.
MariaDB hereby grants you permission to use this License’s text to license
your works, and to refer to it using the trademark "Business Source License",
as long as you comply with the Covenants of Licensor below.
Covenants of Licensor
In consideration of the right to use this License’s text and the "Business
Source License" name and trademark, Licensor covenants to MariaDB, and to all
other recipients of the licensed work to be provided by Licensor:
1. To specify as the Change License the GPL Version 2.0 or any later version,
or a license that is compatible with GPL Version 2.0 or a later version,
where "compatible" means that software provided under the Change License can
be included in a program with software provided under GPL Version 2.0 or a
later version. Licensor may specify additional Change Licenses without
limitation.
2. To either: (a) specify an additional grant of rights to use that does not
impose any additional restriction on the right granted in this License, as
the Additional Use Grant; or (b) insert the text "None".
3. To specify a Change Date.
4. Not to modify this License in any other way.
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2020-present 650 Industries, Inc. (aka Expo)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1 -1
View File
@@ -1,6 +1,6 @@
# Releasing EAS CLI
1. Run `yarn release` in the repository root folder. The next version is chosen automatically based on the changelog entries. If you want to use different version, pass the version string as a positional argument to the command, e.g. `yarn release 1.2.3`.
1. Invoke the GitHub workflow ["Manually trigger a release"](https://github.com/expo/eas-cli/actions/workflows/trigger-release.yml). The next version is chosen automatically based on the changelog entries. If you want to use different version, pass the version string as an input to the workflow, in semver format, e.g. "1.2.3"
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
## Choosing the next version
+1
View File
@@ -0,0 +1 @@
../packages/local-build-plugin/bin/run
-15
View File
@@ -1,15 +0,0 @@
import type { Config } from 'jest';
// eslint-disable-next-line import/no-relative-packages
import easCliConfig from './packages/eas-cli/jest.config';
// eslint-disable-next-line import/no-relative-packages
import easJsonConfig from './packages/eas-json/jest.config';
const config: Config = {
projects: [easCliConfig, easJsonConfig],
testPathIgnorePatterns: ['.*'],
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
collectCoverageFrom: ['<rootDir>/src/**/*.{ts,tsx,js,jsx}'],
};
export default config;
-9
View File
@@ -1,9 +0,0 @@
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
testRegex: '/__tests__/.*(test|spec)\\.[jt]sx?$',
};
export default config;
+8
View File
@@ -0,0 +1,8 @@
import type { Config } from '@jest/types';
const config: Config.InitialOptions = {
preset: 'ts-jest',
testEnvironment: 'node',
};
export default config;
+2 -2
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "16.17.2",
"version": "19.0.1",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
}
+6
View File
@@ -0,0 +1,6 @@
[env]
_.path = "./tools/bin"
MISE_NODE_COREPACK = "true"
[tools]
node = "20.19.4"
+15 -18
View File
@@ -6,36 +6,33 @@
],
"scripts": {
"build": "lerna run build",
"build:tarballs:linux": "yarn build-for-size-check",
"build-for-size-check": "yarn build && yarn workspace eas-cli pretarball-ci && CLI_SIZE_CHECK=1 yarn workspace eas-cli oclif pack:tarballs --no-xz --targets linux-x64",
"typecheck": "lerna run typecheck",
"start": "lerna run watch --stream --parallel",
"start-allow-unused": "lerna run watch-allow-unused --stream --parallel",
"watch": "yarn start",
"eas": "packages/eas-cli/bin/run",
"lint": "eslint .",
"lint": "oxlint --type-aware --import-plugin --tsconfig ./tsconfig.oxlint.json .",
"fmt": "oxfmt .",
"fmt:check": "oxfmt --check .",
"lint-changelog": "./scripts/bin/run lint-changelog",
"release": "lerna run rebuild && ./scripts/bin/run release",
"test": "jest",
"clean": "lerna run clean && rm -rf node_modules coverage"
"release": "lerna run rebuild --concurrency 1 && ./scripts/bin/run release",
"test": "lerna run test",
"clean": "lerna run clean && rimraf node_modules coverage"
},
"devDependencies": {
"@types/jest": "29.5.11",
"@types/node": "20.11.0",
"eslint": "8.56.0",
"eslint-config-universe": "13.0.0",
"@types/node": "20.14.8",
"eslint-plugin-async-protect": "3.1.0",
"eslint-plugin-node": "11.1.0",
"jest": "29.7.0",
"jest-watch-typeahead": "2.2.2",
"lerna": "8.2.3",
"oclif": "^3.9.0",
"prettier": "3.1.1",
"lerna": "9.0.3",
"oclif": "^4.22.85",
"oxfmt": "0.28.0",
"oxlint": "1.43.0",
"oxlint-tsgolint": "0.11.1",
"rimraf": "3.0.2",
"ts-jest": "29.1.1",
"typescript": "5.3.3"
"typescript": "5.5.4"
},
"volta": {
"node": "20.11.0",
"yarn": "1.22.21"
}
"packageManager": "[email protected]"
}
+9
View File
@@ -0,0 +1,9 @@
node_modules/
dist/
.secrets*
!.secrets.example
!.secrets.test
src/graphql-env.d.ts
schema.graphql
+17
View File
@@ -0,0 +1,17 @@
{
"extends": ["../../.oxlintrc.json"],
"jsPlugins": [
{
"name": "async-protect",
"specifier": "eslint-plugin-async-protect"
}
],
"rules": {
"async-protect/async-suffix": "off",
"no-underscore-dangle": "off",
"node/no-sync": "off",
"no-console": "off",
"no-void": "off",
"no-restricted-imports": "off"
}
}
+7
View File
@@ -0,0 +1,7 @@
# @expo/build-tools
`@expo/build-tools` is the core library for the EAS Build service. It implements the build process for React Native projects and for managed Expo applications.
## Repository
https://github.com/expo/eas-cli/tree/main/packages/build-tools
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -eo pipefail
NAME=$1
VALUE=$2
if [[ -z "$__EAS_BUILD_ENVS_DIR" ]]; then
echo "Set __EAS_BUILD_ENVS_DIR"
exit 1
fi
if [[ -z "$NAME" || -z "$VALUE" ]]; then
echo "Usage: set-env NAME VALUE"
exit 2
fi
if [[ "$NAME" == *"="* ]]; then
echo "Environment name can't include ="
exit 1
fi
echo -n $VALUE > $__EAS_BUILD_ENVS_DIR/$NAME
@@ -0,0 +1,13 @@
import type { Config } from '@jest/types';
import baseConfig from '../../../jest/jest.shared.config';
const config: Config.InitialOptions = {
...baseConfig,
rootDir: '../src',
testMatch: ['**/__integration-tests__/*.test.ts'],
clearMocks: true,
setupFilesAfterEnv: ['<rootDir>/../jest/setup-tests.ts'],
};
export default config;
+20
View File
@@ -0,0 +1,20 @@
import { fs, vol } from 'memfs';
import os from 'node:os';
if (process.env.NODE_ENV !== 'test') {
throw new Error("NODE_ENV environment variable must be set to 'test'.");
}
// Always mock:
jest.mock('fs');
jest.mock('node:fs', () => fs);
jest.mock('fs/promises');
jest.mock('node:fs/promises', () => fs.promises);
jest.mock('node-fetch');
beforeEach(() => {
vol.reset();
vol.fromNestedJSON({
[os.tmpdir()]: {},
});
});
+16
View File
@@ -0,0 +1,16 @@
import type { Config } from '@jest/types';
import baseConfig from '../../../jest/jest.shared.config';
const config: Config.InitialOptions = {
...baseConfig,
rootDir: '../src',
testMatch: [
'**/__tests__/*.test.ts',
...(process.platform === 'darwin' ? ['**/__tests__/*.test.ios.ts'] : []),
],
clearMocks: true,
setupFilesAfterEnv: ['<rootDir>/../jest/setup-tests.ts'],
};
export default config;
+102
View File
@@ -0,0 +1,102 @@
{
"name": "@expo/build-tools",
"version": "19.0.0",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "BUSL-1.1",
"author": "Expo <[email protected]>",
"repository": {
"type": "git",
"url": "https://github.com/expo/eas-cli.git",
"directory": "packages/build-tools"
},
"files": [
"dist",
"resources",
"templates",
"bin"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"start": "yarn watch",
"prewatch": "yarn gql",
"watch": "tsc --watch --preserveWatchOutput",
"prebuild": "yarn gql",
"build": "tsc",
"typecheck": "tsc",
"prepack": "rimraf dist \"*.tsbuildinfo\" && yarn gql && tsc -p tsconfig.build.json",
"jest-unit": "jest --config jest/unit-config.ts",
"jest-integration": "jest --config jest/integration-config.ts",
"jest-unit-watch": "jest --config jest/unit-config.ts --watch",
"jest-integration-watch": "jest --config jest/integration-config.ts --watch",
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\"",
"gql": "gql.tada generate-schema ${API_SERVER_URL:-https://api.expo.dev}/graphql --output ./schema.graphql && gql.tada generate-output",
"gql:local": "API_SERVER_URL=http://api.expo.test yarn gql",
"test": "yarn jest-unit"
},
"dependencies": {
"@expo/config": "55.0.10",
"@expo/config-plugins": "55.0.7",
"@expo/downloader": "19.0.0",
"@expo/eas-build-job": "19.0.0",
"@expo/env": "^0.4.0",
"@expo/logger": "19.0.0",
"@expo/package-manager": "1.9.10",
"@expo/plist": "^0.2.0",
"@expo/results": "^1.0.0",
"@expo/spawn-async": "1.7.2",
"@expo/steps": "19.0.0",
"@expo/template-file": "19.0.0",
"@expo/turtle-spawn": "19.0.0",
"@expo/xcpretty": "^4.3.1",
"@google-cloud/storage": "^7.11.2",
"@sentry/node": "7.77.0",
"@urql/core": "^6.0.1",
"bplist-parser": "0.3.2",
"fast-glob": "^3.3.2",
"fast-xml-parser": "^4.4.1",
"fs-extra": "^11.2.0",
"gql.tada": "^1.8.13",
"joi": "^17.13.1",
"jose": "5.10.0",
"lodash": "^4.17.21",
"node-fetch": "^2.7.0",
"node-forge": "^1.3.1",
"node-stream-zip": "1.15.0",
"nullthrows": "^1.1.1",
"plist": "^3.1.0",
"promise-limit": "^2.7.0",
"promise-retry": "^2.0.1",
"resolve-from": "^5.0.0",
"retry": "^0.13.1",
"semver": "^7.6.2",
"tar": "7.5.7",
"uuid": "9.0.1",
"yaml": "^2.8.1",
"zod": "^4.3.5"
},
"devDependencies": {
"@expo/repack-app": "~0.4.1",
"@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.4",
"@types/node": "20.14.2",
"@types/node-fetch": "^2.6.11",
"@types/node-forge": "^1.3.11",
"@types/plist": "^3.0.5",
"@types/promise-retry": "^1.1.6",
"@types/retry": "^0.12.5",
"@types/semver": "^7.5.8",
"@types/uuid": "^9.0.8",
"bunyan": "^1.8.15",
"jest": "^29.7.0",
"memfs": "^4.17.1",
"nock": "13.4.0",
"rimraf": "3.0.2",
"ts-jest": "^29.1.4",
"ts-mockito": "^2.6.1",
"tslib": "^2.6.3",
"typescript": "^5.5.4",
"uuid": "^9.0.1"
}
}
@@ -0,0 +1,25 @@
import bunyan from 'bunyan';
export function createLogger(): bunyan {
const logger = {
info: () => {},
debug: () => {},
error: () => {},
warn: () => {},
child: _fields => logger,
} as bunyan;
return logger;
}
export enum LoggerLevel {
TRACE = 'trace',
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error',
FATAL = 'fatal',
}
export enum PipeMode {
COMBINED = 'combined',
}
+21
View File
@@ -0,0 +1,21 @@
import { fs } from 'memfs';
const fsRealpath = fs.realpath;
(fsRealpath as any).native = fsRealpath;
const fsRm = (
path: string,
options: object,
callback: (err: NodeJS.ErrnoException | null) => void
): void => {
fs.promises
.rm(path, options)
.then(() => {
callback(null);
})
.catch(err => {
callback(err);
});
};
module.exports = { ...fs, realpath: fsRealpath, rm: fsRm };
@@ -0,0 +1,7 @@
import { fs } from 'memfs';
const fsRm = (path: string, options: object): Promise<void> => {
return fs.promises.rm(path, options);
};
module.exports = { ...fs.promises, rm: fsRm };
@@ -0,0 +1,205 @@
import {
BuildPhase,
BuildPhaseResult,
BuildTrigger,
Job,
Metadata,
Platform,
} from '@expo/eas-build-job';
import { randomUUID } from 'crypto';
import fs from 'fs-extra';
import { vol } from 'memfs';
import { createMockLogger } from './utils/logger';
import { BuildContext } from '../context';
import { Datadog } from '../datadog';
jest.mock('fs');
jest.mock('fs-extra');
jest.mock('../datadog', () => ({
Datadog: {
distribution: jest.fn(),
},
}));
const datadogDistributionMock = jest.mocked(Datadog.distribution);
describe('BuildContext', () => {
beforeEach(() => {
vol.reset();
jest.clearAllMocks();
(fs.readdir as unknown as jest.Mock).mockResolvedValue([]);
});
it('should merge secrets', async () => {
const robotAccessToken = randomUUID();
await vol.promises.mkdir('/workingdir/eas-environment-secrets/', { recursive: true });
const ctx = new BuildContext(
{
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
secrets: {
robotAccessToken,
environmentSecrets: [
{
name: 'TEST_SECRET',
value: 'test-secret-value',
},
],
},
} as Job,
{
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
workingdir: '/workingdir',
logger: createMockLogger(),
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
uploadArtifact: jest.fn(),
}
);
ctx.updateJobInformation({} as Job, {} as Metadata);
expect(ctx.job.secrets).toEqual({
robotAccessToken,
environmentSecrets: [
{
name: 'TEST_SECRET',
value: 'test-secret-value',
},
],
});
const newRobotAccessToken = randomUUID();
ctx.updateJobInformation(
{
secrets: {
robotAccessToken: newRobotAccessToken,
environmentSecrets: [
{
name: 'TEST_SECRET',
value: 'new-test-secret-value',
},
{
name: 'TEST_SECRET_2',
value: 'test-secret-value-2',
},
],
},
} as Job,
{} as Metadata
);
expect(ctx.job.secrets).toEqual({
robotAccessToken: newRobotAccessToken,
environmentSecrets: [
{ name: 'TEST_SECRET', value: 'test-secret-value' },
{ name: 'TEST_SECRET', value: 'new-test-secret-value' },
{ name: 'TEST_SECRET_2', value: 'test-secret-value-2' },
],
});
});
it('should not lose workflowInterpolationContext', async () => {
const robotAccessToken = randomUUID();
await vol.promises.mkdir('/workingdir/eas-environment-secrets/', { recursive: true });
const ctx = new BuildContext(
{
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
secrets: {
robotAccessToken,
environmentSecrets: [
{
name: 'TEST_SECRET',
value: 'test-secret-value',
},
],
},
workflowInterpolationContext: {
foo: 'bar',
} as any,
} as Job,
{
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
workingdir: '/workingdir',
logger: createMockLogger(),
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
uploadArtifact: jest.fn(),
}
);
ctx.updateJobInformation({} as Job, {} as Metadata);
expect(ctx.job.workflowInterpolationContext).toEqual({
foo: 'bar',
});
});
it('emits build phase duration metrics for successful build phases', async () => {
const ctx = createTestBuildContext({ platform: Platform.IOS });
await ctx.runBuildPhase(BuildPhase.RUN_FASTLANE, async () => {});
expect(datadogDistributionMock).toHaveBeenCalledWith(
'eas.build.phase_duration',
expect.any(Number),
{
build_phase: 'run_fastlane',
platform: Platform.IOS,
result: BuildPhaseResult.SUCCESS,
}
);
});
it('emits build phase duration metrics for failed build phases', async () => {
const ctx = createTestBuildContext({ platform: Platform.ANDROID });
await expect(
ctx.runBuildPhase(BuildPhase.INSTALL_DEPENDENCIES, async () => {
throw new Error('boom');
})
).rejects.toThrow();
expect(datadogDistributionMock).toHaveBeenCalledWith(
'eas.build.phase_duration',
expect.any(Number),
{
build_phase: 'install_dependencies',
platform: Platform.ANDROID,
result: BuildPhaseResult.FAIL,
}
);
});
it('does not emit build phase duration metrics for non-platform jobs', async () => {
const ctx = createTestBuildContext({});
await ctx.runBuildPhase(BuildPhase.PREPARE_PROJECT, async () => {});
expect(datadogDistributionMock).not.toHaveBeenCalled();
});
});
function createTestBuildContext({ platform }: { platform?: Platform }): BuildContext {
vol.mkdirSync('/workingdir/env', { recursive: true });
return new BuildContext(
{
...(platform ? { platform } : {}),
builderEnvironment: { env: {} },
} as Job,
{
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
workingdir: '/workingdir',
logger: createMockLogger(),
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
uploadArtifact: jest.fn(),
}
);
}
@@ -0,0 +1,160 @@
import { BuildTrigger, Ios, Metadata } from '@expo/eas-build-job';
import { StepMetric } from '@expo/steps';
import { createTestIosJob } from './utils/job';
import { createMockLogger } from './utils/logger';
import { BuildContext } from '../context';
import { CustomBuildContext } from '../customBuildContext';
import { uploadStepMetricsToWwwAsync } from '../utils/stepMetrics';
jest.mock('../utils/stepMetrics');
const mockUploadStepMetricsToWwwAsync = jest.mocked(uploadStepMetricsToWwwAsync);
describe(CustomBuildContext, () => {
it('should not lose workflowInterpolationContext', () => {
const contextUploadArtifact = jest.fn();
const ctx = new BuildContext(
createTestIosJob({
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
workflowInterpolationContext: {
foo: 'bar',
} as unknown as Ios.Job['workflowInterpolationContext'],
}),
{
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
logger: createMockLogger(),
uploadArtifact: contextUploadArtifact,
workingdir: '',
}
);
const customContext = new CustomBuildContext(ctx);
expect(customContext.job.workflowInterpolationContext).toStrictEqual({
foo: 'bar',
});
customContext.updateJobInformation({} as Ios.Job, {} as Metadata);
expect(customContext.job.workflowInterpolationContext).toStrictEqual({
foo: 'bar',
});
});
describe('reportStepMetric', () => {
const expoApiV2BaseUrl = 'http://exp.test/--/api/v2/';
const workflowJobId = 'test-workflow-job-id';
const robotAccessToken = 'test-robot-token';
const sampleMetric: StepMetric = {
metricsId: 'eas/checkout',
result: 'success',
durationMs: 1000,
platform: 'linux',
};
function createCustomBuildContext(
overrides: {
expoApiV2BaseUrl?: string | undefined;
includeRobotAccessToken?: boolean;
includeWorkflowJobId?: boolean;
} = {}
): CustomBuildContext {
const job = createTestIosJob();
if (overrides.includeRobotAccessToken !== false) {
(job.secrets as any).robotAccessToken = robotAccessToken;
}
const env: Record<string, string> = {
__API_SERVER_URL: 'http://api.expo.test',
};
if (overrides.includeWorkflowJobId !== false) {
env.__WORKFLOW_JOB_ID = workflowJobId;
}
const ctx = new BuildContext(job, {
env,
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
logger: createMockLogger(),
uploadArtifact: jest.fn(),
workingdir: '/workingdir',
expoApiV2BaseUrl:
'expoApiV2BaseUrl' in overrides ? overrides.expoApiV2BaseUrl : expoApiV2BaseUrl,
});
return new CustomBuildContext(ctx);
}
beforeEach(() => {
jest.clearAllMocks();
mockUploadStepMetricsToWwwAsync.mockResolvedValue(undefined);
});
it('calls uploadStepMetricsToWwwAsync when all required fields are present', () => {
const customCtx = createCustomBuildContext();
customCtx.reportStepMetric(sampleMetric);
expect(mockUploadStepMetricsToWwwAsync).toHaveBeenCalledTimes(1);
expect(mockUploadStepMetricsToWwwAsync).toHaveBeenCalledWith(
expect.objectContaining({
workflowJobId,
robotAccessToken,
expoApiV2BaseUrl,
stepMetrics: [sampleMetric],
})
);
});
it('does not call upload when workflowJobId is missing', () => {
const customCtx = createCustomBuildContext({ includeWorkflowJobId: false });
customCtx.reportStepMetric(sampleMetric);
expect(mockUploadStepMetricsToWwwAsync).not.toHaveBeenCalled();
});
it('does not call upload when robotAccessToken is missing', () => {
const customCtx = createCustomBuildContext({ includeRobotAccessToken: false });
customCtx.reportStepMetric(sampleMetric);
expect(mockUploadStepMetricsToWwwAsync).not.toHaveBeenCalled();
});
it('does not call upload when expoApiV2BaseUrl is missing', () => {
const customCtx = createCustomBuildContext({ expoApiV2BaseUrl: undefined });
customCtx.reportStepMetric(sampleMetric);
expect(mockUploadStepMetricsToWwwAsync).not.toHaveBeenCalled();
});
it('drainPendingMetricUploads awaits all pending uploads', async () => {
let resolveUpload!: () => void;
const uploadPromise = new Promise<void>(resolve => {
resolveUpload = resolve;
});
mockUploadStepMetricsToWwwAsync.mockReturnValue(uploadPromise);
const customCtx = createCustomBuildContext();
customCtx.reportStepMetric(sampleMetric);
let drained = false;
const drainPromise = customCtx.drainPendingMetricUploads().then(() => {
drained = true;
});
await new Promise(r => setImmediate(r));
expect(drained).toBe(false);
resolveUpload();
await drainPromise;
expect(drained).toBe(true);
});
it('drainPendingMetricUploads settles even if an upload rejects', async () => {
mockUploadStepMetricsToWwwAsync.mockRejectedValue(new Error('upload failed'));
const customCtx = createCustomBuildContext();
customCtx.reportStepMetric(sampleMetric);
await expect(customCtx.drainPendingMetricUploads()).resolves.not.toThrow();
});
});
});
@@ -0,0 +1,150 @@
import { Response } from 'node-fetch';
import { Datadog } from '../datadog';
import { Sentry } from '../sentry';
import { turtleFetch } from '../utils/turtleFetch';
jest.mock('../sentry', () => ({
Sentry: {
capture: jest.fn(),
},
}));
jest.mock('../utils/turtleFetch', () => ({
turtleFetch: jest.fn(),
}));
const turtleFetchMock = jest.mocked(turtleFetch);
const sentryCaptureMock = jest.mocked(Sentry.capture);
describe('Datadog singleton', () => {
beforeEach(() => {
jest.clearAllMocks();
Datadog.setup(null);
});
it('POSTs distribution metrics to the turtle build metrics endpoint', () => {
turtleFetchMock.mockResolvedValueOnce({} as Response);
Datadog.setup({
expoApiV2BaseUrl: 'https://api.expo.test/v2/',
turtleBuildId: 'build-id',
robotAccessToken: 'token-abc',
});
Datadog.distribution('eas.build.phase_duration', 1234, {
build_phase: 'install_dependencies',
platform: 'ios',
result: 'success',
});
expect(turtleFetchMock).toHaveBeenCalledWith(
'https://api.expo.test/v2/turtle-builds/build-id/metrics',
'POST',
{
json: {
metrics: [
{
name: 'eas.build.phase_duration',
type: 'distribution',
value: 1234,
tags: {
build_phase: 'install_dependencies',
platform: 'ios',
result: 'success',
},
},
],
},
headers: { Authorization: 'Bearer token-abc' },
retries: 2,
}
);
});
it('is a no-op when setup is null', () => {
Datadog.setup(null);
Datadog.distribution('eas.build.phase_duration', 1);
expect(turtleFetchMock).not.toHaveBeenCalled();
});
it('swallows upload failures', async () => {
turtleFetchMock.mockRejectedValueOnce(new Error('network down'));
Datadog.setup({
expoApiV2BaseUrl: 'https://api.expo.test/v2/',
turtleBuildId: 'build-id',
robotAccessToken: 'token-abc',
});
Datadog.distribution('eas.build.phase_duration', 1);
await flushPromises();
expect(turtleFetchMock).toHaveBeenCalled();
expect(sentryCaptureMock).toHaveBeenCalledWith(
'Failed to report turtle build metric',
expect.any(Error),
expect.objectContaining({
extras: {
metrics: [{ name: 'eas.build.phase_duration', type: 'distribution', value: 1, tags: {} }],
},
})
);
});
it('uses the latest setup options', () => {
turtleFetchMock.mockResolvedValue({} as Response);
Datadog.setup({
expoApiV2BaseUrl: 'https://api.expo.test/v2/',
turtleBuildId: 'first-build-id',
robotAccessToken: 'first-token',
});
Datadog.setup({
expoApiV2BaseUrl: 'https://api.expo.test/v2/',
turtleBuildId: 'second-build-id',
robotAccessToken: 'second-token',
});
Datadog.distribution('eas.build.phase_duration', 1);
expect(turtleFetchMock).toHaveBeenCalledWith(
'https://api.expo.test/v2/turtle-builds/second-build-id/metrics',
'POST',
expect.objectContaining({
headers: { Authorization: 'Bearer second-token' },
})
);
});
it('flushes pending metric uploads', async () => {
let resolveUpload!: (response: Response) => void;
turtleFetchMock.mockReturnValueOnce(
new Promise<Response>(resolve => {
resolveUpload = resolve;
})
);
Datadog.setup({
expoApiV2BaseUrl: 'https://api.expo.test/v2/',
turtleBuildId: 'build-id',
robotAccessToken: 'token-abc',
});
let flushed = false;
Datadog.distribution('eas.build.phase_duration', 1);
const flushPromise = Datadog.flushAsync().then(() => {
flushed = true;
});
await Promise.resolve();
expect(flushed).toBe(false);
resolveUpload({} as Response);
await flushPromise;
expect(flushed).toBe(true);
});
});
async function flushPromises(): Promise<void> {
await new Promise(resolve => setImmediate(resolve));
}
@@ -0,0 +1,128 @@
import { BuildPhase } from '@expo/eas-build-job';
import { BuildStepGlobalContext, StepsConfigParser } from '@expo/steps';
import { runGenericJobAsync } from '../generic';
import { CustomBuildContext } from '../customBuildContext';
import { uploadJobOutputsToWwwAsync } from '../utils/outputs';
jest.mock('../customBuildContext');
jest.mock('../utils/outputs');
jest.mock('../common/projectSources');
jest.mock('../steps/easFunctions', () => ({ getEasFunctions: jest.fn().mockReturnValue([]) }));
jest.mock('../steps/easFunctionGroups', () => ({
getEasFunctionGroups: jest.fn().mockReturnValue([]),
}));
jest.mock('@expo/steps');
jest.mock('fs/promises');
const mockUploadJobOutputsToWwwAsync = jest.mocked(uploadJobOutputsToWwwAsync);
describe(runGenericJobAsync, () => {
const expoApiV2BaseUrl = 'http://exp.test/--/api/v2/';
let mockDrainPendingMetricUploads: jest.Mock;
let mockCtx: any;
beforeEach(() => {
jest.clearAllMocks();
mockDrainPendingMetricUploads = jest.fn().mockResolvedValue(undefined);
(CustomBuildContext as unknown as jest.Mock).mockImplementation(() => ({
projectSourceDirectory: '/tmp/src',
env: { __WORKFLOW_JOB_ID: 'test-workflow-job-id' },
drainPendingMetricUploads: mockDrainPendingMetricUploads,
}));
(BuildStepGlobalContext as unknown as jest.Mock).mockImplementation(() => ({
env: { __WORKFLOW_JOB_ID: 'test-workflow-job-id' },
}));
mockCtx = {
expoApiV2BaseUrl,
job: {
secrets: { robotAccessToken: 'test-token' },
steps: [],
},
logger: {
info: jest.fn(),
debug: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
child: jest.fn().mockReturnThis(),
},
runBuildPhase: jest.fn(async (_phase: BuildPhase, fn: () => Promise<any>) => fn()),
};
mockUploadJobOutputsToWwwAsync.mockResolvedValue(undefined);
});
it('awaits drainPendingMetricUploads in COMPLETE_JOB phase', async () => {
const mockWorkflow = { executeAsync: jest.fn().mockResolvedValue(undefined) };
(StepsConfigParser as unknown as jest.Mock).mockImplementation(() => ({
parseAsync: jest.fn().mockResolvedValue(mockWorkflow),
}));
let resolveDrain!: () => void;
mockDrainPendingMetricUploads.mockReturnValue(
new Promise<void>(resolve => {
resolveDrain = resolve;
})
);
let resolved = false;
const resultPromise = runGenericJobAsync(mockCtx).then(() => {
resolved = true;
});
await new Promise(r => setImmediate(r));
expect(resolved).toBe(false);
resolveDrain();
await resultPromise;
expect(resolved).toBe(true);
});
it('awaits drainPendingMetricUploads even when outputs upload fails', async () => {
const mockWorkflow = { executeAsync: jest.fn().mockResolvedValue(undefined) };
(StepsConfigParser as unknown as jest.Mock).mockImplementation(() => ({
parseAsync: jest.fn().mockResolvedValue(mockWorkflow),
}));
mockUploadJobOutputsToWwwAsync.mockRejectedValue(new Error('outputs upload failed'));
let resolveDrain!: () => void;
mockDrainPendingMetricUploads.mockReturnValue(
new Promise<void>(resolve => {
resolveDrain = resolve;
})
);
let rejected = false;
const resultPromise = runGenericJobAsync(mockCtx).catch(err => {
rejected = true;
throw err;
});
await new Promise(r => setImmediate(r));
expect(rejected).toBe(false);
resolveDrain();
await expect(resultPromise).rejects.toThrow('outputs upload failed');
expect(rejected).toBe(true);
});
it('throws before workflow execution when expoApiV2BaseUrl is missing', async () => {
mockCtx.expoApiV2BaseUrl = undefined;
const mockWorkflow = { executeAsync: jest.fn() };
(StepsConfigParser as unknown as jest.Mock).mockImplementation(() => ({
parseAsync: jest.fn().mockResolvedValue(mockWorkflow),
}));
await expect(runGenericJobAsync(mockCtx)).rejects.toThrow(
'expoApiV2BaseUrl is required for generic jobs'
);
expect(CustomBuildContext).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,256 @@
import * as sentryNode from '@sentry/node';
import { Sentry } from '../sentry';
jest.mock('@sentry/node');
describe('Sentry singleton', () => {
beforeEach(() => {
jest.mocked(sentryNode.init).mockReset();
jest.mocked(sentryNode.flush).mockReset();
jest.mocked(sentryNode.captureException).mockReset();
jest.mocked(sentryNode.captureMessage).mockReset();
jest.mocked(sentryNode.withScope).mockReset();
});
describe('setup', () => {
it('initializes the SDK with dsn + environment when dsn is non-null', () => {
Sentry.setup({ dsn: 'https://[email protected]/1', environment: 'production' });
expect(sentryNode.init).toHaveBeenCalledWith({
dsn: 'https://[email protected]/1',
environment: 'production',
});
});
it('passes default tags into the SDK init as initialScope', () => {
Sentry.setup({
dsn: 'https://[email protected]/1',
environment: 'production',
tags: { service: 'worker:darwin' },
});
expect(sentryNode.init).toHaveBeenCalledWith({
dsn: 'https://[email protected]/1',
environment: 'production',
initialScope: { tags: { service: 'worker:darwin' } },
});
});
it('skips SDK init when dsn is null', () => {
Sentry.setup({ dsn: null, environment: 'development' });
expect(sentryNode.init).not.toHaveBeenCalled();
});
it('passes the latest initialScope tags when setup is called more than once', () => {
Sentry.setup({
dsn: 'https://[email protected]/1',
environment: 'test',
tags: { service: 'first' },
});
Sentry.setup({
dsn: 'https://[email protected]/1',
environment: 'test',
tags: { service: 'second' },
});
expect(sentryNode.init).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ initialScope: { tags: { service: 'second' } } })
);
});
});
describe('capture', () => {
it('routes (msg) to captureMessage with msg only', () => {
const scope = mockWithScope();
Sentry.capture('plain message');
expect(sentryNode.captureMessage).toHaveBeenCalledWith('plain message');
expect(sentryNode.captureException).not.toHaveBeenCalled();
expect(scope.setExtra).not.toHaveBeenCalled();
});
it('routes (msg, options) and sets per-call tags/extras/level on the scope', () => {
const scope = mockWithScope();
Sentry.capture('something happened', {
tags: { phase: 'running-xclogparser' },
extras: { buildId: 'abc' },
level: 'warning',
});
expect(scope.setTags).toHaveBeenCalledWith({ phase: 'running-xclogparser' });
expect(scope.setExtras).toHaveBeenCalledWith({ buildId: 'abc' });
expect(scope.setLevel).toHaveBeenCalledWith('warning');
expect(sentryNode.captureMessage).toHaveBeenCalledWith('something happened');
expect(sentryNode.captureException).not.toHaveBeenCalled();
});
it('routes (msg, err) to captureException and sets msg as `message` extra when distinct from err.message', () => {
const scope = mockWithScope();
const err = new Error('boom');
Sentry.capture('something went wrong', err);
expect(scope.setExtra).toHaveBeenCalledWith('message', 'something went wrong');
expect(sentryNode.captureException).toHaveBeenCalledWith(err);
expect(sentryNode.captureMessage).not.toHaveBeenCalled();
});
it('routes (msg, err, options) to captureException with per-call tags/extras', () => {
const scope = mockWithScope();
const err = new Error('spawn ENOENT');
Sentry.capture('xclogparser failed', err, {
tags: { phase: 'running-xclogparser' },
extras: { buildId: 'abc' },
});
expect(scope.setTags).toHaveBeenCalledWith({ phase: 'running-xclogparser' });
expect(scope.setExtras).toHaveBeenCalledWith({ buildId: 'abc' });
expect(scope.setExtra).toHaveBeenCalledWith('message', 'xclogparser failed');
expect(sentryNode.captureException).toHaveBeenCalledWith(err);
});
it('does not set the `message` extra when msg equals err.message', () => {
const scope = mockWithScope();
const err = new Error('build failed');
Sentry.capture('build failed', err);
expect(scope.setExtra).not.toHaveBeenCalledWith('message', expect.anything());
expect(sentryNode.captureException).toHaveBeenCalledWith(err);
});
it('routes (err) alone to captureException with no scope mutations', () => {
const scope = mockWithScope();
const err = new Error('boom');
Sentry.capture(err);
expect(scope.setTags).not.toHaveBeenCalled();
expect(scope.setExtras).not.toHaveBeenCalled();
expect(scope.setLevel).not.toHaveBeenCalled();
expect(scope.setExtra).not.toHaveBeenCalled();
expect(sentryNode.captureException).toHaveBeenCalledWith(err);
expect(sentryNode.captureMessage).not.toHaveBeenCalled();
});
it('treats (msg, null) as a no-err call (parity with 3-arg null behavior)', () => {
const scope = mockWithScope();
Sentry.capture('forwarded null', null as unknown as Error);
expect(scope.setExtra).not.toHaveBeenCalled();
expect(sentryNode.captureMessage).toHaveBeenCalledWith('forwarded null');
expect(sentryNode.captureException).not.toHaveBeenCalled();
});
it('treats (msg, null, options) as a no-err call', () => {
const scope = mockWithScope();
Sentry.capture('forwarded null with opts', null as unknown as Error, {
tags: { phase: 'cleanup' },
});
expect(scope.setTags).toHaveBeenCalledWith({ phase: 'cleanup' });
expect(scope.setExtra).not.toHaveBeenCalled();
expect(sentryNode.captureMessage).toHaveBeenCalledWith('forwarded null with opts');
expect(sentryNode.captureException).not.toHaveBeenCalled();
});
it('treats (msg, undefined, options) as a no-err call (forwarding `Error | undefined` typed vars)', () => {
const scope = mockWithScope();
Sentry.capture('forwarded with no err', undefined, {
tags: { phase: 'install-dependencies' },
extras: { buildId: 'abc' },
});
expect(scope.setTags).toHaveBeenCalledWith({ phase: 'install-dependencies' });
expect(scope.setExtras).toHaveBeenCalledWith({ buildId: 'abc' });
expect(scope.setExtra).not.toHaveBeenCalled();
expect(sentryNode.captureMessage).toHaveBeenCalledWith('forwarded with no err');
expect(sentryNode.captureException).not.toHaveBeenCalled();
});
it('coerces 2-arg primitive thrown value to Error (e.g. `throw "boom"`)', () => {
mockWithScope();
Sentry.capture('caught a string', 'boom' as unknown as Error);
expect(sentryNode.captureException).toHaveBeenCalledWith(expect.any(Error));
const coerced = jest.mocked(sentryNode.captureException).mock.calls[0][0] as Error;
expect(coerced.message).toBe('boom');
expect(sentryNode.captureMessage).not.toHaveBeenCalled();
});
it('coerces non-Error middle arg to Error when caller passes 3 args (e.g. `catch (err: any)`)', () => {
const scope = mockWithScope();
Sentry.capture('something failed', 'raw string thrown' as unknown as Error, {
tags: { phase: 'install-dependencies' },
extras: { buildId: 'abc' },
});
expect(scope.setTags).toHaveBeenCalledWith({ phase: 'install-dependencies' });
expect(scope.setExtras).toHaveBeenCalledWith({ buildId: 'abc' });
expect(scope.setExtra).toHaveBeenCalledWith('message', 'something failed');
expect(sentryNode.captureException).toHaveBeenCalledWith(expect.any(Error));
const coerced = jest.mocked(sentryNode.captureException).mock.calls[0][0] as Error;
expect(coerced.message).toBe('raw string thrown');
expect(sentryNode.captureMessage).not.toHaveBeenCalled();
});
it('routes (err, options) to captureException with no message extra', () => {
const scope = mockWithScope();
const err = new Error('boom');
Sentry.capture(err, { tags: { phase: 'install-dependencies' }, level: 'warning' });
expect(scope.setTags).toHaveBeenCalledWith({ phase: 'install-dependencies' });
expect(scope.setLevel).toHaveBeenCalledWith('warning');
expect(scope.setExtra).not.toHaveBeenCalled();
expect(sentryNode.captureException).toHaveBeenCalledWith(err);
expect(sentryNode.captureMessage).not.toHaveBeenCalled();
});
});
describe('flush', () => {
it('forwards the timeout to the SDK and resolves with its result', async () => {
jest.mocked(sentryNode.flush).mockResolvedValue(true);
const result = await Sentry.flush(3000);
expect(sentryNode.flush).toHaveBeenCalledWith(3000);
expect(result).toBe(true);
});
it('defaults the timeout to 2000ms when not specified', async () => {
jest.mocked(sentryNode.flush).mockResolvedValue(true);
await Sentry.flush();
expect(sentryNode.flush).toHaveBeenCalledWith(2000);
});
});
});
function mockWithScope(): {
setTags: jest.Mock;
setExtras: jest.Mock;
setExtra: jest.Mock;
setLevel: jest.Mock;
} {
const scope = {
setTags: jest.fn(),
setExtras: jest.fn(),
setExtra: jest.fn(),
setLevel: jest.fn(),
};
jest.mocked(sentryNode.withScope).mockImplementation((cb: any) => {
cb(scope as unknown as sentryNode.Scope);
return undefined as any;
});
return scope;
}
@@ -0,0 +1,100 @@
import { bunyan } from '@expo/logger';
import {
BuildRuntimePlatform,
BuildStepContext,
BuildStepEnv,
BuildStepGlobalContext,
ExternalBuildContextProvider,
} from '@expo/steps';
import os from 'os';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { createMockLogger } from './logger';
export class MockContextProvider implements ExternalBuildContextProvider {
private _env: BuildStepEnv = {};
constructor(
public readonly logger: bunyan,
public readonly runtimePlatform: BuildRuntimePlatform,
public readonly projectSourceDirectory: string,
public readonly projectTargetDirectory: string,
public readonly defaultWorkingDirectory: string,
public readonly buildLogsDirectory: string,
public readonly staticContextContent: Record<string, any> = {}
) {}
public get env(): BuildStepEnv {
return this._env;
}
public staticContext(): any {
return { ...this.staticContextContent };
}
public updateEnv(env: BuildStepEnv): void {
this._env = env;
}
}
interface BuildContextParams {
buildId?: string;
logger?: bunyan;
skipCleanup?: boolean;
runtimePlatform?: BuildRuntimePlatform;
projectSourceDirectory?: string;
projectTargetDirectory?: string;
relativeWorkingDirectory?: string;
staticContextContent?: Record<string, any>;
}
export function createStepContextMock({
buildId,
logger,
skipCleanup,
runtimePlatform,
projectSourceDirectory,
projectTargetDirectory,
relativeWorkingDirectory,
staticContextContent,
}: BuildContextParams = {}): BuildStepContext {
const globalCtx = createGlobalContextMock({
buildId,
logger,
skipCleanup,
runtimePlatform,
projectSourceDirectory,
projectTargetDirectory,
relativeWorkingDirectory,
staticContextContent,
});
return new BuildStepContext(globalCtx, {
logger: logger ?? createMockLogger(),
relativeWorkingDirectory,
});
}
export function createGlobalContextMock({
logger,
skipCleanup,
runtimePlatform,
projectSourceDirectory,
projectTargetDirectory,
relativeWorkingDirectory,
staticContextContent,
}: BuildContextParams = {}): BuildStepGlobalContext {
const resolvedProjectTargetDirectory =
projectTargetDirectory ?? path.join(os.tmpdir(), 'eas-build', uuidv4());
return new BuildStepGlobalContext(
new MockContextProvider(
logger ?? createMockLogger(),
runtimePlatform ?? BuildRuntimePlatform.LINUX,
projectSourceDirectory ?? '/non/existent/dir',
resolvedProjectTargetDirectory,
relativeWorkingDirectory
? path.resolve(resolvedProjectTargetDirectory, relativeWorkingDirectory)
: resolvedProjectTargetDirectory,
'/non/existent/dir',
staticContextContent ?? {}
),
skipCleanup ?? false
);
}
@@ -0,0 +1,94 @@
import {
Android,
ArchiveSourceType,
BuildMode,
BuildTrigger,
Ios,
Platform,
Workflow,
} from '@expo/eas-build-job';
import { randomUUID } from 'crypto';
const androidCredentials: Android.BuildSecrets['buildCredentials'] = {
keystore: {
dataBase64: 'MjEzNwo=',
keystorePassword: 'pass1',
keyAlias: 'alias',
keyPassword: 'pass2',
},
};
const iosCredentials: Ios.BuildCredentials = {
testapp: {
provisioningProfileBase64: '',
distributionCertificate: {
dataBase64: '',
password: '',
},
},
};
export function createTestAndroidJob({
buildCredentials = androidCredentials,
}: {
buildCredentials?: Android.BuildSecrets['buildCredentials'];
} = {}): Android.Job {
return {
mode: BuildMode.BUILD,
platform: Platform.ANDROID,
triggeredBy: BuildTrigger.EAS_CLI,
type: Workflow.GENERIC,
projectArchive: {
type: ArchiveSourceType.URL,
url: 'https://turtle-v2-test-fixtures.s3.us-east-2.amazonaws.com/project.tar.gz',
},
projectRootDirectory: '.',
applicationArchivePath: './android/app/build/outputs/apk/release/*.apk',
cache: {
clear: false,
disabled: false,
paths: [],
},
secrets: {
buildCredentials,
},
appId: randomUUID(),
initiatingUserId: randomUUID(),
};
}
export function createTestIosJob({
buildCredentials = iosCredentials,
triggeredBy = BuildTrigger.EAS_CLI,
workflowInterpolationContext,
}: {
buildCredentials?: Ios.BuildCredentials;
triggeredBy?: Ios.Job['triggeredBy'];
workflowInterpolationContext?: Ios.Job['workflowInterpolationContext'];
} = {}): Ios.Job {
return {
mode: BuildMode.BUILD,
platform: Platform.IOS,
triggeredBy,
type: Workflow.GENERIC,
projectArchive: {
type: ArchiveSourceType.URL,
url: 'https://turtle-v2-test-fixtures.s3.us-east-2.amazonaws.com/project.tar.gz',
},
scheme: 'turtlebareproj',
buildConfiguration: 'Release',
applicationArchivePath: './ios/build/*.ipa',
projectRootDirectory: '.',
cache: {
clear: false,
disabled: false,
paths: [],
},
secrets: {
buildCredentials,
},
appId: randomUUID(),
initiatingUserId: randomUUID(),
workflowInterpolationContext,
};
}
@@ -0,0 +1,12 @@
import { bunyan } from '@expo/logger';
export function createMockLogger({ logToConsole = false } = {}): bunyan {
const logger = {
info: jest.fn(logToConsole ? console.info : () => {}),
debug: jest.fn(logToConsole ? console.debug : () => {}),
error: jest.fn(logToConsole ? console.error : () => {}),
warn: jest.fn(logToConsole ? console.warn : () => {}),
child: jest.fn().mockImplementation(() => createMockLogger({ logToConsole })),
} as unknown as bunyan;
return logger;
}
@@ -0,0 +1,147 @@
import { AndroidConfig } from '@expo/config-plugins';
import { vol } from 'memfs';
import path from 'path';
import {
AndroidMetadataName,
androidGetNativelyDefinedChannelAsync,
androidGetNativelyDefinedRuntimeVersionAsync,
androidSetChannelNativelyAsync,
androidSetRuntimeVersionNativelyAsync,
} from '../expoUpdates';
jest.mock('fs');
const originalFs = jest.requireActual('fs');
const channel = 'easupdatechannel';
const manifestPath = '/app/android/app/src/main/AndroidManifest.xml';
afterEach(() => {
vol.reset();
});
describe(androidSetChannelNativelyAsync, () => {
it('sets the channel', async () => {
vol.fromJSON(
{
'android/app/src/main/AndroidManifest.xml': originalFs.readFileSync(
path.join(__dirname, 'fixtures/NoMetadataAndroidManifest.xml'),
'utf-8'
),
},
'/app'
);
const ctx = {
getReactNativeProjectDirectory: () => '/app',
job: { updates: { channel } },
logger: { info: () => {} },
};
await androidSetChannelNativelyAsync(ctx as any);
const newAndroidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
const newValue = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
newAndroidManifest,
AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
);
expect(newValue).toBeDefined();
expect(JSON.parse(newValue!)).toEqual({ 'expo-channel-name': channel });
});
});
describe(androidGetNativelyDefinedChannelAsync, () => {
it('gets the channel', async () => {
vol.fromJSON(
{
'android/app/src/main/AndroidManifest.xml': originalFs.readFileSync(
path.join(__dirname, 'fixtures/AndroidManifestWithChannel.xml'),
'utf-8'
),
},
'/app'
);
const ctx = {
getReactNativeProjectDirectory: () => '/app',
logger: { info: () => {} },
};
await expect(androidGetNativelyDefinedChannelAsync(ctx as any)).resolves.toBe('staging-123');
});
});
describe(androidGetNativelyDefinedRuntimeVersionAsync, () => {
it('gets the native runtime version', async () => {
vol.fromJSON(
{
'android/app/src/main/AndroidManifest.xml': originalFs.readFileSync(
path.join(__dirname, 'fixtures/AndroidManifestWithRuntimeVersion.xml'),
'utf-8'
),
},
'/app'
);
const ctx = {
getReactNativeProjectDirectory: () => '/app',
job: {},
logger: { info: () => {} },
};
const nativelyDefinedRuntimeVersion = await androidGetNativelyDefinedRuntimeVersionAsync(
ctx as any
);
expect(nativelyDefinedRuntimeVersion).toBe('exampleruntimeversion');
});
});
describe(androidSetRuntimeVersionNativelyAsync, () => {
it('sets the runtime version when nothing is set natively', async () => {
vol.fromJSON(
{
'android/app/src/main/AndroidManifest.xml': originalFs.readFileSync(
path.join(__dirname, 'fixtures/NoMetadataAndroidManifest.xml'),
'utf-8'
),
},
'/app'
);
const ctx = {
getReactNativeProjectDirectory: () => '/app',
job: {},
logger: { info: () => {} },
};
await androidSetRuntimeVersionNativelyAsync(ctx as any, '1.2.3');
const newAndroidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
const newValue = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
newAndroidManifest,
AndroidMetadataName.RUNTIME_VERSION
);
expect(newValue).toBe('1.2.3');
});
it('updates the runtime version when value is already set natively', async () => {
vol.fromJSON(
{
'android/app/src/main/AndroidManifest.xml': originalFs.readFileSync(
path.join(__dirname, 'fixtures/AndroidManifestWithRuntimeVersion.xml'),
'utf-8'
),
},
'/app'
);
const ctx = {
getReactNativeProjectDirectory: () => '/app',
job: {},
logger: { info: () => {} },
};
await androidSetRuntimeVersionNativelyAsync(ctx as any, '1.2.3');
const newAndroidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
const newValue = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
newAndroidManifest,
AndroidMetadataName.RUNTIME_VERSION
);
expect(newValue).toBe('1.2.3');
});
});
@@ -0,0 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.expo.mycoolapp">
<uses-permission android:name="android.permission.INTERNET"/>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme">
<activity android:name=".MainActivity" android:launchMode="singleTask" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
<meta-data android:name="expo.modules.updates.EXPO_RELEASE_CHANNEL" android:value="default"/>
<meta-data android:name="expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY" android:value="{&quot;expo-channel-name&quot;:&quot;staging-123&quot;}"/>
</application>
</manifest>
@@ -0,0 +1,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.expo.mycoolapp">
<uses-permission android:name="android.permission.INTERNET"/>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme">
<activity android:name=".MainActivity" android:launchMode="singleTask" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
<meta-data android:name="expo.modules.updates.EXPO_RELEASE_CHANNEL" android:value="example_channel"/>
</application>
</manifest>
@@ -0,0 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.expo.mycoolapp">
<uses-permission android:name="android.permission.INTERNET"/>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme">
<activity android:name=".MainActivity" android:launchMode="singleTask" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
<meta-data android:name="expo.modules.updates.EXPO_RELEASE_CHANNEL" android:value="default"/>
<meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="exampleruntimeversion"/>
</application>
</manifest>
@@ -0,0 +1,27 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.expo.mycoolapp">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:launchMode="singleTask"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
</manifest>
@@ -0,0 +1,40 @@
import { Android } from '@expo/eas-build-job';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { BuildContext } from '../context';
async function restoreCredentials(ctx: BuildContext<Android.Job>): Promise<void> {
const { buildCredentials } = nullthrows(
ctx.job.secrets,
'Secrets must be defined for non-custom builds'
);
if (!buildCredentials) {
// TODO: sentry (should be detected earlier)
throw new Error('secrets are missing in the job object');
}
ctx.logger.info("Writing secrets to the project's directory");
const keystorePath = path.join(ctx.buildDirectory, `keystore-${uuidv4()}`);
await fs.writeFile(
keystorePath,
new Uint8Array(Buffer.from(buildCredentials.keystore.dataBase64, 'base64'))
);
const credentialsJson = {
android: {
keystore: {
keystorePath,
keystorePassword: buildCredentials.keystore.keystorePassword,
keyAlias: buildCredentials.keystore.keyAlias,
keyPassword: buildCredentials.keystore.keyPassword,
},
},
};
await fs.writeFile(
path.join(ctx.buildDirectory, 'credentials.json'),
JSON.stringify(credentialsJson)
);
}
export { restoreCredentials };
@@ -0,0 +1,106 @@
import { AndroidConfig } from '@expo/config-plugins';
import { BuildJob, Job } from '@expo/eas-build-job';
import assert from 'assert';
import fs from 'fs-extra';
import { BuildContext } from '../context';
export enum AndroidMetadataName {
UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY = 'expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY',
RUNTIME_VERSION = 'expo.modules.updates.EXPO_RUNTIME_VERSION',
}
export async function androidSetRuntimeVersionNativelyAsync(
ctx: BuildContext<Job>,
runtimeVersion: string
): Promise<void> {
const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(
ctx.getReactNativeProjectDirectory()
);
if (!(await fs.pathExists(manifestPath))) {
throw new Error(`Couldn't find Android manifest at ${manifestPath}`);
}
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
const mainApp = AndroidConfig.Manifest.getMainApplicationOrThrow(androidManifest);
AndroidConfig.Manifest.addMetaDataItemToMainApplication(
mainApp,
AndroidMetadataName.RUNTIME_VERSION,
runtimeVersion,
'value'
);
await AndroidConfig.Manifest.writeAndroidManifestAsync(manifestPath, androidManifest);
}
export async function androidSetChannelNativelyAsync(ctx: BuildContext<BuildJob>): Promise<void> {
assert(ctx.job.updates?.channel, 'updates.channel must be defined');
const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(
ctx.getReactNativeProjectDirectory()
);
if (!(await fs.pathExists(manifestPath))) {
throw new Error(`Couldn't find Android manifest at ${manifestPath}`);
}
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
const mainApp = AndroidConfig.Manifest.getMainApplicationOrThrow(androidManifest);
const stringifiedUpdatesRequestHeaders = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
androidManifest,
AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
);
AndroidConfig.Manifest.addMetaDataItemToMainApplication(
mainApp,
AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY,
JSON.stringify({
...JSON.parse(stringifiedUpdatesRequestHeaders ?? '{}'),
'expo-channel-name': ctx.job.updates.channel,
}),
'value'
);
await AndroidConfig.Manifest.writeAndroidManifestAsync(manifestPath, androidManifest);
}
export async function androidGetNativelyDefinedChannelAsync(
ctx: BuildContext<Job>
): Promise<string | null> {
const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(
ctx.getReactNativeProjectDirectory()
);
if (!(await fs.pathExists(manifestPath))) {
return null;
}
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
const stringifiedUpdatesRequestHeaders = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
androidManifest,
AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
);
try {
const updatesRequestHeaders = JSON.parse(stringifiedUpdatesRequestHeaders ?? '{}');
return updatesRequestHeaders['expo-channel-name'] ?? null;
} catch (err: any) {
throw new Error(
`Failed to parse ${AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY} from AndroidManifest.xml: ${err.message}`
);
}
}
export async function androidGetNativelyDefinedRuntimeVersionAsync(
ctx: BuildContext<Job>
): Promise<string | null> {
const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(
ctx.getReactNativeProjectDirectory()
);
if (!(await fs.pathExists(manifestPath))) {
return null;
}
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
return AndroidConfig.Manifest.getMainApplicationMetaDataValue(
androidManifest,
AndroidMetadataName.RUNTIME_VERSION
);
}
+127
View File
@@ -0,0 +1,127 @@
import { Android, Env, Job, Platform } from '@expo/eas-build-job';
import { bunyan } from '@expo/logger';
import spawn, { SpawnPromise, SpawnResult } from '@expo/turtle-spawn';
import assert from 'assert';
import fs from 'fs-extra';
import path from 'path';
import { BuildContext } from '../context';
import { getParentAndDescendantProcessPidsAsync } from '../utils/processes';
export async function ensureLFLineEndingsInGradlewScript<TJob extends Job>(
ctx: BuildContext<TJob>
): Promise<void> {
const gradlewPath = path.join(ctx.getReactNativeProjectDirectory(), 'android', 'gradlew');
const gradlewContent = await fs.readFile(gradlewPath, 'utf8');
if (gradlewContent.includes('\r')) {
ctx.logger.info('Replacing CRLF line endings with LF in gradlew script');
await fs.writeFile(gradlewPath, gradlewContent.replace(/\r\n/g, '\n'), 'utf8');
}
}
export async function runGradleCommand(
ctx: BuildContext<Job>,
{
logger,
gradleCommand,
androidDir,
extraEnv,
}: { logger: bunyan; gradleCommand: string; androidDir: string; extraEnv?: Env }
): Promise<void> {
logger.info(`Running 'gradlew ${gradleCommand}' in ${androidDir}`);
await fs.chmod(path.join(androidDir, 'gradlew'), 0o755);
const verboseFlag = ctx.env['EAS_VERBOSE'] === '1' ? '--info' : '';
const spawnPromise = spawn(
'bash',
['-c', `./gradlew ${gradleCommand} --profile ${verboseFlag}`],
{
cwd: androidDir,
logger,
lineTransformer: (line?: string) => {
if (!line || /^\.+$/.exec(line)) {
return null;
} else {
return line;
}
},
env: {
...ctx.env,
...extraEnv,
...resolveVersionOverridesEnvs(ctx),
LC_ALL: 'C.UTF-8',
},
}
);
if (ctx.env.EAS_BUILD_RUNNER === 'eas-build' && process.platform === 'linux') {
adjustOOMScore(spawnPromise, logger);
}
await spawnPromise;
}
/**
* OOM Killer sometimes kills worker server while build is exceeding memory limits.
* `oom_score_adj` is a value between -1000 and 1000 and it defaults to 0.
* It defines which process is more likely to get killed (higher value more likely).
*
* This function sets oom_score_adj for Gradle process and all its child processes.
*/
function adjustOOMScore(spawnPromise: SpawnPromise<SpawnResult>, logger: bunyan): void {
setTimeout(
async () => {
try {
assert(spawnPromise.child.pid);
const pids = await getParentAndDescendantProcessPidsAsync(spawnPromise.child.pid);
await Promise.all(
pids.map(async (pid: number) => {
// Value 800 is just a guess here. It's probably higher than most other
// process. I didn't want to set it any higher, because I'm not sure if OOM Killer
// can start killing processes when there is still enough memory left.
const oomScoreOverride = 800;
await fs.writeFile(`/proc/${pid}/oom_score_adj`, `${oomScoreOverride}\n`);
})
);
} catch (err: any) {
logger.debug({ err, stderr: err?.stderr }, 'Failed to override oom_score_adj');
}
},
// Wait 20 seconds to make sure all child processes are started
20000
);
}
// Version envs should be set at the beginning of the build, but when building
// from github those values are resolved later.
function resolveVersionOverridesEnvs(ctx: BuildContext<Job>): Env {
const extraEnvs: Env = {};
if (
ctx.job.platform === Platform.ANDROID &&
ctx.job.version?.versionCode &&
!ctx.env.EAS_BUILD_ANDROID_VERSION_CODE
) {
extraEnvs.EAS_BUILD_ANDROID_VERSION_CODE = ctx.job.version.versionCode;
}
if (
ctx.job.platform === Platform.ANDROID &&
ctx.job.version?.versionName &&
!ctx.env.EAS_BUILD_ANDROID_VERSION_NAME
) {
extraEnvs.EAS_BUILD_ANDROID_VERSION_NAME = ctx.job.version.versionName;
}
return extraEnvs;
}
export function resolveGradleCommand(job: Android.Job): string {
if (job.gradleCommand) {
return job.gradleCommand;
} else if (job.developmentClient) {
return ':app:assembleDebug';
} else if (!job.buildType) {
return ':app:bundleRelease';
} else if (job.buildType === Android.BuildType.APK) {
return ':app:assembleRelease';
} else {
return ':app:bundleRelease';
}
}
@@ -0,0 +1,231 @@
import { XMLParser } from 'fast-xml-parser';
import fs from 'fs-extra';
import path from 'path';
export interface GradleProfileTask {
path: string;
durationMs: number;
result: string;
}
export async function parseGradleProfile(androidDir: string): Promise<GradleProfileTask[]> {
const profileDir = path.join(androidDir, 'build', 'reports', 'profile');
// Gradle profile may not exist for all builds (e.g. custom, repack
// jobs). We should short circuit and not unnecessarily log.
if (!(await fs.pathExists(profileDir))) {
return [];
}
const files = await fs.readdir(profileDir);
const htmlFile = files
.filter(f => f.startsWith('profile-') && f.endsWith('.html'))
.sort()
.pop();
if (!htmlFile) {
throw new Error(`No Gradle profile HTML found in ${profileDir}`);
}
const html = await fs.readFile(path.join(profileDir, htmlFile), 'utf8');
// Locate the <h2>Task Execution</h2> heading and extract its <table>
const headingMatch = html.match(/<h2[^>]*>\s*Task Execution\s*<\/h2>/i);
if (!headingMatch || headingMatch.index === undefined) {
throw new Error('Could not find Task Execution section in Gradle profile');
}
const sectionStart = headingMatch.index;
const tableStart = html.indexOf('<table', sectionStart);
const tableEnd = html.indexOf('</table>', tableStart);
if (tableStart === -1 || tableEnd === -1) {
throw new Error('Could not find task execution table in Gradle profile');
}
const tableHtml = html.slice(tableStart, tableEnd + '</table>'.length);
const parser = new XMLParser({
ignoreAttributes: true,
isArray: name => name === 'tr' || name === 'td',
trimValues: true,
});
const parsed = parser.parse(tableHtml);
const rows: any[] = parsed?.table?.tbody?.tr ?? parsed?.table?.tr ?? [];
const tasks: GradleProfileTask[] = [];
for (const row of rows) {
const cells: unknown[] = row?.td;
if (!cells || cells.length < 2) {
continue;
}
const taskPath = String(cells[0] ?? '').trim();
const durationStr = String(cells[1] ?? '').trim();
const result = String(cells[2] ?? '').trim() || 'executed';
if (!taskPath || !durationStr) {
continue;
}
const durationMs = parseDurationToMs(durationStr);
tasks.push({ path: taskPath, durationMs, result: result.toLowerCase() });
}
return tasks;
}
function parseDurationToMs(duration: string): number {
// Gradle profile durations can be like "1.234s", "0.045s", "12.5s"
const secondsMatch = duration.match(/^([\d.]+)s$/);
if (secondsMatch) {
return Math.round(parseFloat(secondsMatch[1]) * 1000);
}
return 0;
}
function formatSeconds(ms: number): string {
const s = ms / 1000;
if (s < 0.1) {
return `${ms}ms`;
}
return `${s.toFixed(1)}s`;
}
export function formatGradleProfileReport(tasks: GradleProfileTask[]): string {
// Filter out tasks under 1 second
const significantTasks = tasks.filter(t => t.durationMs >= 1000);
// Separate module totals from individual tasks
const moduleTotals = significantTasks.filter(t => t.result === '(total)');
const individualTasks = significantTasks.filter(t => t.result !== '(total)');
// Group individual tasks by their module prefix
const moduleChildren = new Map<string, GradleProfileTask[]>();
const orphanTasks: GradleProfileTask[] = [];
for (const task of individualTasks) {
const parent = moduleTotals.find(m => task.path.startsWith(m.path + ':'));
if (parent) {
const children = moduleChildren.get(parent.path) ?? [];
children.push(task);
moduleChildren.set(parent.path, children);
} else {
orphanTasks.push(task);
}
}
// Sort module totals by duration, sort children within each module
const sortedModules = [...moduleTotals].sort((a, b) => b.durationMs - a.durationMs);
for (const children of moduleChildren.values()) {
children.sort((a, b) => b.durationMs - a.durationMs);
}
orphanTasks.sort((a, b) => b.durationMs - a.durationMs);
// Build display rows: [displayName, task]
const rows: { displayName: string; task: GradleProfileTask }[] = [];
for (const mod of sortedModules) {
rows.push({ displayName: mod.path, task: mod });
const children = moduleChildren.get(mod.path) ?? [];
for (let i = 0; i < children.length; i++) {
const isLast = i === children.length - 1;
const prefix = isLast ? ' └─ ' : ' ├─ ';
const shortName = children[i].path.slice(mod.path.length + 1);
rows.push({ displayName: prefix + shortName, task: children[i] });
}
}
for (const task of orphanTasks) {
rows.push({ displayName: task.path, task });
}
// Compute totals from individual tasks only (avoid double-counting)
const totalMs = individualTasks.reduce((sum, t) => sum + t.durationMs, 0);
const maxMs = totalMs || 1;
const nameWidth = Math.max(4, ...rows.map(r => r.displayName.length)) + 2;
const barMaxWidth = 20;
const header =
'┌─' +
'─'.repeat(nameWidth) +
'─┬────────────┬──────────┬────────────┬─' +
'─'.repeat(barMaxWidth) +
'─┐';
const divider =
'├─' +
'─'.repeat(nameWidth) +
'─┼────────────┼──────────┼────────────┼─' +
'─'.repeat(barMaxWidth) +
'─┤';
const footer =
'└─' +
'─'.repeat(nameWidth) +
'─┴────────────┴──────────┴────────────┴─' +
'─'.repeat(barMaxWidth) +
'─┘';
const taskCount = individualTasks.length;
const cachedCount = individualTasks.filter(t => t.result !== 'executed').length;
const lines: string[] = [];
lines.push('Gradle Build — Task Execution Profile');
const cachedSuffix = cachedCount > 0 ? ` (${cachedCount} cached/up-to-date)` : '';
lines.push(`${taskCount} tasks${cachedSuffix}, total task time: ${formatSeconds(totalMs)}`);
lines.push('% Time = share of total task execution time');
lines.push('');
lines.push(header);
lines.push(
'│ ' +
'Task'.padEnd(nameWidth) +
' │ ' +
'Duration'.padStart(10) +
' │ ' +
'% Time'.padStart(8) +
' │ ' +
'Result'.padEnd(10) +
' │ ' +
' '.repeat(barMaxWidth) +
' │'
);
lines.push(divider);
for (const row of rows) {
const pct = totalMs === 0 ? 0 : (row.task.durationMs / totalMs) * 100;
const barLength = Math.round((row.task.durationMs / maxMs) * barMaxWidth);
const bar = '█'.repeat(barLength) + '░'.repeat(barMaxWidth - barLength);
const result = row.task.result === '(total)' ? 'total' : row.task.result;
lines.push(
'│ ' +
row.displayName.padEnd(nameWidth) +
' │ ' +
formatSeconds(row.task.durationMs).padStart(10) +
' │ ' +
`${pct.toFixed(1)}%`.padStart(8) +
' │ ' +
result.padEnd(10) +
' │ ' +
bar +
' │'
);
}
lines.push(divider);
lines.push(
'│ ' +
'TOTAL'.padEnd(nameWidth) +
' │ ' +
formatSeconds(totalMs).padStart(10) +
' │ ' +
'100.0%'.padStart(8) +
' │ ' +
' '.repeat(10) +
' │ ' +
' '.repeat(barMaxWidth) +
' │'
);
lines.push(footer);
lines.push('');
return lines.join('\n');
}
@@ -0,0 +1,312 @@
import { BuildMode, BuildPhase, Job, Platform, errors } from '@expo/eas-build-job';
import { vol } from 'memfs';
import path from 'path';
import { resolveBuildPhaseErrorAsync } from '../detectError';
jest.mock('fs');
const originalFs = jest.requireActual('fs');
afterEach(() => {
vol.reset();
});
describe(resolveBuildPhaseErrorAsync, () => {
it('detects log for corrupted npm package', async () => {
const fakeError = new Error();
const err = await resolveBuildPhaseErrorAsync(
fakeError,
[
'[stderr] WARN tarball tarball data for @typescript-eslint/[email protected] (sha512-cozo/GbwixVR0sgfHItz3t1yXu521yn71Wj6PlYCFA3WPhy51CUPkifFKfBis91bDclGmAY45hhaAXVjdn4new==) seems to be corrupted. Trying again.',
],
{
job: { platform: Platform.ANDROID } as Job,
phase: BuildPhase.INSTALL_DEPENDENCIES,
env: {},
},
'/fake/path'
);
expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
expect(err.message).toBe(
'Unknown error. See logs of the Install dependencies build phase for more information.'
);
expect(err.trackingCode).toBe('NPM_CORRUPTED_PACKAGE');
expect(err.metadata).toEqual({
packageName: '@typescript-eslint/[email protected]',
});
});
it('detects log for invalid bundler and reports it to user', async () => {
const fakeError = new Error();
const err = await resolveBuildPhaseErrorAsync(
fakeError,
[
"/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/rubygems/dependency.rb:313:in `to_specs': Could not find 'bundler' (2.2.3) required by your /Users/expo/project/build/ios/Gemfile.lock. (Gem::MissingSpecVersionError)",
],
{
job: { platform: Platform.IOS } as Job,
phase: BuildPhase.RUN_FASTLANE,
env: {},
},
'/fake/path'
);
expect(err.errorCode).toBe('EAS_BUILD_UNSUPPORTED_BUNDLER_VERSION_ERROR');
expect(err.message).toBe(
'Your project requires a different version of the Ruby "bundler" program than the version installed in this EAS Build environment. You can specify which version of "bundler" to install by specifying the version under "build"→[buildProfileName]→"ios"→"bundler" in eas.json.'
);
expect(err.trackingCode).toBeUndefined();
expect(err.metadata).toBeUndefined();
});
it('does not detect errors if they show up in different build phase', async () => {
const fakeError = new Error();
const err = await resolveBuildPhaseErrorAsync(
fakeError,
[
"/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/rubygems/dependency.rb:313:in `to_specs': Could not find 'bundler' (2.2.3) required by your /Users/expo/project/build/ios/Gemfile.lock. (Gem::MissingSpecVersionError)",
],
{
job: { platform: Platform.IOS } as Job,
phase: BuildPhase.INSTALL_DEPENDENCIES, // it should be in RUN_FASTLANE
env: {},
},
'/fake/path'
);
expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
expect(err.message).toBe(
'Unknown error. See logs of the Install dependencies build phase for more information.'
);
expect(err.trackingCode).toBeUndefined();
});
it('detects npm cache error if cache is enabled', async () => {
const mockEnv = {
EAS_BUILD_NPM_CACHE_URL: 'https://dominik.sokal.pl/npm/cache',
};
const fakeError = new Error();
const err = await resolveBuildPhaseErrorAsync(
fakeError,
[`Blah blah Error ... ${mockEnv.EAS_BUILD_NPM_CACHE_URL}`],
{
job: { platform: Platform.ANDROID } as Job,
phase: BuildPhase.INSTALL_DEPENDENCIES,
env: mockEnv,
},
'/fake/path'
);
expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
expect(err.message).toBe(
'Unknown error. See logs of the Install dependencies build phase for more information.'
);
expect(err.trackingCode).toBe('NPM_CACHE_ERROR');
});
it('does not detect npm cache error if cache is disabled', async () => {
const mockEnv = {
EAS_BUILD_NPM_CACHE_URL: 'https://dominik.sokal.pl/npm/cache',
};
const fakeError = new Error();
const err = await resolveBuildPhaseErrorAsync(
fakeError,
[`Blah blah Error ... ${mockEnv.EAS_BUILD_NPM_CACHE_URL}`],
{
job: { platform: Platform.ANDROID } as Job,
phase: BuildPhase.INSTALL_DEPENDENCIES,
env: {},
},
'/fake/path'
);
expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
expect(err.message).toBe(
'Unknown error. See logs of the Install dependencies build phase for more information.'
);
expect(err.trackingCode).toBeUndefined();
});
it('detects xcode line error', async () => {
vol.fromJSON({
'/path/to/xcodelogs.log': originalFs.readFileSync(
path.resolve('./src/buildErrors/__tests__/fixtures/xcode.log'),
'utf-8'
),
});
const fakeError = new Error();
const err = await resolveBuildPhaseErrorAsync(
fakeError,
[''],
{
job: { platform: Platform.IOS } as Job,
phase: BuildPhase.RUN_FASTLANE,
env: {},
},
'/path/to/'
);
expect(err.errorCode).toBe('XCODE_RESOURCE_BUNDLE_CODE_SIGNING_ERROR');
expect(err.trackingCode).toBeUndefined();
});
it('detects minimum deployment target error correctly', async () => {
const fakeError = new Error();
const err = await resolveBuildPhaseErrorAsync(
fakeError,
[
'CocoaPods could not find compatible versions for pod "react-native-google-maps":16 In Podfile:17 react-native-google-maps (from `/Users/expo/workingdir/build/node_modules/react-native-maps`)18Specs satisfying the `react-native-google-maps (from `/Users/expo/workingdir/build/node_modules/react-native-maps`)` dependency were found, but they required a higher minimum deployment target.19Error: Compatible versions of some pods could not be resolved.',
],
{
job: { platform: Platform.IOS } as Job,
phase: BuildPhase.INSTALL_PODS,
env: {},
},
'/fake/path'
);
expect(err.errorCode).toBe('EAS_BUILD_HIGHER_MINIMUM_DEPLOYMENT_TARGET_ERROR');
expect(err.trackingCode).toBeUndefined();
});
it('detects provisioning profile mismatch error correctly', async () => {
const fakeError = new Error();
const err = await resolveBuildPhaseErrorAsync(
fakeError,
[
`No provisioning profile for application: '_floatsignTemp/Payload/EcoBatteryPREVIEW.app' with bundle identifier 'com.ecobattery.ecobattery-preview'`,
],
{
job: { platform: Platform.IOS, mode: BuildMode.RESIGN } as Job,
phase: BuildPhase.RUN_FASTLANE,
env: {},
},
'/fake/path'
);
expect(err.errorCode).toBe('EAS_BUILD_RESIGN_PROVISIONING_PROFILE_MISMATCH_ERROR');
expect(err.trackingCode).toBeUndefined();
});
it('detects generic "Run Fastlane" error for resign correctly', async () => {
const fakeError = new Error();
const err = await resolveBuildPhaseErrorAsync(
fakeError,
[`other error`],
{
job: { platform: Platform.IOS, mode: BuildMode.RESIGN } as Job,
phase: BuildPhase.RUN_FASTLANE,
env: {},
},
'/fake/path'
);
expect(err.errorCode).toBe('EAS_BUILD_UNKNOWN_FASTLANE_RESIGN_ERROR');
expect(err.trackingCode).toBeUndefined();
});
it('detects build error in "Run Fastlane" phase correctly', async () => {
const fakeError = new Error();
const err = await resolveBuildPhaseErrorAsync(
fakeError,
[`some build error`],
{
job: { platform: Platform.IOS, mode: BuildMode.BUILD } as Job,
phase: BuildPhase.RUN_FASTLANE,
env: {},
},
'/fake/path'
);
expect(err.errorCode).toBe('EAS_BUILD_UNKNOWN_FASTLANE_ERROR');
expect(err.trackingCode).toBeUndefined();
});
it('detects provisioning profile mismatch error correctly', async () => {
const xcodeLogs = `Intermediates.noindex/Pods.build/Release-iphonesimulator/Yoga.build/Objects-normal/arm64/82b82416624d2658e5098eb0a28c15c5-common-args.resp
-target arm64-apple-ios15.0-simulator '-std=gnu++14' '-stdlib=libc++' -fmodules '-fmodules-cache-path=/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/ModuleCache.noindex' '-fmodule-name=yoga' -fpascal-strings -Os -fno-common '-DPOD_CONFIGURATION_RELEASE=1' '-DCOCOAPODS=1' -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator18.1.sdk -g -iquote /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Intermediates.noindex/Pods.build/Release-iphonesimulator/Yoga.build/Yoga-generated-files.hmap -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Intermediates.noindex/Pods.build/Release-iphonesimulator/Yoga.build/Yoga-own-target-headers.hmap -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Intermediates.noindex/Pods.build/Release-iphonesimulator/Yoga.build/Yoga-all-non-framework-target-headers.hmap -ivfsoverlay /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Intermediates.noindex/Pods.build/Release-iphonesimulator/Pods-8699adb1dd336b26511df848a716bd42-VFS-iphonesimulator/all-product-headers.yaml -iquote /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Intermediates.noindex/Pods.build/Release-iphonesimulator/Yoga.build/Yoga-project-headers.hmap -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/Yoga/include -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/Pods/Headers/Private -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/Pods/Headers/Private/Yoga -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/Pods/Headers/Public -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/Pods/Headers/Public/Yoga -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Intermediates.noindex/Pods.build/Release-iphonesimulator/Yoga.build/DerivedSources-normal/arm64 -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Intermediates.noindex/Pods.build/Release-iphonesimulator/Yoga.build/DerivedSources/arm64 -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Intermediates.noindex/Pods.build/Release-iphonesimulator/Yoga.build/DerivedSources -F/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/Yoga
MkDir /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/expo-dev-menu/EXDevMenu.bundle (in target 'expo-dev-menu-EXDevMenu' from project 'Pods')
cd /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/Pods
/bin/mkdir -p /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/expo-dev-menu/EXDevMenu.bundle
MkDir /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/expo-dev-launcher/EXDevLauncher.bundle (in target 'expo-dev-launcher-EXDevLauncher' from project 'Pods')
cd /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/Pods
/bin/mkdir -p /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/expo-dev-launcher/EXDevLauncher.bundle
ProcessXCFramework /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/ProgrammaticAccessLibrary.framework ios simulator
cd /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios
builtin-process-xcframework --xcframework /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework --platform ios --environment simulator --target-path /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator
/Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework:1:1: error: The signature of “ProgrammaticAccessLibrary.xcframework” cannot be verified.
note: A sealed resource is missing or invalid
note: /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework: a sealed resource is missing or invalid
file modified: /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework/ios-arm64_x86_64-simulator/ProgrammaticAccessLibrary.framework/ProgrammaticAccessLibrary
file modified: /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework/ios-arm64/ProgrammaticAccessLibrary.framework/ProgrammaticAccessLibrary
error: Some other error
log
log
log
error: The last one
log
note`;
vol.fromJSON({
'/path/to/xcode.log': xcodeLogs,
});
const fakeError = new Error();
const err = await resolveBuildPhaseErrorAsync(
fakeError,
[`some logs`],
{
job: { platform: Platform.IOS, mode: BuildMode.BUILD } as Job,
phase: BuildPhase.RUN_FASTLANE,
env: {},
},
'/path/to'
);
expect(err.errorCode).toBe('XCODE_BUILD_ERROR');
expect(err.trackingCode).toBeUndefined();
expect(err.message)
.toBe(`The "Run fastlane" step failed because of an error in the Xcode build process. We automatically detected following errors in your Xcode build logs:
- The signature of “ProgrammaticAccessLibrary.xcframework” cannot be verified.
- Some other error
- The last one
Refer to "Xcode Logs" below for additional, more detailed logs.`);
});
it('detects MAVEN_CACHE_ERROR correctly', async () => {
const err = await resolveBuildPhaseErrorAsync(
new Error(),
[`https://szymon.pl/maven/cache`],
{
job: { platform: Platform.ANDROID, mode: BuildMode.BUILD } as Job,
phase: BuildPhase.RUN_GRADLEW,
env: {
EAS_BUILD_MAVEN_CACHE_URL: 'https://szymon.pl/maven/cache',
},
},
'/fake/path'
);
expect(err.errorCode).toBe('EAS_BUILD_UNKNOWN_GRADLE_ERROR');
expect(err.trackingCode).toBe('MAVEN_CACHE_ERROR');
expect(err.message).toBe(
`Gradle build failed with unknown error. See logs for the "Run gradlew" phase for more information.`
);
});
it('does not throw MAVEN_CACHE_ERROR if "Could not find BlurView-version-2.0.3.jar" log is present', async () => {
const err = await resolveBuildPhaseErrorAsync(
new Error(),
[`Could not find BlurView-version-2.0.3.jar sth sthelse https://szymon.pl/maven/cache`],
{
job: { platform: Platform.ANDROID, mode: BuildMode.BUILD } as Job,
phase: BuildPhase.RUN_GRADLEW,
env: {
EAS_BUILD_MAVEN_CACHE_URL: 'https://szymon.pl/maven/cache',
},
},
'/fake/path'
);
expect(err.errorCode).toBe('EAS_BUILD_UNKNOWN_GRADLE_ERROR');
expect(err.trackingCode).toBeUndefined();
expect(err.message).toBe(
`Gradle build failed with unknown error. See logs for the "Run gradlew" phase for more information.`
);
});
});
@@ -0,0 +1 @@
/Users/expo/workingdir/build/managed/ios/Pods/Pods.xcodeproj: error: Signing for "EXConstants-EXConstants" requires a development team. Select a development team in the Signing & Capabilities editor. (in target 'EXConstants-EXConstants' from project 'Pods')
@@ -0,0 +1,364 @@
import { BuildPhase, Platform } from '@expo/eas-build-job';
import escapeRegExp from 'lodash/escapeRegExp';
import { ErrorContext, ErrorHandler } from './errors.types';
export class TrackedBuildError extends Error {
constructor(
public errorCode: string,
public message: string,
public metadata?: Record<string, unknown>
) {
super(message);
}
}
export const buildErrorHandlers: ErrorHandler<TrackedBuildError>[] = [
{
platform: Platform.IOS,
phase: BuildPhase.INSTALL_PODS,
// example log:
// CDN: trunk URL couldn't be downloaded: https://cdn.jsdelivr.net/cocoa/Specs/2/a/e/MultiplatformBleAdapter/0.0.3/MultiplatformBleAdapter.podspec.json Response: 429 429: Too Many Requests
regexp: /CDN: trunk URL couldn't be downloaded.* Response: 429 429: Too Many Requests/,
createError: () =>
new TrackedBuildError('COCOAPODS_TO_MANY_REQUEST', 'cocoapods: too many requests'),
},
{
phase: BuildPhase.INSTALL_DEPENDENCIES,
// Host key verification failed.
// fatal: Could not read from remote repository.
regexp: /Host key verification failed\.\nfatal: Could not read from remote repository/,
createError: () => new TrackedBuildError('NPM_INSTALL_SSH_AUTHENTICATION', 'Missing ssh key.'),
},
{
phase: BuildPhase.INSTALL_DEPENDENCIES,
// error [email protected]: The engine "node" is incompatible with this module. Expected version "14". Got "16.13.2"
// error Found incompatible module.
regexp: /The engine "node" is incompatible with this module\. Expected version/,
createError: () =>
new TrackedBuildError('NODE_ENGINE_INCOMPATIBLE', 'node: Incompatible engine field.'),
},
{
phase: BuildPhase.INSTALL_DEPENDENCIES,
// error An unexpected error occurred: "https://registry.yarnpkg.com/@react-native/normalize-color/-/normalize-color-2.0.0.tgz: Request failed \"500 Internal Server Error\"".
// or
// error An unexpected error occurred: "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz: Request failed \"503 Service Unavailable\"".
regexp: /An unexpected error occurred: "https:\/\/registry.yarnpkg.com\/.*Request failed \\"5/,
createError: () => new TrackedBuildError('YARN_REGISTRY_5XX_RESPONSE', 'yarn: 5xx response.'),
},
{
phase: BuildPhase.PREBUILD,
// Input is required, but Expo CLI is in non-interactive mode
// or
// CommandError: Input is required, but 'npx expo' is in non-interactive mode.
regexp: /Input is required, but .* is in non-interactive mode/,
createError: () =>
new TrackedBuildError(
'EXPO_CLI_INPUT_REQUIRED_ERROR',
`expo-cli: Input required in non-interactive mode.`
),
},
{
platform: Platform.IOS,
phase: BuildPhase.PREBUILD,
// [03:03:05] [ios.infoPlist]: withIosInfoPlistBaseMod: GoogleService-Info.plist is empty
regexp: /withIosInfoPlistBaseMod: GoogleService-Info\.plist is empty/,
createError: () =>
new TrackedBuildError(
'EXPO_CLI_EMPTY_GOOGLE_SERVICES_PLIST_ERROR',
`expo-cli: Empty GoogleService-Info.plist.`
),
},
{
platform: Platform.IOS,
phase: BuildPhase.PREBUILD,
// [01:52:04] [ios.xcodeproj]: withIosXcodeprojBaseMod: Path to GoogleService-Info.plist is not defined. Please specify the `expo.ios.googleServicesFile` field in app.json.
regexp: /withIosXcodeprojBaseMod: Path to GoogleService-Info\.plist is not defined/,
createError: () =>
new TrackedBuildError(
'EXPO_CLI_NOT_DEFINED_GOOGLE_SERVICES_PLIST_ERROR',
`expo-cli: Path to GoogleService-Info.plist is not defined.`
),
},
{
phase: BuildPhase.PREBUILD,
// Error: [android.dangerous]: withAndroidDangerousBaseMod: ENOENT: no such file or directory, open './assets/adaptive-icon.png'
// at Object.openSync (node:fs:585:3)
// at readFileSync (node:fs:453:35)
// at calculateHash (/home/expo/workingdir/build/node_modules/@expo/image-utils/build/Cache.js:14:91)
// at createCacheKey (/home/expo/workingdir/build/node_modules/@expo/image-utils/build/Cache.js:19:18)
// at Object.createCacheKeyWithDirectoryAsync (/home/expo/workingdir/build/node_modules/@expo/image-utils/build/Cache.js:24:33)
// at generateImageAsync (/home/expo/workingdir/build/node_modules/@expo/image-utils/build/Image.js:151:34)
// at async generateIconAsync (/home/expo/workingdir/build/node_modules/@expo/prebuild-config/build/plugins/icons/withAndroidIcons.js:369:11)
// at async /home/expo/workingdir/build/node_modules/@expo/prebuild-config/build/plugins/icons/withAndroidIcons.js:310:21
// at async Promise.all (index 0)
// at async generateMultiLayerImageAsync (/home/expo/workingdir/build/node_modules/@expo/prebuild-config/build/plugins/icons/withAndroidIcons.js:306:3)
// or
// Error: [ios.dangerous]: withIosDangerousBaseMod: ENOENT: no such file or directory, open './assets/images/app_icon_staging.png'
// at Object.openSync (fs.js:497:3)
// at readFileSync (fs.js:393:35)
// at calculateHash (/Users/expo/workingdir/build/node_modules/@expo/image-utils/build/Cache.js:14:91)
// at createCacheKey (/Users/expo/workingdir/build/node_modules/@expo/image-utils/build/Cache.js:19:18)
// at Object.createCacheKeyWithDirectoryAsync (/Users/expo/workingdir/build/node_modules/@expo/image-utils/build/Cache.js:24:33)
// at generateImageAsync (/Users/expo/workingdir/build/node_modules/@expo/image-utils/build/Image.js:151:34)
// at async setIconsAsync (/Users/expo/workingdir/build/node_modules/@expo/prebuild-config/build/plugins/icons/withIosIcons.js:169:15)
// at async /Users/expo/workingdir/build/node_modules/@expo/prebuild-config/build/plugins/icons/withIosIcons.js:71:5
// at async action (/Users/expo/workingdir/build/node_modules/@expo/config-plugins/build/plugins/withMod.js:235:23)
// at async interceptingMod (/Users/expo/workingdir/build/node_modules/@expo/config-plugins/build/plugins/withMod.js:126:21)
regexp:
/ENOENT: no such file or directory[\s\S]*prebuild-config\/build\/plugins\/icons\/with(Android|Ios)Icons\.js/,
createError: () => new TrackedBuildError('EXPO_CLI_MISSING_ICON', 'expo-cli: Missing icon.'),
},
{
phase: BuildPhase.PREBUILD,
// Cannot determine which native SDK version your project uses because the module `expo` is not installed. Please install it with `yarn add expo` and try again.
regexp:
/Cannot determine which native SDK version your project uses because the module `expo` is not installed/,
createError: () =>
new TrackedBuildError('EXPO_CLI_EXPO_PACKAGE_MISSING', 'expo-cli: "expo" package missing.'),
},
{
phase: BuildPhase.INSTALL_PODS,
// The Swift pod `FirebaseCoreInternal` depends upon `GoogleUtilities`, which does not define modules. To opt into those targets generating module maps (which is necessary to import them from Swift when building as static
regexp: /The Swift pod .* depends upon .* which does not define modules/,
createError: () =>
new TrackedBuildError(
'SWIFT_POD_INCOMPATIBLE_DEPENDENCY',
'pod: Swift pod depends on a pod that does not define modules.'
),
},
{
platform: Platform.IOS,
phase: BuildPhase.INSTALL_PODS,
// Adding spec repo `24-repository-cocoapods-proxy` with CDN `http://10.254.24.7:8081/repository/cocoapods-proxy/`
// [!] No podspec exists at path `/Users/expo/.cocoapods/repos/24-repository-cocoapods-proxy/Specs/1/9/2/libwebp/1.2.0/libwebp.podspec.json`.
regexp: /Adding spec repo .* with CDN .*\n\s*\[!\] No podspec exists at path `(.*)`/,
// Some pods are hosted on git registries that are not supported e.g. chromium.googlesource.com
createError: (match: RegExpMatchArray) =>
new TrackedBuildError(
'COCOAPODS_CACHE_INCOMPATIBLE_REPO_ERROR',
`cocoapods: Missing podspec ${match[1]}.`
),
},
{
platform: Platform.IOS,
phase: BuildPhase.INSTALL_PODS,
// [!] Invalid `Podfile` file: 783: unexpected token at 'info Run CLI with --verbose flag for more details.
regexp:
/\[!\] Invalid `Podfile` file: .* unexpected token at 'info Run CLI with --verbose flag for more details./,
createError: () =>
new TrackedBuildError('NODE_ENV_PRODUCTION_DEFINED', 'npm: NODE_ENV=production was defined.'),
},
...[BuildPhase.INSTALL_DEPENDENCIES, BuildPhase.PREBUILD].map(phase => ({
phase,
// example log:
// [stderr] WARN tarball tarball data for @typescript-eslint/[email protected] (sha512-cozo/GbwixVR0sgfHItz3t1yXu521yn71Wj6PlYCFA3WPhy51CUPkifFKfBis91bDclGmAY45hhaAXVjdn4new==) seems to be corrupted. Trying again.
regexp: /tarball tarball data for ([^ ]*) .* seems to be corrupted. Trying again/,
createError: (match: RegExpMatchArray) =>
new TrackedBuildError('NPM_CORRUPTED_PACKAGE', `npm: corrupted package ${match[1]}`, {
packageName: match[1],
}),
})),
...[BuildPhase.INSTALL_DEPENDENCIES, BuildPhase.PREBUILD].map(phase => ({
phase,
regexp: ({ env }: ErrorContext) =>
env.EAS_BUILD_NPM_CACHE_URL
? new RegExp(escapeRegExp(env.EAS_BUILD_NPM_CACHE_URL))
: undefined,
createError: () => new TrackedBuildError('NPM_CACHE_ERROR', `npm: cache error`),
})),
{
platform: Platform.IOS,
phase: BuildPhase.INSTALL_PODS,
regexp: ({ env }: ErrorContext) =>
env.EAS_BUILD_COCOAPODS_CACHE_URL ? /Error installing/ : undefined,
createError: () =>
new TrackedBuildError(
'COCOAPODS_CACHE_INSTALLING_POD_ERROR',
`cocoapods: error installing a pod using internal cache instance`
),
},
{
platform: Platform.IOS,
phase: BuildPhase.INSTALL_PODS,
regexp: ({ env }: ErrorContext) =>
env.EAS_BUILD_COCOAPODS_CACHE_URL ? /No podspec exists at path/ : undefined,
createError: () =>
new TrackedBuildError(
'COCOAPODS_CACHE_NO_PODSPEC_EXISTS_AT_PATH_ERROR',
`cocoapods: error fetching a podspec through internal cache instance`
),
},
{
platform: Platform.IOS,
phase: BuildPhase.INSTALL_PODS,
regexp: ({ env }: ErrorContext) =>
env.EAS_BUILD_COCOAPODS_CACHE_URL
? new RegExp(escapeRegExp(env.EAS_BUILD_COCOAPODS_CACHE_URL))
: undefined,
createError: () => new TrackedBuildError('COCOAPODS_CACHE_ERROR', `cocoapods: cache error`),
},
{
phase: BuildPhase.INSTALL_PODS,
// [!] Invalid `Podfile` file: uninitialized constant Pod::Podfile::FlipperConfiguration.
regexp: /\[!\] Invalid `Podfile` file/,
createError: () => new TrackedBuildError('INVALID_PODFILE', 'pod: Invalid Podfile file.'),
},
{
phase: BuildPhase.INSTALL_DEPENDENCIES,
// info There appears to be trouble with your network connection. Retrying...
regexp: /info There appears to be trouble with your network connection. Retrying/,
createError: () =>
new TrackedBuildError(
'YARN_INSTALL_TROUBLE_WITH_NETWORK_CONNECTION',
'yarn: There appears to be trouble with your network connection'
),
},
{
phase: BuildPhase.RUN_GRADLEW,
platform: Platform.ANDROID,
// Android Gradle plugin requires Java 11 to run. You are currently using Java 1.8
regexp: /Android Gradle plugin requires Java .* to run. You are currently using Java/,
createError: () =>
new TrackedBuildError('INCOMPATIBLE_JAVA_VERSION', 'gradle: Incompatible java version.'),
},
{
phase: BuildPhase.RUN_GRADLEW,
platform: Platform.ANDROID,
// /home/expo/workingdir/build/android/app/src/main/AndroidManifest.xml:27:9-33:20 Error:
// android:exported needs to be explicitly specified for element <activity#androidx.test.core.app.InstrumentationActivityInvoker$EmptyActivity>. Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported` when the corresponding component has an intent filter defined. See https://developer.android.com/guide/topics/manifest/activity-element#exported for details.
regexp:
/Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported`/,
createError: () =>
new TrackedBuildError(
'REQUIRE_EXPLICIT_EXPORTED_ANDROID_12',
'Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported`.'
),
},
{
phase: BuildPhase.RUN_GRADLEW,
platform: Platform.ANDROID,
// > A failure occurred while executing com.android.build.gradle.internal.res.Aapt2CompileRunnable
// > Android resource compilation failed
// ERROR:/home/expo/workingdir/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: AAPT: error: failed to read PNG signature: file does not start with PNG signature.
regexp: /AAPT: error: failed to read PNG signature: file does not start with PNG signature/,
createError: () =>
new TrackedBuildError('INVALID_PNG_SIGNATURE', 'gradle: Invalid PNG signature.'),
},
{
phase: BuildPhase.RUN_GRADLEW,
platform: Platform.ANDROID,
// Execution failed for task ':app:processReleaseGoogleServices'.
// > Malformed root json
regexp: /Execution failed for task ':app:process.*GoogleServices'.*\s.*Malformed root json/,
createError: () =>
new TrackedBuildError(
'GRADLE_MALFORMED_GOOGLE_SERVICES_JSON',
'gradle: Malformed google-services.json.'
),
},
{
phase: BuildPhase.RUN_GRADLEW,
platform: Platform.ANDROID,
// Execution failed for task ':app:processDebugGoogleServices'.
// > Missing project_info object
regexp:
/Execution failed for task ':app:process.*GoogleServices'.*\s.*Missing project_info object/,
createError: () =>
new TrackedBuildError(
'GRADLE_MALFORMED_GOOGLE_SERVICES_JSON',
'gradle: Missing project_info object.'
),
},
{
phase: BuildPhase.RUN_GRADLEW,
platform: Platform.ANDROID,
// Execution failed for task ':app:bundleReleaseJsAndAssets'.
// > Process 'command 'node'' finished with non-zero exit value 1
regexp:
/Execution failed for task ':app:bundleReleaseJsAndAssets'.*\s.*Process 'command 'node'' finished with non-zero exit value/,
createError: () =>
new TrackedBuildError(
'GRADLE_BUILD_BUNDLER_ERROR',
"gradle: ':app:bundleReleaseJsAndAssets' failed."
),
},
{
platform: Platform.ANDROID,
phase: BuildPhase.RUN_GRADLEW,
// > Could not resolve org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.5.10.
// Required by:
// project :expo-updates
// > Could not resolve org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.5.10.
// > Could not get resource 'https://lsdkfjsdlkjf.com/android/releases/org/jetbrains/kotlin/kotlin-annotation-processing-gradle/1.5.10/kotlin-annotation-processing-gradle-1.5.10.pom'.
// > Could not HEAD 'https://slkdfjldskjfl.com/android/releases/org/jetbrains/kotlin/kotlin-annotation-processing-gradle/1.5.10/kotlin-annotation-processing-gradle-1.5.10.pom'.
// > Connect to sdlkfjsdlkf.com:443 [slkdfjdslk.com/38.178.101.5] failed: connect timed out
regexp: /Could not get resource.*\n.*Could not HEAD 'https/,
createError: () =>
new TrackedBuildError('MAVEN_REGISTRY_CONNECTION_ERROR', `maven: registry connection error`),
},
{
platform: Platform.ANDROID,
phase: BuildPhase.RUN_GRADLEW,
regexp: ({ env }: ErrorContext) =>
env.EAS_BUILD_MAVEN_CACHE_URL
? // The BlurView 2.0.3 jar was removed from jitpack.io
// and it caused false positive MAVEN_CACHE_ERROR errors being reported.
new RegExp(
`^(?!.*Could not find BlurView-version-2\\.0\\.3\\.jar).*${escapeRegExp(
env.EAS_BUILD_MAVEN_CACHE_URL
)}`
)
: undefined,
createError: () => new TrackedBuildError('MAVEN_CACHE_ERROR', `maven: cache error`),
},
{
phase: BuildPhase.RUN_FASTLANE,
platform: Platform.IOS,
// error: exportArchive: exportOptionsPlist error for key "iCloudContainerEnvironment": expected one of {Development, Production}, but no value was provided
regexp: /exportArchive: exportOptionsPlist error for key "iCloudContainerEnvironment"/,
createError: () =>
new TrackedBuildError(
'MISSING_ICLOUD_CONTAINER_ENVIRONMENT',
'fastlane: Missing iCloudContainerEnvironment in exportOptionsPlist.'
),
},
{
phase: BuildPhase.RUN_FASTLANE,
platform: Platform.IOS,
// The following build commands failed:
// PhaseScriptExecution [CP-User]\ Generate\ app.manifest\ for\ expo-updates /Users/expo/Library/Developer/Xcode/DerivedData/Kenkohub-eqseedlxbgrzjqagscbclhbtstwh/Build/Intermediates.noindex/ArchiveIntermediates/Kenkohub/IntermediateBuildFilesPath/Pods.build/Release-iphoneos/EXUpdates.build/Script-BB6B5FD28815C045A20B2E5E3FEEBD6E.sh (in target 'EXUpdates' from project 'Pods')
regexp:
/The following build commands failed.*\s.*\[CP-User\]\\ Generate\\ app\.manifest\\ for\\ expo-updates/,
createError: () =>
new TrackedBuildError(
'XCODE_BUILD_UPDATES_PHASE_SCRIPT',
'fastlane: Generating app.manifest for expo-updates failed.'
),
},
{
phase: BuildPhase.RUN_FASTLANE,
platform: Platform.IOS,
// The following build commands failed:
// CompileC /Users/expo/Library/Developer/Xcode/DerivedData/Docent-amxxhphjfdtkpxecgidgzvwvnvtc/Build/Intermediates.noindex/ArchiveIntermediates/Docent/IntermediateBuildFilesPath/Pods.build/Release-iphoneos/Flipper-Folly.build/Objects-normal/arm64/SSLErrors.o /Users/expo/workingdir/build/ios/Pods/Flipper-Folly/folly/io/async/ssl/SSLErrors.cpp normal arm64 c++ com.apple.compilers.llvm.clang.1_0.compiler (in target 'Flipper-Folly' from project 'Pods')
regexp: /in target 'Flipper-Folly' from project 'Pods'/,
createError: () =>
new TrackedBuildError(
'FLIPPER_FOLLY_COMPILE_ERROR',
'fastlane: Flipper-Folly compile error.'
),
},
{
phase: BuildPhase.RUN_FASTLANE,
platform: Platform.IOS,
// The following build commands failed:
// PhaseScriptExecution Bundle\ React\ Native\ code\ and\ images /Users/expo/Library/Developer/Xcode/DerivedData/cnaxwpahkhcjluhigkcwrturapmm/Build/Intermediates.noindex/ArchiveIntermediates/Test/IntermediateBuildFilesPath/Test.build/Release-iphoneos/Test.build/Script-00DD1BFF151E006B06BC.sh (in target 'Test' from project 'Test')
regexp:
/The following build commands failed.*\s.*PhaseScriptExecution Bundle\\ React\\ Native\\ code\\ and\\ images \/Users\/expo/,
createError: () =>
new TrackedBuildError(
'XCODE_BUILD_BUNDLER_ERROR',
'fastlane: Bundle React Native code and images failed.'
),
},
];
@@ -0,0 +1,92 @@
import { BuildPhase, errors } from '@expo/eas-build-job';
import fs from 'fs-extra';
import { buildErrorHandlers } from './buildErrorHandlers';
import { ErrorContext, ErrorHandler, XCODE_BUILD_PHASE } from './errors.types';
import { userErrorHandlers } from './userErrorHandlers';
import { findXcodeBuildLogsPathAsync } from '../ios/xcodeBuildLogs';
async function maybeReadXcodeBuildLogs(
phase: BuildPhase,
buildLogsDirectory: string
): Promise<string | undefined> {
if (phase !== BuildPhase.RUN_FASTLANE) {
return;
}
try {
const xcodeBuildLogsPath = await findXcodeBuildLogsPathAsync(buildLogsDirectory);
if (!xcodeBuildLogsPath) {
return;
}
return await fs.readFile(xcodeBuildLogsPath, 'utf-8');
} catch {
return undefined;
}
}
function resolveError<TError extends Error>(
errorHandlers: ErrorHandler<TError>[],
logLines: string[],
errorContext: ErrorContext,
xcodeBuildLogs?: string
): TError | undefined {
const { job, phase } = errorContext;
const { platform } = job;
const logs = logLines.join('\n');
const handlers = errorHandlers
.filter(handler => handler.platform === platform || !handler.platform)
.filter(
handler =>
(handler.phase === XCODE_BUILD_PHASE && phase === BuildPhase.RUN_FASTLANE) ||
handler.phase === phase ||
!handler.phase
)
.filter(handler => ('mode' in job && handler.mode === job.mode) || !handler.mode);
for (const handler of handlers) {
const regexp =
typeof handler.regexp === 'function' ? handler.regexp(errorContext) : handler.regexp;
if (!regexp) {
continue;
}
const match =
handler.phase === XCODE_BUILD_PHASE ? xcodeBuildLogs?.match(regexp) : logs.match(regexp);
if (match) {
return handler.createError(match, errorContext);
}
}
return undefined;
}
export async function resolveBuildPhaseErrorAsync(
error: any,
logLines: string[],
errorContext: ErrorContext,
buildLogsDirectory: string
): Promise<errors.ExpoError> {
const { phase } = errorContext;
if (error instanceof errors.ExpoError) {
error.buildPhase ??= phase;
return error;
}
const xcodeBuildLogs = await maybeReadXcodeBuildLogs(phase, buildLogsDirectory);
const userError =
resolveError(userErrorHandlers, logLines, errorContext, xcodeBuildLogs) ??
new errors.UnknownError(errorContext.phase);
const buildError = resolveError(buildErrorHandlers, logLines, errorContext, xcodeBuildLogs);
const trackingCode =
buildError && buildError.errorCode !== userError.errorCode ? buildError.errorCode : undefined;
return new errors.UserError(userError.errorCode, userError.message, {
trackingCode,
docsUrl: userError.docsUrl,
buildPhase: phase,
metadata: buildError?.metadata,
cause: error,
});
}
@@ -0,0 +1,17 @@
import { BuildMode, BuildPhase, Env, Job, Platform } from '@expo/eas-build-job';
export interface ErrorContext<TJob extends Job = Job> {
phase: BuildPhase;
job: TJob;
env: Env;
}
export const XCODE_BUILD_PHASE = 'XCODE_BUILD';
export interface ErrorHandler<T extends Error> {
regexp: RegExp | ((ctx: ErrorContext) => RegExp | undefined);
platform?: Platform;
phase?: BuildPhase | typeof XCODE_BUILD_PHASE;
mode?: BuildMode;
createError: (matchResult: RegExpMatchArray, errCtx: ErrorContext) => T | undefined;
}
@@ -0,0 +1,317 @@
import { BuildMode, BuildPhase, Platform, Workflow, errors } from '@expo/eas-build-job';
import { ErrorHandler, XCODE_BUILD_PHASE } from './errors.types';
import UserError = errors.UserError;
export const userErrorHandlers: ErrorHandler<UserError>[] = [
{
platform: Platform.IOS,
phase: BuildPhase.INSTALL_PODS,
regexp: /requires CocoaPods version/,
// example log:
// [!] `React` requires CocoaPods version `>= 1.10.1`, which is not satisfied by your current version, `1.10.0`.
createError: () =>
new UserError(
'EAS_BUILD_UNSUPPORTED_COCOAPODS_VERSION_ERROR',
`Your project requires a newer version of CocoaPods. You can update it in the build profile in eas.json by either:
- changing the current version under key "cocoapods"
- switching to an image that supports that version under key "image"`,
{ docsUrl: 'https://docs.expo.dev/build-reference/eas-json/' }
),
},
{
platform: Platform.IOS,
phase: BuildPhase.RUN_FASTLANE,
regexp: /Could not find 'bundler' (.*) required by your/,
// example log:
// /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/rubygems/dependency.rb:313:in `to_specs': Could not find 'bundler' (2.2.3) required by your /Users/expo/project/build/ios/Gemfile.lock. (Gem::MissingSpecVersionError)
createError: () =>
new UserError(
'EAS_BUILD_UNSUPPORTED_BUNDLER_VERSION_ERROR',
`Your project requires a different version of the Ruby "bundler" program than the version installed in this EAS Build environment. You can specify which version of "bundler" to install by specifying the version under "build"→[buildProfileName]→"ios"→"bundler" in eas.json.`,
{ docsUrl: 'https://docs.expo.dev/build-reference/eas-json/' }
),
},
{
platform: Platform.ANDROID,
phase: BuildPhase.RUN_GRADLEW,
// example log:
// > Failed to read key keyalias from store "/build/workingdir/build/generic/keystore-5787e6af-3002-4cb7-8a57-3e73d13313c2.jks": Invalid keystore format
regexp: /Invalid keystore format/,
createError: () =>
new UserError(
'EAS_BUILD_INVALID_KEYSTORE_FORMAT_ERROR',
'The keystore used in this build is malformed or it has an unsupported type. Make sure you provided the correct file.'
),
},
{
platform: Platform.ANDROID,
phase: BuildPhase.RUN_GRADLEW,
// example log:
// > Failed to read key keyalias from store "/build/workingdir/build/generic/keystore-286069a8-4bb9-48a6-add9-acf6b58ea06d.jks": null
regexp: /Failed to read key[^\n]+from store/,
createError: () =>
new UserError(
'EAS_BUILD_INVALID_KEYSTORE_ALIAS_ERROR',
'The alias specified for this keystore does not exist. Make sure you specified the correct value.'
),
},
{
platform: Platform.ANDROID,
phase: BuildPhase.PREBUILD,
// example log:
// [15:42:05] Error: Cannot copy google-services.json from /build/workingdir/build/managed/abc to /build/workingdir/build/managed/android/app/google-services.json
// or
// [11:17:29] [android.dangerous]: withAndroidDangerousBaseMod: Cannot copy google-services.json from /home/expo/workingdir/build/test/test-google-services.json to /home/expo/workingdir/build/android/app/google-services.json. Please make sure the source and destination paths exist.
// [11:17:29] Error: [android.dangerous]: withAndroidDangerousBaseMod: Cannot copy google-services.json from /home/expo/workingdir/build/test/test-google-services.json to /home/expo/workingdir/build/android/app/google-services.json. Please make sure the source and destination paths exist.
regexp: /Cannot copy google-services\.json/,
createError: () =>
new UserError(
'EAS_BUILD_MISSING_GOOGLE_SERVICES_JSON_ERROR',
'"google-services.json" is missing, make sure that the file exists. Remember that EAS Build only uploads the files tracked by git. Use EAS environment variables to provide EAS Build with the file.',
{ docsUrl: 'https://docs.expo.dev/eas/environment-variables/#file-environment-variables' }
),
},
{
platform: Platform.ANDROID,
phase: BuildPhase.RUN_GRADLEW,
// Execution failed for task ':app:processReleaseGoogleServices'.
// > File google-services.json is missing. The Google Services Plugin cannot function without it.
// Searched Location:
regexp:
/File google-services\.json is missing\. The Google Services Plugin cannot function without it/,
createError: () =>
new UserError(
'EAS_BUILD_MISSING_GOOGLE_SERVICES_JSON_ERROR',
'"google-services.json" is missing, make sure that the file exists. Remember that EAS Build only uploads the files tracked by git. Use EAS environment variables to provide EAS Build with the file.',
{ docsUrl: 'https://docs.expo.dev/eas/environment-variables/#file-environment-variables' }
),
},
{
platform: Platform.IOS,
phase: BuildPhase.PREBUILD,
// example log:
// [08:44:18] ENOENT: no such file or directory, copyfile '/Users/expo/workingdir/build/managed/abc' -> '/Users/expo/workingdir/build/managed/ios/testapp/GoogleService-Info.plist'
regexp: /ENOENT: no such file or directory, copyfile .*GoogleService-Info.plist/,
createError: () =>
new UserError(
'EAS_BUILD_MISSING_GOOGLE_SERVICES_PLIST_ERROR',
'"GoogleService-Info.plist" is missing, make sure that the file exists. Remember that EAS Build only uploads the files tracked by git. Use EAS environment variables to provide EAS Build with the file.',
{ docsUrl: 'https://docs.expo.dev/eas/environment-variables/#file-environment-variables' }
),
},
{
platform: Platform.IOS,
phase: BuildPhase.INSTALL_PODS,
// example log:
// [!] CocoaPods could not find compatible versions for pod "react-native-google-maps"
// In Podfile:
// react-native-google-maps (from `/Users/expo/workingdir/build/node_modules/react-native-maps`)
// Specs satisfying the `react-native-google-maps (from `/Users/expo/workingdir/build/node_modules/react-native-maps`)` dependency were found, but they required a higher minimum deployment target.
// Error: Compatible versions of some pods could not be resolved.
regexp:
/Specs satisfying the `(.*)` dependency were found, but they required a higher minimum deployment target/,
createError: (_, { job }) => {
return new UserError(
'EAS_BUILD_HIGHER_MINIMUM_DEPLOYMENT_TARGET_ERROR',
`Some pods require a higher minimum deployment target.
${
'type' in job && job.type === Workflow.MANAGED
? 'You can use the expo-build-properties config plugin (https://docs.expo.dev/versions/latest/sdk/build-properties/) to override the default native build properties and set a different minimum deployment target.'
: 'You need to manually update the minimum deployment target in your project to resolve this issue.'
}
`
);
},
},
{
platform: Platform.IOS,
phase: BuildPhase.INSTALL_PODS,
// example log:
// [!] CocoaPods could not find compatible versions for pod "Firebase/Core":
// In snapshot (Podfile.lock):
// Firebase/Core (= 6.14.0)
// In Podfile:
// EXFirebaseCore (from `../node_modules/expo-firebase-core/ios`) was resolved to 3.0.0, which depends on
// Firebase/Core (= 7.7.0)
// You have either:
// * out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`.
// * changed the constraints of dependency `Firebase/Core` inside your development pod `EXFirebaseCore`.
// You should run `pod update Firebase/Core` to apply changes you've made.
regexp: /CocoaPods could not find compatible versions for pod /,
createError: () => {
return new UserError(
'EAS_BUILD_INCOMPATIBLE_PODS_ERROR',
`Compatible versions of some pods could not be resolved.
You are seeing this error because either:
- Some of the pods used in your project depend on different versions of the same pod. See logs for more information.
- If you are caching Podfile.lock using "cache" field in eas.json, then versions there might not match required values in Podspecs of some installed libraries. To fix this, you can re-run build command with "--clear-cache" option, or select "Clear cache and retry build" on the build page.
`
);
},
},
{
phase: BuildPhase.INSTALL_DEPENDENCIES,
// example log:
// [stderr] npm ERR! Fix the upstream dependency conflict, or retry
// [stderr] npm ERR! this command with --force, or --legacy-peer-deps
// [stderr] npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
regexp:
/Fix the upstream dependency conflict, or retry.*\s.*this command with --force, or --legacy-peer-deps/,
createError: (matchResult: RegExpMatchArray) => {
if (matchResult.length >= 2) {
return new UserError(
'EAS_BUILD_NPM_CONFLICTING_PEER_DEPENDENCIES',
`Some of your peer dependencies are not compatible. The recommended approach is to fix your dependencies by resolving any conflicts listed by "npm install". As a temporary workaround you can:
- Add ".npmrc" file with "legacy-peer-deps=true" and commit that to your repo.
- Delete package-lock.json and use yarn instead. It does not enforce peer dependencies.
- Downgrade to older version of npm on EAS Build, by adding "npm install -g npm@version" in "eas-build-pre-install" script in package.json.`
);
}
return undefined;
},
},
{
phase: BuildPhase.INSTALL_DEPENDENCIES,
// example log:
// [stderr] error https://github.com/expo/react-native/archive/sdk-41.0.0.tar.gz: Integrity check failed for "react-native" (computed integrity doesn't match our records, got "sha512-3jHI2YufrJi7eIABRf/DN/I2yOkmIZ0vAyezTz+PAUJiEs4v//5LLojWEU+W53AZsnuaEMcl/4fVy4bd+OuUbA== sha1-o9QuQTXIkc8VozXPaZIullB9a40=")
regexp: /Integrity check failed for "(.*)" \(computed integrity doesn't match our records, got/,
createError: (matchResult: RegExpMatchArray) => {
if (matchResult.length >= 2) {
return new UserError(
'EAS_BUILD_YARN_LOCK_CHECKSUM_ERROR',
`Checksum for package "${matchResult[1]}" does not match value in registry. To fix that:
- run "yarn cache clean"
- remove yarn.lock (or only the section for that package)
- run "yarn install --force"`
);
}
return undefined;
},
},
{
phase: BuildPhase.INSTALL_DEPENDENCIES,
// example log:
// yarn install v1.22.17
// [1/4] Resolving packages...
// [2/4] Fetching packages...
// [1/4] Resolving packages...
// [2/4] Fetching packages...
// [stderr] error https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz: Extracting tar content of undefined failed, the file appears to be corrupt: "ENOENT: no such file or directory, chmod '/Users/expo/Library/Caches/Yarn/v6/npm-jest-util-26.6.2-907535dbe4d5a6cb4c47ac9b926f6af29576cbc1-integrity/node_modules/jest-util/build/pluralize.d.ts'"
regexp:
/\[1\/4\] Resolving packages...\s*\[2\/4\] Fetching packages...\s*\[1\/4\] Resolving packages...\s*\[2\/4\] Fetching packages.../,
createError: (matchResult: RegExpMatchArray) => {
if (matchResult) {
return new UserError(
'EAS_BUILD_YARN_MULTIPLE_INSTANCES_ERROR',
`One of project dependencies is starting new install process while the main one is still in progress, which might result in corrupted packages. Most likely the reason for error is "prepare" script in git-referenced dependency of your project. Learn more: https://github.com/yarnpkg/yarn/issues/7212#issuecomment-493720324`
);
}
return undefined;
},
},
{
platform: Platform.IOS,
phase: XCODE_BUILD_PHASE,
// Prepare packages
// Computing target dependency graph and provisioning inputs
// Create build description
// Build description signature: 33a5c28977280822abe5e7bd7fe02529
// Build description path: /Users/expo/Library/Developer/Xcode/DerivedData/testapp-fazozgerxcvvfifkipojsjftgyih/Build/Intermediates.noindex/ArchiveIntermediates/testapp/IntermediateBuildFilesPath/XCBuildData/33a5c28977280822abe5e7bd7fe02529-desc.xcbuild
// note: Building targets in dependency order
// /Users/expo/workingdir/build/managed/ios/Pods/Pods.xcodeproj: error: Signing for "EXConstants-EXConstants" requires a development team. Select a development team in the Signing & Capabilities editor. (in target 'EXConstants-EXConstants' from project 'Pods')
// /Users/expo/workingdir/build/managed/ios/Pods/Pods.xcodeproj: error: Signing for "React-Core-AccessibilityResources" requires a development team. Select a development team in the Signing & Capabilities editor. (in target 'React-Core-AccessibilityResources' from project 'Pods')
// warning: Run script build phase '[CP-User] Generate app.manifest for expo-updates' will be run during every build because it does not specify any outputs. To address this warning, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'EXUpdates' from project 'Pods')
// warning: Run script build phase 'Start Packager' will be run during every build because it does not specify any outputs. To address this warning, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'testapp' from project 'testapp')
// warning: Run script build phase 'Bundle React Native code and images' will be run during every build because it does not specify any outputs. To address this warning, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'testapp' from project 'testapp')
// warning: Run script build phase '[CP-User] Generate app.config for prebuilt Constants.manifest' will be run during every build because it does not specify any outputs. To address this warning, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'EXConstants' from project 'Pods')
// /Users/expo/workingdir/build/managed/ios/Pods/Pods.xcodeproj: error: Signing for "EXUpdates-EXUpdates" requires a development team. Select a development team in the Signing & Capabilities editor. (in target 'EXUpdates-EXUpdates' from project 'Pods')
regexp: /error: Signing for "[a-zA-Z-0-9_]+" requires a development team/,
createError: (_, { job }) =>
'type' in job && job.type === Workflow.MANAGED
? new UserError(
'XCODE_RESOURCE_BUNDLE_CODE_SIGNING_ERROR',
`Starting from Xcode 14, resource bundles are signed by default, which requires setting the development team for each resource bundle target.
To resolve this issue, downgrade to an older Xcode version using the "image" field in eas.json, or upgrade to SDK 46 or higher.`,
{
docsUrl:
'https://docs.expo.dev/build-reference/infrastructure/#ios-build-server-configurations',
}
)
: new UserError(
'XCODE_RESOURCE_BUNDLE_CODE_SIGNING_ERROR',
`Starting from Xcode 14, resource bundles are signed by default, which requires setting the development team for each resource bundle target.
To resolve this issue, downgrade to an older Xcode version using the "image" field in eas.json, or turn off signing resource bundles in your Podfile: https://expo.fyi/r/disable-bundle-resource-signing`,
{
docsUrl:
'https://docs.expo.dev/build-reference/infrastructure/#ios-build-server-configurations',
}
),
},
{
platform: Platform.ANDROID,
phase: BuildPhase.RUN_GRADLEW,
regexp: /.*/,
createError: () =>
new UserError(
errors.ErrorCode.UNKNOWN_GRADLE_ERROR,
'Gradle build failed with unknown error. See logs for the "Run gradlew" phase for more information.'
),
},
{
platform: Platform.IOS,
phase: BuildPhase.RUN_FASTLANE,
mode: BuildMode.RESIGN,
regexp: /No provisioning profile for application: '(.+)' with bundle identifier '(.+)'/,
createError: () =>
new UserError(
'EAS_BUILD_RESIGN_PROVISIONING_PROFILE_MISMATCH_ERROR',
`The bundle identifier in provisioning profile used to resign the app does not match the bundle identifier of the app selected to be resigned. See logs above for more information.`
),
},
{
platform: Platform.IOS,
phase: BuildPhase.RUN_FASTLANE,
mode: BuildMode.RESIGN,
regexp: /.*/,
createError: () =>
new UserError(
errors.ErrorCode.UNKNOWN_FASTLANE_RESIGN_ERROR,
`The "Run fastlane" step failed with an unknown error.`
),
},
{
platform: Platform.IOS,
phase: XCODE_BUILD_PHASE,
// MkDir /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/expo-dev-launcher/EXDevLauncher.bundle (in target 'expo-dev-launcher-EXDevLauncher' from project 'Pods')
// cd /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/Pods
// /bin/mkdir -p /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/expo-dev-launcher/EXDevLauncher.bundle
// ProcessXCFramework /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/ProgrammaticAccessLibrary.framework ios simulator
// cd /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios
// builtin-process-xcframework --xcframework /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework --platform ios --environment simulator --target-path /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator
// /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework:1:1: error: The signature of “ProgrammaticAccessLibrary.xcframework” cannot be verified.
// note: A sealed resource is missing or invalid
// note: /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework: a sealed resource is missing or invalid
// file modified: /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework/ios-arm64_x86_64-simulator/ProgrammaticAccessLibrary.framework/ProgrammaticAccessLibrary
// file modified: /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework/ios-arm64/ProgrammaticAccessLibrary.framework/ProgrammaticAccessLibrary
regexp: /error: .+/g,
createError: matchResult =>
new UserError(
'XCODE_BUILD_ERROR',
`The "Run fastlane" step failed because of an error in the Xcode build process. We automatically detected following errors in your Xcode build logs:\n${matchResult
.map(match => `- ${match.replace('error: ', '')}`)
.join('\n')}\nRefer to "Xcode Logs" below for additional, more detailed logs.`
),
},
{
platform: Platform.IOS,
phase: BuildPhase.RUN_FASTLANE,
regexp: /.*/,
createError: () =>
new UserError(
errors.ErrorCode.UNKNOWN_FASTLANE_ERROR,
`The "Run fastlane" step failed with an unknown error. Refer to "Xcode Logs" below for additional, more detailed logs.`
),
},
];
@@ -0,0 +1,266 @@
import { Android, BuildPhase, BuildPhaseResult } from '@expo/eas-build-job';
import { vol } from 'memfs';
import { createTestAndroidJob } from '../../__tests__/utils/job';
import { createMockLogger } from '../../__tests__/utils/logger';
import { BuildContext } from '../../context';
import { restoreCredentials } from '../../android/credentials';
import androidBuilder from '../android';
import { runBuilderWithHooksAsync } from '../common';
import {
injectConfigureVersionGradleConfig,
injectCredentialsGradleConfig,
} from '../../steps/utils/android/gradleConfig';
jest.mock('../common', () => ({
runBuilderWithHooksAsync: jest.fn(async (ctx, buildFn) => {
await buildFn(ctx);
return ctx.artifacts;
}),
}));
jest.mock('../custom', () => ({
runCustomBuildAsync: jest.fn(),
}));
jest.mock('../../android/credentials');
jest.mock('../../android/gradle', () => ({
ensureLFLineEndingsInGradlewScript: jest.fn(),
resolveGradleCommand: jest.fn(() => ':app:bundleRelease'),
runGradleCommand: jest.fn(),
}));
jest.mock('../../common/eagerBundle', () => ({
eagerBundleAsync: jest.fn(),
shouldUseEagerBundle: jest.fn(() => false),
}));
jest.mock('../../common/prebuild', () => ({
prebuildAsync: jest.fn(),
}));
jest.mock('../../common/setup', () => ({
setupAsync: jest.fn(),
}));
jest.mock('../../steps/functions/restoreBuildCache', () => ({
cacheStatsAsync: jest.fn(),
restoreCcacheAsync: jest.fn(),
}));
jest.mock('../../steps/functions/saveBuildCache', () => ({
saveCcacheAsync: jest.fn(),
}));
jest.mock('../../steps/utils/android/gradleConfig', () => ({
...jest.requireActual('../../steps/utils/android/gradleConfig'),
injectConfigureVersionGradleConfig: jest.fn(),
injectCredentialsGradleConfig: jest.fn(),
}));
jest.mock('../../utils/artifacts', () => ({
uploadApplicationArchive: jest.fn(),
}));
jest.mock('../../utils/expoUpdates', () => ({
configureExpoUpdatesIfInstalledAsync: jest.fn(),
resolveRuntimeVersionForExpoUpdatesIfConfiguredAsync: jest.fn(async () => null),
}));
jest.mock('../../utils/hooks', () => ({
Hook: {
POST_INSTALL: 'POST_INSTALL',
PRE_UPLOAD_ARTIFACTS: 'PRE_UPLOAD_ARTIFACTS',
},
runHookIfPresent: jest.fn(),
}));
jest.mock('../../utils/prepareBuildExecutable', () => ({
prepareExecutableAsync: jest.fn(),
}));
describe(androidBuilder, () => {
beforeEach(() => {
vol.fromJSON(
{
'/workingdir/env/.gitkeep': '',
'/workingdir/build/package.json': JSON.stringify({
name: 'test-app',
version: '1.0.0',
}),
},
'/'
);
});
it('injects Android version config without build credentials', async () => {
const job: Android.Job = {
...createTestAndroidJob(),
secrets: {
buildCredentials: undefined,
},
version: {
versionCode: '42',
versionName: '1.2.3',
},
};
const ctx = new BuildContext(job, {
workingdir: '/workingdir',
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
logger: createMockLogger(),
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
uploadArtifact: jest.fn(),
});
await androidBuilder(ctx);
expect(injectConfigureVersionGradleConfig).toHaveBeenCalledWith(
expect.anything(),
'/workingdir/build',
{
versionCode: '42',
versionName: '1.2.3',
}
);
expect(injectCredentialsGradleConfig).not.toHaveBeenCalled();
expect(restoreCredentials).not.toHaveBeenCalled();
});
it('injects Android credentials config when build credentials are present', async () => {
const ctx = new BuildContext(createTestAndroidJob(), {
workingdir: '/workingdir',
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
logger: createMockLogger(),
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
uploadArtifact: jest.fn(),
});
await androidBuilder(ctx);
expect(injectCredentialsGradleConfig).toHaveBeenCalledWith(
expect.anything(),
'/workingdir/build'
);
expect(restoreCredentials).toHaveBeenCalledWith(ctx);
expect(injectConfigureVersionGradleConfig).not.toHaveBeenCalled();
});
it('marks the configure Android version phase as warning for legacy eas-build.gradle', async () => {
const reportBuildPhaseStats = jest.fn();
const job: Android.Job = {
...createTestAndroidJob(),
secrets: {
buildCredentials: undefined,
},
version: {
versionCode: '42',
versionName: '1.2.3',
},
};
const ctx = new BuildContext(job, {
workingdir: '/workingdir',
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
logger: createMockLogger(),
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
uploadArtifact: jest.fn(),
reportBuildPhaseStats,
});
vol.mkdirSync('/workingdir/build/android/app', { recursive: true });
vol.writeFileSync('/workingdir/build/android/app/eas-build.gradle', '// Legacy content');
await androidBuilder(ctx);
expect(reportBuildPhaseStats).toHaveBeenCalledWith(
expect.objectContaining({
buildPhase: BuildPhase.CONFIGURE_ANDROID_VERSION,
result: BuildPhaseResult.WARNING,
})
);
});
it('marks the prepare credentials phase as warning for legacy eas-build.gradle', async () => {
const reportBuildPhaseStats = jest.fn();
const ctx = new BuildContext(createTestAndroidJob(), {
workingdir: '/workingdir',
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
logger: createMockLogger(),
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
uploadArtifact: jest.fn(),
reportBuildPhaseStats,
});
vol.mkdirSync('/workingdir/build/android/app', { recursive: true });
vol.writeFileSync('/workingdir/build/android/app/eas-build.gradle', '// Legacy content');
await androidBuilder(ctx);
expect(reportBuildPhaseStats).toHaveBeenCalledWith(
expect.objectContaining({
buildPhase: BuildPhase.PREPARE_CREDENTIALS,
result: BuildPhaseResult.WARNING,
})
);
});
it('injects credentials and version config when both are configured', async () => {
const reportBuildPhaseStats = jest.fn();
const job: Android.Job = {
...createTestAndroidJob(),
version: {
versionCode: '42',
versionName: '1.2.3',
},
};
const ctx = new BuildContext(job, {
workingdir: '/workingdir',
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
logger: createMockLogger(),
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
uploadArtifact: jest.fn(),
reportBuildPhaseStats,
});
vol.mkdirSync('/workingdir/build/android/app', { recursive: true });
vol.writeFileSync('/workingdir/build/android/app/eas-build.gradle', '// Legacy content');
await androidBuilder(ctx);
expect(injectCredentialsGradleConfig).toHaveBeenCalledWith(
expect.anything(),
'/workingdir/build'
);
expect(restoreCredentials).toHaveBeenCalledWith(ctx);
expect(injectConfigureVersionGradleConfig).toHaveBeenCalledWith(
expect.anything(),
'/workingdir/build',
{
versionCode: '42',
versionName: '1.2.3',
}
);
expect(reportBuildPhaseStats).toHaveBeenCalledWith(
expect.objectContaining({
buildPhase: BuildPhase.PREPARE_CREDENTIALS,
result: BuildPhaseResult.WARNING,
})
);
expect(reportBuildPhaseStats).toHaveBeenCalledWith(
expect.objectContaining({
buildPhase: BuildPhase.CONFIGURE_ANDROID_VERSION,
result: BuildPhaseResult.WARNING,
})
);
});
it('runs the builder through hooks', async () => {
const ctx = new BuildContext(createTestAndroidJob(), {
workingdir: '/workingdir',
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
logger: createMockLogger(),
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
uploadArtifact: jest.fn(),
});
await androidBuilder(ctx);
expect(runBuilderWithHooksAsync).toHaveBeenCalledWith(ctx, expect.any(Function));
});
});
@@ -0,0 +1,135 @@
import { BuildJob } from '@expo/eas-build-job';
import { BuildWorkflow } from '@expo/steps';
import { vol } from 'memfs';
import { createTestIosJob } from '../../__tests__/utils/job';
import { createMockLogger } from '../../__tests__/utils/logger';
import { prepareProjectSourcesAsync } from '../../common/projectSources';
import { BuildContext } from '../../context';
import { CustomBuildContext } from '../../customBuildContext';
import { findAndUploadXcodeBuildLogsAsync } from '../../ios/xcodeBuildLogs';
import { runCustomBuildAsync } from '../custom';
jest.mock('../../common/projectSources');
jest.mock('../../ios/xcodeBuildLogs');
const findAndUploadXcodeBuildLogsAsyncMock = jest.mocked(findAndUploadXcodeBuildLogsAsync);
describe(runCustomBuildAsync, () => {
let ctx: BuildContext<BuildJob>;
beforeEach(() => {
const job = createTestIosJob();
jest.mocked(prepareProjectSourcesAsync).mockImplementation(async () => {
vol.mkdirSync('/workingdir/env', { recursive: true });
vol.mkdirSync('/workingdir/temporary-custom-build', { recursive: true });
vol.fromJSON(
{
'test.yaml': `
build:
steps:
- eas/checkout
`,
},
'/workingdir/temporary-custom-build'
);
return { handled: true };
});
ctx = new BuildContext(
{
...job,
customBuildConfig: {
path: 'test.yaml',
},
},
{
workingdir: '/workingdir',
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
logger: createMockLogger(),
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
uploadArtifact: jest.fn(),
}
);
});
it('calls findAndUploadXcodeBuildLogsAsync in an iOS job if its artifacts is empty', async () => {
await runCustomBuildAsync(ctx);
expect(findAndUploadXcodeBuildLogsAsyncMock).toHaveBeenCalled();
});
it('does not call findAndUploadXcodeBuildLogsAsync in an iOS job if artifacts is already present', async () => {
ctx.artifacts.XCODE_BUILD_LOGS = 'uploaded';
await runCustomBuildAsync(ctx);
expect(findAndUploadXcodeBuildLogsAsyncMock).not.toHaveBeenCalled();
});
it('retries checking out the project', async () => {
jest.mocked(prepareProjectSourcesAsync).mockImplementationOnce(async () => {
throw new Error('Failed to clone repository');
});
await runCustomBuildAsync(ctx);
expect(prepareProjectSourcesAsync).toHaveBeenCalledTimes(2);
});
it('awaits drainPendingMetricUploads after workflow execution', async () => {
let resolveDrain!: () => void;
const drainSpy = jest
.spyOn(CustomBuildContext.prototype, 'drainPendingMetricUploads')
.mockReturnValue(
new Promise<void>(resolve => {
resolveDrain = resolve;
})
);
let resolved = false;
const resultPromise = runCustomBuildAsync(ctx).then(() => {
resolved = true;
});
await new Promise(r => setImmediate(r));
expect(resolved).toBe(false);
resolveDrain();
await resultPromise;
expect(resolved).toBe(true);
drainSpy.mockRestore();
});
it('awaits drainPendingMetricUploads even when workflow throws', async () => {
let resolveDrain!: () => void;
const drainSpy = jest
.spyOn(CustomBuildContext.prototype, 'drainPendingMetricUploads')
.mockReturnValue(
new Promise<void>(resolve => {
resolveDrain = resolve;
})
);
const executeSpy = jest
.spyOn(BuildWorkflow.prototype, 'executeAsync')
.mockRejectedValue(new Error('workflow failed'));
let rejected = false;
const resultPromise = runCustomBuildAsync(ctx).catch(err => {
rejected = true;
throw err;
});
await new Promise(r => setImmediate(r));
expect(rejected).toBe(false);
resolveDrain();
await expect(resultPromise).rejects.toThrow('workflow failed');
expect(rejected).toBe(true);
drainSpy.mockRestore();
executeSpy.mockRestore();
});
});
@@ -0,0 +1,224 @@
import { Android, BuildMode, BuildPhase, Workflow } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
import path from 'path';
import { runBuilderWithHooksAsync } from './common';
import { runCustomBuildAsync } from './custom';
import { restoreCredentials } from '../android/credentials';
import {
ensureLFLineEndingsInGradlewScript,
resolveGradleCommand,
runGradleCommand,
} from '../android/gradle';
import { eagerBundleAsync, shouldUseEagerBundle } from '../common/eagerBundle';
import { prebuildAsync } from '../common/prebuild';
import { setupAsync } from '../common/setup';
import { Artifacts, BuildContext, SkipNativeBuildError } from '../context';
import {
cacheStatsAsync,
restoreCcacheAsync,
restoreGradleCacheAsync,
} from '../steps/functions/restoreBuildCache';
import { saveCcacheAsync, saveGradleCacheAsync } from '../steps/functions/saveBuildCache';
import {
injectConfigureVersionGradleConfig,
injectCredentialsGradleConfig,
warnIfLegacyEasBuildGradleExists,
} from '../steps/utils/android/gradleConfig';
import { uploadApplicationArchive } from '../utils/artifacts';
import {
configureExpoUpdatesIfInstalledAsync,
resolveRuntimeVersionForExpoUpdatesIfConfiguredAsync,
} from '../utils/expoUpdates';
import { Hook, runHookIfPresent } from '../utils/hooks';
import { prepareExecutableAsync } from '../utils/prepareBuildExecutable';
export default async function androidBuilder(ctx: BuildContext<Android.Job>): Promise<Artifacts> {
if (ctx.job.mode === BuildMode.BUILD) {
await prepareExecutableAsync(ctx);
return await runBuilderWithHooksAsync(ctx, buildAsync);
} else if (ctx.job.mode === BuildMode.RESIGN) {
throw new Error('Not implemented');
} else if (ctx.job.mode === BuildMode.CUSTOM || ctx.job.mode === BuildMode.REPACK) {
return await runCustomBuildAsync(ctx);
} else {
throw new Error('Not implemented');
}
}
async function buildAsync(ctx: BuildContext<Android.Job>): Promise<void> {
await setupAsync(ctx);
const evictUsedBefore = new Date();
const workingDirectory = ctx.getReactNativeProjectDirectory();
const hasNativeCode = ctx.job.type === Workflow.GENERIC;
if (hasNativeCode) {
await ctx.runBuildPhase(BuildPhase.FIX_GRADLEW, async () => {
await ensureLFLineEndingsInGradlewScript(ctx);
});
}
await ctx.runBuildPhase(BuildPhase.PREBUILD, async () => {
if (hasNativeCode) {
ctx.markBuildPhaseSkipped();
ctx.logger.info(
'Skipped running "expo prebuild" because the "android" directory already exists. Learn more about the build process: https://docs.expo.dev/build-reference/android-builds/'
);
return;
}
await prebuildAsync(ctx, {
logger: ctx.logger,
workingDir: ctx.getReactNativeProjectDirectory(),
});
});
await ctx.runBuildPhase(BuildPhase.RESTORE_CACHE, async () => {
if (ctx.isLocal) {
ctx.logger.info('Local builds do not support restoring cache');
return;
}
await ctx.cacheManager?.restoreCache(ctx);
await restoreCcacheAsync({
logger: ctx.logger,
workingDirectory,
platform: ctx.job.platform,
env: ctx.env,
secrets: ctx.job.secrets,
});
await restoreGradleCacheAsync({
logger: ctx.logger,
workingDirectory,
env: ctx.env,
secrets: ctx.job.secrets,
});
});
await ctx.runBuildPhase(BuildPhase.POST_INSTALL_HOOK, async () => {
await runHookIfPresent(ctx, Hook.POST_INSTALL);
});
const resolvedExpoUpdatesRuntimeVersion = await ctx.runBuildPhase(
BuildPhase.CALCULATE_EXPO_UPDATES_RUNTIME_VERSION,
async () => {
return await resolveRuntimeVersionForExpoUpdatesIfConfiguredAsync({
cwd: ctx.getReactNativeProjectDirectory(),
logger: ctx.logger,
appConfig: await ctx.appConfig,
platform: ctx.job.platform,
workflow: ctx.job.type,
env: ctx.env,
});
}
);
if (
nullthrows(ctx.job.secrets, 'Secrets must be defined for non-custom builds').buildCredentials
) {
await ctx.runBuildPhase(BuildPhase.PREPARE_CREDENTIALS, async () => {
await warnIfLegacyEasBuildGradleExists(ctx);
await restoreCredentials(ctx);
await injectCredentialsGradleConfig(ctx.logger, ctx.getReactNativeProjectDirectory());
});
}
if (ctx.job.version?.versionCode || ctx.job.version?.versionName) {
await ctx.runBuildPhase(BuildPhase.CONFIGURE_ANDROID_VERSION, async () => {
await warnIfLegacyEasBuildGradleExists(ctx);
await injectConfigureVersionGradleConfig(ctx.logger, ctx.getReactNativeProjectDirectory(), {
versionCode: ctx.job.version?.versionCode,
versionName: ctx.job.version?.versionName,
});
});
}
await ctx.runBuildPhase(BuildPhase.CONFIGURE_EXPO_UPDATES, async () => {
await configureExpoUpdatesIfInstalledAsync(ctx, {
resolvedRuntimeVersion: resolvedExpoUpdatesRuntimeVersion?.runtimeVersion ?? null,
resolvedFingerprintSources: resolvedExpoUpdatesRuntimeVersion?.fingerprintSources ?? null,
});
});
if (ctx.skipNativeBuild) {
throw new SkipNativeBuildError('Skipping Gradle build');
}
if (!ctx.env.EAS_BUILD_DISABLE_BUNDLE_JAVASCRIPT_STEP && shouldUseEagerBundle(ctx.metadata)) {
await ctx.runBuildPhase(BuildPhase.EAGER_BUNDLE, async () => {
await eagerBundleAsync({
platform: ctx.job.platform,
workingDir: ctx.getReactNativeProjectDirectory(),
logger: ctx.logger,
env: {
...ctx.env,
...(resolvedExpoUpdatesRuntimeVersion?.runtimeVersion
? {
EXPO_UPDATES_FINGERPRINT_OVERRIDE:
resolvedExpoUpdatesRuntimeVersion?.runtimeVersion,
EXPO_UPDATES_WORKFLOW_OVERRIDE: ctx.job.type,
}
: null),
},
packageManager: ctx.packageManager,
});
});
}
await ctx.runBuildPhase(BuildPhase.RUN_GRADLEW, async () => {
const gradleCommand = resolveGradleCommand(ctx.job);
await runGradleCommand(ctx, {
logger: ctx.logger,
gradleCommand,
androidDir: path.join(ctx.getReactNativeProjectDirectory(), 'android'),
...(resolvedExpoUpdatesRuntimeVersion?.runtimeVersion
? {
extraEnv: {
EXPO_UPDATES_FINGERPRINT_OVERRIDE: resolvedExpoUpdatesRuntimeVersion.runtimeVersion,
EXPO_UPDATES_WORKFLOW_OVERRIDE: ctx.job.type,
},
}
: null),
});
});
await ctx.runBuildPhase(BuildPhase.PRE_UPLOAD_ARTIFACTS_HOOK, async () => {
await runHookIfPresent(ctx, Hook.PRE_UPLOAD_ARTIFACTS);
});
await ctx.runBuildPhase(BuildPhase.UPLOAD_APPLICATION_ARCHIVE, async () => {
await uploadApplicationArchive(ctx, {
patternOrPath: ctx.job.applicationArchivePath ?? 'android/app/build/outputs/**/*.{apk,aab}',
rootDir: ctx.getReactNativeProjectDirectory(),
logger: ctx.logger,
});
});
await ctx.runBuildPhase(BuildPhase.SAVE_CACHE, async () => {
if (ctx.isLocal) {
ctx.logger.info('Local builds do not support saving cache.');
return;
}
await ctx.cacheManager?.saveCache(ctx);
await saveCcacheAsync({
logger: ctx.logger,
workingDirectory,
platform: ctx.job.platform,
evictUsedBefore,
env: ctx.env,
secrets: ctx.job.secrets,
});
await saveGradleCacheAsync({
logger: ctx.logger,
workingDirectory,
env: ctx.env,
secrets: ctx.job.secrets,
});
});
await ctx.runBuildPhase(BuildPhase.CACHE_STATS, async () => {
await cacheStatsAsync({
logger: ctx.logger,
env: ctx.env,
secrets: ctx.job.secrets,
});
});
}
@@ -0,0 +1,52 @@
import { BuildJob, BuildPhase, Ios, Platform } from '@expo/eas-build-job';
import { Artifacts, BuildContext } from '../context';
import { findAndUploadXcodeBuildLogsAsync } from '../ios/xcodeBuildLogs';
import { maybeFindAndUploadBuildArtifacts } from '../utils/artifacts';
import { Hook, runHookIfPresent } from '../utils/hooks';
export async function runBuilderWithHooksAsync<T extends BuildJob>(
ctx: BuildContext<T>,
builderAsync: (ctx: BuildContext<T>) => Promise<void>
): Promise<Artifacts> {
try {
let buildSuccess = true;
try {
await builderAsync(ctx);
await ctx.runBuildPhase(BuildPhase.ON_BUILD_SUCCESS_HOOK, async () => {
await runHookIfPresent(ctx, Hook.ON_BUILD_SUCCESS);
});
} catch (err: any) {
buildSuccess = false;
await ctx.runBuildPhase(BuildPhase.ON_BUILD_ERROR_HOOK, async () => {
await runHookIfPresent(ctx, Hook.ON_BUILD_ERROR);
});
throw err;
} finally {
await ctx.runBuildPhase(BuildPhase.ON_BUILD_COMPLETE_HOOK, async () => {
await runHookIfPresent(ctx, Hook.ON_BUILD_COMPLETE, {
extraEnvs: {
EAS_BUILD_STATUS: buildSuccess ? 'finished' : 'errored',
},
});
});
if (ctx.job.platform === Platform.IOS) {
await findAndUploadXcodeBuildLogsAsync(ctx as BuildContext<Ios.Job>, {
logger: ctx.logger,
});
}
await ctx.runBuildPhase(BuildPhase.UPLOAD_BUILD_ARTIFACTS, async () => {
await maybeFindAndUploadBuildArtifacts(ctx, {
logger: ctx.logger,
});
});
}
} catch (err: any) {
err.artifacts = ctx.artifacts;
throw err;
}
return ctx.artifacts;
}
+105
View File
@@ -0,0 +1,105 @@
import { BuildJob, BuildPhase, BuildTrigger, Ios, Platform } from '@expo/eas-build-job';
import { BuildConfigParser, BuildStepGlobalContext, StepsConfigParser, errors } from '@expo/steps';
import assert from 'assert';
import fs from 'fs/promises';
import nullthrows from 'nullthrows';
import path from 'path';
import { resolveEnvFromBuildProfileAsync } from '../common/easBuildInternal';
import { prepareProjectSourcesAsync } from '../common/projectSources';
import { Artifacts, BuildContext } from '../context';
import { CustomBuildContext } from '../customBuildContext';
import { findAndUploadXcodeBuildLogsAsync } from '../ios/xcodeBuildLogs';
import { getEasFunctionGroups } from '../steps/easFunctionGroups';
import { getEasFunctions } from '../steps/easFunctions';
import { retryAsync } from '../utils/retry';
export async function runCustomBuildAsync(ctx: BuildContext<BuildJob>): Promise<Artifacts> {
const customBuildCtx = new CustomBuildContext(ctx);
await ctx.runBuildPhase(BuildPhase.PREPARE_PROJECT, async () => {
await retryAsync(
async () => {
await fs.rm(customBuildCtx.projectSourceDirectory, { recursive: true, force: true });
await fs.mkdir(customBuildCtx.projectSourceDirectory, { recursive: true });
await prepareProjectSourcesAsync(ctx, customBuildCtx.projectSourceDirectory);
},
{
retryOptions: {
retries: 3,
retryIntervalMs: 1_000,
},
}
);
});
if (ctx.job.triggeredBy === BuildTrigger.GIT_BASED_INTEGRATION) {
// We need to setup envs from eas.json
const env = await resolveEnvFromBuildProfileAsync(ctx, {
cwd: path.join(customBuildCtx.projectSourceDirectory, ctx.job.projectRootDirectory ?? '.'),
});
ctx.updateEnv(env);
customBuildCtx.updateEnv(ctx.env);
}
assert(
'steps' in ctx.job || 'customBuildConfig' in ctx.job,
'Steps or custom build config path are required in custom jobs'
);
const globalContext = new BuildStepGlobalContext(customBuildCtx, false);
const easFunctions = getEasFunctions(customBuildCtx);
const easFunctionGroups = getEasFunctionGroups(customBuildCtx);
const parser = ctx.job.steps
? new StepsConfigParser(globalContext, {
externalFunctions: easFunctions,
externalFunctionGroups: easFunctionGroups,
steps: ctx.job.steps,
})
: new BuildConfigParser(globalContext, {
externalFunctions: easFunctions,
externalFunctionGroups: easFunctionGroups,
configPath: path.join(
ctx.getReactNativeProjectDirectory(customBuildCtx.projectSourceDirectory),
nullthrows(
ctx.job.customBuildConfig?.path,
'Steps or custom build config path are required in custom jobs'
)
),
});
const workflow = await ctx.runBuildPhase(BuildPhase.PARSE_CUSTOM_WORKFLOW_CONFIG, async () => {
try {
return await parser.parseAsync();
} catch (parseError: any) {
ctx.logger.error('Failed to parse the custom build config file.');
if (parseError instanceof errors.BuildWorkflowError) {
for (const err of parseError.errors) {
ctx.logger.error({ err });
}
}
throw parseError;
}
});
try {
try {
await workflow.executeAsync();
} finally {
if (!ctx.artifacts.XCODE_BUILD_LOGS && ctx.job.platform === Platform.IOS) {
try {
await findAndUploadXcodeBuildLogsAsync(ctx as BuildContext<Ios.Job>, {
logger: ctx.logger,
});
} catch {
// do nothing, it's a non-breaking error.
}
}
await customBuildCtx.drainPendingMetricUploads();
}
} catch (err: any) {
err.artifacts = ctx.artifacts;
throw err;
}
return ctx.artifacts;
}
@@ -0,0 +1,4 @@
import androidBuilder from './android';
import iosBuilder from './ios';
export { androidBuilder, iosBuilder };
+351
View File
@@ -0,0 +1,351 @@
import { IOSConfig } from '@expo/config-plugins';
import { BuildMode, BuildPhase, Ios, ManagedArtifactType, Workflow } from '@expo/eas-build-job';
import plist from '@expo/plist';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { runBuilderWithHooksAsync } from './common';
import { runCustomBuildAsync } from './custom';
import { eagerBundleAsync, shouldUseEagerBundle } from '../common/eagerBundle';
import { prebuildAsync } from '../common/prebuild';
import { setupAsync } from '../common/setup';
import { Artifacts, BuildContext } from '../context';
import { configureXcodeProject } from '../ios/configure';
import CredentialsManager from '../ios/credentials/manager';
import { runFastlaneGym, runFastlaneResign } from '../ios/fastlane';
import { installPods } from '../ios/pod';
import { downloadApplicationArchiveAsync } from '../ios/resign';
import { resolveArtifactPath, resolveBuildConfiguration, resolveScheme } from '../ios/resolve';
import { Sentry } from '../sentry';
import { parseAndReportXcactivitylog } from '../steps/utils/ios/xcactivitylog';
import { cacheStatsAsync, restoreCcacheAsync } from '../steps/functions/restoreBuildCache';
import { saveCcacheAsync } from '../steps/functions/saveBuildCache';
import { uploadApplicationArchive } from '../utils/artifacts';
import {
configureExpoUpdatesIfInstalledAsync,
resolveRuntimeVersionForExpoUpdatesIfConfiguredAsync,
} from '../utils/expoUpdates';
import { Hook, runHookIfPresent } from '../utils/hooks';
import { prepareExecutableAsync } from '../utils/prepareBuildExecutable';
import { getParentAndDescendantProcessPidsAsync } from '../utils/processes';
const INSTALL_PODS_WARN_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
const INSTALL_PODS_KILL_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
class InstallPodsTimeoutError extends Error {}
export default async function iosBuilder(ctx: BuildContext<Ios.Job>): Promise<Artifacts> {
if (ctx.job.mode === BuildMode.BUILD) {
await prepareExecutableAsync(ctx);
return await runBuilderWithHooksAsync(ctx, buildAsync);
} else if (ctx.job.mode === BuildMode.RESIGN) {
return await resignAsync(ctx);
} else if (ctx.job.mode === BuildMode.CUSTOM || ctx.job.mode === BuildMode.REPACK) {
return await runCustomBuildAsync(ctx);
} else {
throw new Error('Not implemented');
}
}
async function buildAsync(ctx: BuildContext<Ios.Job>): Promise<void> {
await setupAsync(ctx);
const hasNativeCode = ctx.job.type === Workflow.GENERIC;
const evictUsedBefore = new Date();
const credentialsManager = new CredentialsManager(ctx);
const workingDirectory = ctx.getReactNativeProjectDirectory();
let fastlaneResult: { derivedDataPath: string; workspacePath: string } | undefined;
try {
const credentials = await ctx.runBuildPhase(BuildPhase.PREPARE_CREDENTIALS, async () => {
return await credentialsManager.prepare();
});
await ctx.runBuildPhase(BuildPhase.PREBUILD, async () => {
if (hasNativeCode) {
ctx.markBuildPhaseSkipped();
ctx.logger.info(
'Skipped running "expo prebuild" because the "ios" directory already exists. Learn more about the build process: https://docs.expo.dev/build-reference/ios-builds/'
);
return;
}
const extraEnvs: Record<string, string> = credentials?.teamId
? { APPLE_TEAM_ID: credentials.teamId }
: {};
await prebuildAsync(ctx, {
logger: ctx.logger,
workingDir: ctx.getReactNativeProjectDirectory(),
options: { extraEnvs },
});
});
await ctx.runBuildPhase(BuildPhase.RESTORE_CACHE, async () => {
if (ctx.isLocal) {
ctx.logger.info('Local builds do not support restoring cache');
return;
}
await ctx.cacheManager?.restoreCache(ctx);
await restoreCcacheAsync({
logger: ctx.logger,
workingDirectory,
platform: ctx.job.platform,
env: ctx.env,
secrets: ctx.job.secrets,
});
});
await ctx.runBuildPhase(BuildPhase.INSTALL_PODS, async () => {
await runInstallPodsAsync(ctx);
});
await ctx.runBuildPhase(BuildPhase.POST_INSTALL_HOOK, async () => {
await runHookIfPresent(ctx, Hook.POST_INSTALL);
});
const resolvedExpoUpdatesRuntimeVersion = await ctx.runBuildPhase(
BuildPhase.CALCULATE_EXPO_UPDATES_RUNTIME_VERSION,
async () => {
return await resolveRuntimeVersionForExpoUpdatesIfConfiguredAsync({
cwd: ctx.getReactNativeProjectDirectory(),
logger: ctx.logger,
appConfig: await ctx.appConfig,
platform: ctx.job.platform,
workflow: ctx.job.type,
env: ctx.env,
});
}
);
const buildConfiguration = resolveBuildConfiguration(ctx);
if (credentials) {
await ctx.runBuildPhase(BuildPhase.CONFIGURE_XCODE_PROJECT, async () => {
await configureXcodeProject(ctx, { credentials, buildConfiguration });
});
}
await ctx.runBuildPhase(BuildPhase.CONFIGURE_EXPO_UPDATES, async () => {
await configureExpoUpdatesIfInstalledAsync(ctx, {
resolvedRuntimeVersion: resolvedExpoUpdatesRuntimeVersion?.runtimeVersion ?? null,
resolvedFingerprintSources: resolvedExpoUpdatesRuntimeVersion?.fingerprintSources ?? null,
});
});
if (!ctx.env.EAS_BUILD_DISABLE_BUNDLE_JAVASCRIPT_STEP && shouldUseEagerBundle(ctx.metadata)) {
await ctx.runBuildPhase(BuildPhase.EAGER_BUNDLE, async () => {
await eagerBundleAsync({
platform: ctx.job.platform,
workingDir: ctx.getReactNativeProjectDirectory(),
logger: ctx.logger,
env: {
...ctx.env,
...(resolvedExpoUpdatesRuntimeVersion?.runtimeVersion
? {
EXPO_UPDATES_FINGERPRINT_OVERRIDE:
resolvedExpoUpdatesRuntimeVersion?.runtimeVersion,
EXPO_UPDATES_WORKFLOW_OVERRIDE: ctx.job.type,
}
: null),
},
packageManager: ctx.packageManager,
});
});
}
fastlaneResult = await ctx.runBuildPhase(BuildPhase.RUN_FASTLANE, async () => {
const scheme = resolveScheme(ctx);
const entitlements = await readEntitlementsAsync(ctx, { scheme, buildConfiguration });
return await runFastlaneGym(ctx, {
credentials,
scheme,
buildConfiguration,
entitlements,
...(resolvedExpoUpdatesRuntimeVersion?.runtimeVersion
? {
extraEnv: {
EXPO_UPDATES_FINGERPRINT_OVERRIDE:
resolvedExpoUpdatesRuntimeVersion?.runtimeVersion,
EXPO_UPDATES_WORKFLOW_OVERRIDE: ctx.job.type,
},
}
: null),
});
});
} finally {
await ctx.runBuildPhase(BuildPhase.CLEAN_UP_CREDENTIALS, async () => {
await credentialsManager.cleanUp();
});
}
await ctx.runBuildPhase(BuildPhase.PARSE_XCACTIVITYLOG, async () => {
if (ctx.isLocal) {
ctx.logger.info('Local builds skip build performance analysis.');
return;
}
try {
const { derivedDataPath, workspacePath } = nullthrows(fastlaneResult);
await parseAndReportXcactivitylog({
derivedDataPath,
workspacePath,
logger: ctx.logger,
proxyBaseUrl: ctx.env.EAS_BUILD_COCOAPODS_CACHE_URL,
env: ctx.env,
});
} catch (err: any) {
Sentry.capture('Failed to parse xcactivitylog', err);
ctx.markBuildPhaseSkipped();
}
});
await ctx.runBuildPhase(BuildPhase.PRE_UPLOAD_ARTIFACTS_HOOK, async () => {
await runHookIfPresent(ctx, Hook.PRE_UPLOAD_ARTIFACTS);
});
await ctx.runBuildPhase(BuildPhase.UPLOAD_APPLICATION_ARCHIVE, async () => {
await uploadApplicationArchive(ctx, {
patternOrPath: resolveArtifactPath(ctx),
rootDir: ctx.getReactNativeProjectDirectory(),
logger: ctx.logger,
});
});
await ctx.runBuildPhase(BuildPhase.SAVE_CACHE, async () => {
if (ctx.isLocal) {
ctx.logger.info('Local builds do not support saving cache.');
return;
}
await ctx.cacheManager?.saveCache(ctx);
await saveCcacheAsync({
logger: ctx.logger,
workingDirectory,
platform: ctx.job.platform,
evictUsedBefore,
env: ctx.env,
secrets: ctx.job.secrets,
});
});
await ctx.runBuildPhase(BuildPhase.CACHE_STATS, async () => {
await cacheStatsAsync({
logger: ctx.logger,
env: ctx.env,
secrets: ctx.job.secrets,
});
});
}
async function readEntitlementsAsync(
ctx: BuildContext<Ios.Job>,
{ scheme, buildConfiguration }: { scheme: string; buildConfiguration: string }
): Promise<object | null> {
try {
const applicationTargetName =
await IOSConfig.BuildScheme.getApplicationTargetNameForSchemeAsync(
ctx.getReactNativeProjectDirectory(),
scheme
);
const entitlementsPath = IOSConfig.Entitlements.getEntitlementsPath(
ctx.getReactNativeProjectDirectory(),
{
buildConfiguration,
targetName: applicationTargetName,
}
);
if (!entitlementsPath) {
return null;
}
const entitlementsRaw = await fs.readFile(entitlementsPath, 'utf8');
return plist.parse(entitlementsRaw);
} catch (err) {
ctx.logger.warn({ err }, 'Failed to read entitlements');
ctx.markBuildPhaseHasWarnings();
return null;
}
}
async function resignAsync(ctx: BuildContext<Ios.Job>): Promise<Artifacts> {
const applicationArchivePath = await ctx.runBuildPhase(
BuildPhase.DOWNLOAD_APPLICATION_ARCHIVE,
async () => {
return await downloadApplicationArchiveAsync(ctx);
}
);
const credentialsManager = new CredentialsManager(ctx);
try {
const credentials = await ctx.runBuildPhase(BuildPhase.PREPARE_CREDENTIALS, async () => {
return await credentialsManager.prepare();
});
await ctx.runBuildPhase(BuildPhase.RUN_FASTLANE, async () => {
await runFastlaneResign(ctx, {
credentials: nullthrows(credentials),
ipaPath: applicationArchivePath,
});
});
} finally {
await ctx.runBuildPhase(BuildPhase.CLEAN_UP_CREDENTIALS, async () => {
await credentialsManager.cleanUp();
});
}
await ctx.runBuildPhase(BuildPhase.UPLOAD_APPLICATION_ARCHIVE, async () => {
ctx.logger.info(`Application archive: ${applicationArchivePath}`);
await ctx.uploadArtifact({
artifact: {
type: ManagedArtifactType.APPLICATION_ARCHIVE,
paths: [applicationArchivePath],
},
logger: ctx.logger,
});
});
return ctx.artifacts;
}
async function runInstallPodsAsync(ctx: BuildContext<Ios.Job>): Promise<void> {
let warnTimeout: NodeJS.Timeout | undefined;
let killTimeout: NodeJS.Timeout | undefined;
let timedOutToKill: boolean = false;
try {
const installPodsSpawnPromise = (
await installPods(ctx, {
infoCallbackFn: () => {
warnTimeout?.refresh();
killTimeout?.refresh();
},
})
).spawnPromise;
warnTimeout = setTimeout(() => {
ctx.logger.warn(
'"Install pods" phase takes longer then expected and it did not produce any logs in the past 15 minutes'
);
}, INSTALL_PODS_WARN_TIMEOUT_MS);
killTimeout = setTimeout(async () => {
timedOutToKill = true;
ctx.logger.error(
'"Install pods" phase takes a very long time and it did not produce any logs in the past 30 minutes. Most likely an unexpected error happened which caused the process to hang and it will be terminated'
);
const ppid = nullthrows(installPodsSpawnPromise.child.pid);
const pids = await getParentAndDescendantProcessPidsAsync(ppid);
pids.forEach(pid => {
process.kill(pid);
});
ctx.reportError?.('"Install pods" phase takes a very long time', undefined, {
extras: { buildId: ctx.env.EAS_BUILD_ID },
});
}, INSTALL_PODS_KILL_TIMEOUT_MS);
await installPodsSpawnPromise;
} catch (err: any) {
if (timedOutToKill) {
throw new InstallPodsTimeoutError('"Install pods" phase was inactive for over 30 minutes');
}
throw err;
} finally {
if (warnTimeout) {
clearTimeout(warnTimeout);
}
if (killTimeout) {
clearTimeout(killTimeout);
}
}
}
@@ -0,0 +1,72 @@
import { BuildJob } from '@expo/eas-build-job';
import spawn from '@expo/turtle-spawn';
import { resolveEnvFromBuildProfileAsync, runEasBuildInternalAsync } from '../easBuildInternal';
import { resolveEasCommandPrefixAndEnvAsync } from '../../utils/easCli';
jest.mock('@expo/turtle-spawn', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('../../utils/easCli', () => ({
...jest.requireActual('../../utils/easCli'),
resolveEasCommandPrefixAndEnvAsync: jest.fn(),
}));
describe('easBuildInternal', () => {
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(resolveEasCommandPrefixAndEnvAsync).mockResolvedValue({
cmd: 'npx',
args: ['-y', 'eas-cli@latest'],
extraEnv: {},
});
});
it('calls resolveEasCommandPrefixAndEnvAsync in runEasBuildInternalAsync', async () => {
const logger = {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
child: jest.fn(),
} as any;
const job = {
platform: 'android',
secrets: { robotAccessToken: 'token' },
} as unknown as BuildJob;
await expect(
runEasBuildInternalAsync({
job,
logger,
env: {},
cwd: '/tmp/project',
})
).rejects.toThrow('build profile is missing in a build from git-based integration.');
expect(resolveEasCommandPrefixAndEnvAsync).toHaveBeenCalledWith();
expect(spawn).not.toHaveBeenCalled();
});
it('calls resolveEasCommandPrefixAndEnvAsync in resolveEnvFromBuildProfileAsync', async () => {
const logger = {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
child: jest.fn(),
} as any;
const ctx = {
logger,
env: {},
job: { platform: 'ios' },
} as any;
await expect(resolveEnvFromBuildProfileAsync(ctx, { cwd: '/tmp/project' })).rejects.toThrow(
'build profile is missing in a build from git-based integration.'
);
expect(resolveEasCommandPrefixAndEnvAsync).toHaveBeenCalledWith();
expect(spawn).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,575 @@
import {
ArchiveSourceType,
BuildMode,
BuildTrigger,
Job,
Platform,
Workflow,
} from '@expo/eas-build-job';
import { randomBytes, randomUUID } from 'crypto';
import { vol } from 'memfs';
import fetch, { Response } from 'node-fetch';
import { setTimeout } from 'timers/promises';
import { createMockLogger } from '../../__tests__/utils/logger';
import { BuildContext } from '../../context';
import { shallowCloneRepositoryAsync } from '../git';
import { prepareProjectSourcesAsync } from '../projectSources';
jest.mock('@expo/turtle-spawn');
jest.mock('node-fetch');
jest.mock('../git');
jest.mock('@expo/downloader');
jest.mock('@urql/core');
describe('projectSources', () => {
it('should use the refreshed repository URL', async () => {
const robotAccessToken = randomUUID();
const buildId = randomUUID();
await vol.promises.mkdir('/workingdir/environment-secrets/', { recursive: true });
const gitCommitHash = randomBytes(20).toString('hex');
const ctx = new BuildContext(
{
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
type: Workflow.MANAGED,
mode: BuildMode.BUILD,
initiatingUserId: randomUUID(),
appId: randomUUID(),
projectArchive: {
type: ArchiveSourceType.GIT,
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
gitRef: 'refs/heads/main',
gitCommitHash,
},
platform: Platform.IOS,
secrets: {
robotAccessToken,
environmentSecrets: [],
},
} as Job,
{
env: {
__API_SERVER_URL: 'https://api.expo.dev',
EXPO_TOKEN: robotAccessToken,
EAS_BUILD_ID: buildId,
EAS_BUILD_RUNNER: 'eas-build',
},
workingdir: '/workingdir',
logger: createMockLogger(),
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
uploadArtifact: jest.fn(),
}
);
const fetchMock = jest.mocked(fetch);
fetchMock.mockImplementation(
async () =>
({
ok: true,
json: async () => ({
data: {
gitRef: 'refs/heads/main',
gitCommitHash,
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
type: ArchiveSourceType.GIT,
},
}),
}) as Response
);
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
expect(shallowCloneRepositoryAsync).toHaveBeenCalledWith(
expect.objectContaining({
archiveSource: {
...ctx.job.projectArchive,
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
},
})
);
});
it('should fallback to the original repository URL if the refresh fails', async () => {
const robotAccessToken = randomUUID();
const buildId = randomUUID();
await vol.promises.mkdir('/workingdir/environment-secrets/', { recursive: true });
const ctx = new BuildContext(
{
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
type: Workflow.MANAGED,
mode: BuildMode.BUILD,
initiatingUserId: randomUUID(),
appId: randomUUID(),
projectArchive: {
type: ArchiveSourceType.GIT,
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
gitRef: 'refs/heads/main',
gitCommitHash: randomBytes(20).toString('hex'),
},
platform: Platform.IOS,
secrets: {
robotAccessToken,
environmentSecrets: [],
},
} as Job,
{
env: {
__API_SERVER_URL: 'https://api.expo.dev',
EXPO_TOKEN: robotAccessToken,
EAS_BUILD_ID: buildId,
EAS_BUILD_RUNNER: 'eas-build',
},
workingdir: '/workingdir',
logger: createMockLogger(),
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
uploadArtifact: jest.fn(),
}
);
const fetchMock = jest.mocked(fetch);
fetchMock.mockImplementation(
async () =>
({
ok: false,
text: async () => 'Failed to generate repository URL',
}) as Response
);
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
expect(shallowCloneRepositoryAsync).toHaveBeenLastCalledWith(
expect.objectContaining({
archiveSource: {
...ctx.job.projectArchive,
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
},
})
);
fetchMock.mockImplementation(
async () =>
({
ok: false,
json: async () => ({
// repositoryUrl is the right key
repository_url: 'https://x-access-token:[email protected]/expo/eas-cli.git',
}),
}) as Response
);
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
expect(shallowCloneRepositoryAsync).toHaveBeenLastCalledWith(
expect.objectContaining({
archiveSource: {
...ctx.job.projectArchive,
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
},
})
);
fetchMock.mockImplementation(
async () =>
({
ok: false,
json: () => Promise.reject(new Error('Failed to generate repository URL')),
}) as Response
);
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
expect(shallowCloneRepositoryAsync).toHaveBeenLastCalledWith(
expect.objectContaining({
archiveSource: {
...ctx.job.projectArchive,
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
},
})
);
expect(shallowCloneRepositoryAsync).toHaveBeenCalledTimes(3);
}, 15_000);
it('should retry fetching the repository URL', async () => {
const robotAccessToken = randomUUID();
const buildId = randomUUID();
await vol.promises.mkdir('/workingdir/environment-secrets/', { recursive: true });
const gitCommitHash = randomBytes(20).toString('hex');
const ctx = new BuildContext(
{
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
type: Workflow.MANAGED,
mode: BuildMode.BUILD,
initiatingUserId: randomUUID(),
appId: randomUUID(),
projectArchive: {
type: ArchiveSourceType.GIT,
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
gitRef: 'refs/heads/main',
gitCommitHash,
},
platform: Platform.IOS,
secrets: {
robotAccessToken,
environmentSecrets: [],
},
} as Job,
{
env: {
__API_SERVER_URL: 'https://api.expo.dev',
EXPO_TOKEN: robotAccessToken,
EAS_BUILD_ID: buildId,
EAS_BUILD_RUNNER: 'eas-build',
},
workingdir: '/workingdir',
logger: createMockLogger(),
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
uploadArtifact: jest.fn(),
}
);
const fetchMock = jest.mocked(fetch);
fetchMock.mockImplementationOnce(
async () =>
({
ok: false,
text: async () => 'Failed to generate repository URL',
}) as Response
);
fetchMock.mockImplementationOnce(
async () =>
({
ok: true,
json: async () => ({
data: {
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
gitRef: 'refs/heads/main',
gitCommitHash,
type: ArchiveSourceType.GIT,
},
}),
}) as Response
);
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
expect(shallowCloneRepositoryAsync).toHaveBeenLastCalledWith(
expect.objectContaining({
archiveSource: {
...ctx.job.projectArchive,
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
},
})
);
}, 15_000);
it(`should fallback to the original repository URL if we're missing some config`, async () => {
const robotAccessToken = randomUUID();
await vol.promises.mkdir('/workingdir/environment-secrets/', { recursive: true });
const logger = createMockLogger();
const ctx = new BuildContext(
{
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
type: Workflow.MANAGED,
mode: BuildMode.BUILD,
initiatingUserId: randomUUID(),
appId: randomUUID(),
projectArchive: {
type: ArchiveSourceType.GIT,
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
gitRef: 'refs/heads/main',
gitCommitHash: randomBytes(20).toString('hex'),
},
platform: Platform.IOS,
secrets: {
robotAccessToken,
environmentSecrets: [],
},
} as Job,
{
env: {
__API_SERVER_URL: 'https://api.expo.dev',
EXPO_TOKEN: robotAccessToken,
EAS_BUILD_RUNNER: 'eas-build',
// EAS_BUILD_ID: buildId,
},
workingdir: '/workingdir',
logger,
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
uploadArtifact: jest.fn(),
}
);
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
expect(logger.error).toHaveBeenCalledWith(
{ err: expect.any(Error) },
'Failed to refresh project archive, falling back to the original one'
);
});
describe('uploadProjectMetadataAsFireAndForget', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should upload project metadata for build job with URL archive type', async () => {
const robotAccessToken = randomUUID();
const buildId = randomUUID();
const bucketKey = `test-bucket-key-${randomUUID()}`;
await vol.promises.mkdir('/workingdir/build', { recursive: true });
await vol.promises.writeFile('/workingdir/build/package.json', '{}');
await vol.promises.writeFile('/workingdir/build/README.md', 'Hello, world!');
const mockGraphqlMutation = jest.fn();
mockGraphqlMutation
.mockReturnValueOnce({
toPromise: () =>
Promise.resolve({
data: {
uploadSession: {
createUploadSession: {
url: 'https://storage.example.com/upload',
bucketKey,
headers: { 'x-custom-header': 'value' },
},
},
},
}),
})
.mockReturnValueOnce({
toPromise: () =>
Promise.resolve({
data: {
build: {
updateBuildMetadata: {
id: buildId,
},
},
},
}),
});
const ctx = new BuildContext(
{
triggeredBy: BuildTrigger.EAS_CLI,
type: Workflow.MANAGED,
mode: BuildMode.BUILD,
initiatingUserId: randomUUID(),
appId: randomUUID(),
projectArchive: {
type: ArchiveSourceType.URL,
url: 'https://example.com/project.tar.gz',
},
platform: Platform.IOS,
secrets: {
robotAccessToken,
environmentSecrets: [],
},
} as Job,
{
env: {
__API_SERVER_URL: 'https://api.expo.dev',
EXPO_TOKEN: robotAccessToken,
EAS_BUILD_ID: buildId,
EAS_BUILD_RUNNER: 'eas-build',
},
workingdir: '/workingdir',
logger: createMockLogger(),
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
uploadArtifact: jest.fn(),
}
);
ctx.graphqlClient.mutation = mockGraphqlMutation as any;
const fetchMock = jest.mocked(fetch);
fetchMock.mockImplementation(async () => ({ ok: true }) as Response);
// Call prepareProjectSourcesAsync and don't await metadata upload
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
// Wait for the fire-and-forget async operation to complete
await setTimeout(1000);
// Verify the upload session was created
expect(mockGraphqlMutation).toHaveBeenNthCalledWith(
1,
expect.anything(),
expect.objectContaining({})
);
// Verify metadata was uploaded to storage
expect(fetchMock).toHaveBeenCalledWith(
'https://storage.example.com/upload',
expect.objectContaining({
method: 'PUT',
headers: { 'x-custom-header': 'value' },
body: '{"archiveContent":["project/README.md","project/package.json"]}',
})
);
// Verify build metadata was updated
expect(mockGraphqlMutation).toHaveBeenNthCalledWith(
2,
expect.anything(),
expect.objectContaining({
buildId,
projectMetadataFile: {
type: 'GCS',
bucketKey,
},
})
);
});
it('should not await the metadata upload (fire-and-forget)', async () => {
const robotAccessToken = randomUUID();
const buildId = randomUUID();
await vol.promises.mkdir('/workingdir/build', { recursive: true });
await vol.promises.writeFile('/workingdir/build/app.json', '{}');
let uploadCompleted = false;
const mockGraphqlMutation = jest.fn();
mockGraphqlMutation
.mockReturnValueOnce({
toPromise: async () => {
// Simulate a slow upload
await setTimeout(100);
uploadCompleted = true;
return {
data: {
uploadSession: {
createUploadSession: {
url: 'https://storage.example.com/upload',
bucketKey: 'test-key',
headers: {},
},
},
},
};
},
})
.mockReturnValueOnce({
toPromise: () =>
Promise.resolve({
data: {
build: {
updateBuildMetadata: {
id: buildId,
},
},
},
}),
});
const ctx = new BuildContext(
{
triggeredBy: BuildTrigger.EAS_CLI,
type: Workflow.MANAGED,
mode: BuildMode.BUILD,
initiatingUserId: randomUUID(),
appId: randomUUID(),
projectArchive: {
type: ArchiveSourceType.URL,
url: 'https://example.com/project.tar.gz',
},
platform: Platform.IOS,
secrets: {
robotAccessToken,
environmentSecrets: [],
},
} as Job,
{
env: {
__API_SERVER_URL: 'https://api.expo.dev',
EXPO_TOKEN: robotAccessToken,
EAS_BUILD_ID: buildId,
EAS_BUILD_RUNNER: 'eas-build',
},
workingdir: '/workingdir',
logger: createMockLogger(),
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
uploadArtifact: jest.fn(),
}
);
ctx.graphqlClient.mutation = mockGraphqlMutation as any;
const fetchMock = jest.mocked(fetch);
fetchMock.mockResolvedValue({ ok: true } as Response);
const startTime = Date.now();
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
const endTime = Date.now();
// prepareProjectSourcesAsync should complete quickly without waiting for upload
expect(endTime - startTime).toBeLessThan(50);
// Upload should not be completed yet
expect(uploadCompleted).toBe(false);
// Wait for the fire-and-forget operation to complete
await setTimeout(150);
// Now the upload should be completed
expect(uploadCompleted).toBe(true);
});
it('should handle upload errors gracefully without failing the build', async () => {
const robotAccessToken = randomUUID();
const buildId = randomUUID();
await vol.promises.mkdir('/workingdir/build', { recursive: true });
await vol.promises.writeFile('/workingdir/build/index.js', 'console.log("test");');
const mockGraphqlMutation = jest.fn();
mockGraphqlMutation.mockReturnValue({
toPromise: () => Promise.reject(new Error('Upload failed')),
});
const logger = createMockLogger();
const ctx = new BuildContext(
{
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
type: Workflow.MANAGED,
mode: BuildMode.BUILD,
initiatingUserId: randomUUID(),
appId: randomUUID(),
projectArchive: {
type: ArchiveSourceType.URL,
url: 'https://example.com/project.tar.gz',
},
platform: Platform.IOS,
secrets: {
robotAccessToken,
environmentSecrets: [],
},
} as Job,
{
env: {
__API_SERVER_URL: 'https://api.expo.dev',
EXPO_TOKEN: robotAccessToken,
EAS_BUILD_ID: buildId,
EAS_BUILD_RUNNER: 'eas-build',
},
workingdir: '/workingdir',
logger,
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
uploadArtifact: jest.fn(),
}
);
ctx.graphqlClient.mutation = mockGraphqlMutation as any;
const fetchMock = jest.mocked(fetch);
fetchMock.mockResolvedValue({ ok: true } as Response);
// Should not throw even though upload will fail
await expect(prepareProjectSourcesAsync(ctx, ctx.buildDirectory)).resolves.not.toThrow();
// Wait for the fire-and-forget operation to complete
await setTimeout(100);
// Verify that a warning was logged
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('Failed to upload project metadata')
);
});
});
});
@@ -0,0 +1,38 @@
import { Metadata, Platform } from '@expo/eas-build-job';
import { bunyan } from '@expo/logger';
import semver from 'semver';
import { PackageManager } from '../utils/packageManager';
import { runExpoCliCommand } from '../utils/project';
export async function eagerBundleAsync({
platform,
workingDir,
logger,
env,
packageManager,
}: {
platform: Platform;
workingDir: string;
logger: bunyan;
env: Record<string, string | undefined>;
packageManager: PackageManager;
}): Promise<void> {
await runExpoCliCommand({
args: ['export:embed', '--eager', '--platform', platform, '--dev', 'false'],
options: {
cwd: workingDir,
logger,
env,
},
packageManager,
});
}
export function shouldUseEagerBundle(metadata: Metadata | null | undefined): boolean {
return Boolean(
!metadata?.developmentClient &&
metadata?.sdkVersion &&
semver.satisfies(metadata?.sdkVersion, '>=52')
);
}
@@ -0,0 +1,159 @@
import { BuildJob, Env, Metadata, sanitizeBuildJob, sanitizeMetadata } from '@expo/eas-build-job';
import { PipeMode, bunyan } from '@expo/logger';
import { BuildStepEnv } from '@expo/steps';
import spawn from '@expo/turtle-spawn';
import assert from 'assert';
import Joi from 'joi';
import nullthrows from 'nullthrows';
import { BuildContext } from '../context';
import { resolveEasCommandPrefixAndEnvAsync } from '../utils/easCli';
const EasBuildInternalResultSchema = Joi.object<{ job: object; metadata: object }>({
job: Joi.object().unknown(),
metadata: Joi.object().unknown(),
});
export async function runEasBuildInternalAsync<TJob extends BuildJob>({
job,
logger,
env,
cwd,
projectRootOverride,
}: {
job: TJob;
logger: bunyan;
env: BuildStepEnv;
cwd: string;
projectRootOverride?: string;
}): Promise<{
newJob: TJob;
newMetadata: Metadata;
}> {
const { cmd, args, extraEnv } = await resolveEasCommandPrefixAndEnvAsync();
const { buildProfile, githubTriggerOptions } = job;
assert(buildProfile, 'build profile is missing in a build from git-based integration.');
const autoSubmitArgs = [];
if (githubTriggerOptions?.submitProfile) {
autoSubmitArgs.push('--auto-submit-with-profile');
autoSubmitArgs.push(githubTriggerOptions.submitProfile);
} else if (githubTriggerOptions?.autoSubmit) {
autoSubmitArgs.push('--auto-submit');
}
try {
const result = await spawn(
cmd,
[
...args,
'build:internal',
'--platform',
job.platform,
'--profile',
buildProfile,
...autoSubmitArgs,
],
{
cwd,
env: {
...env,
EXPO_TOKEN: nullthrows(job.secrets, 'Secrets must be defined for non-custom builds')
.robotAccessToken,
...extraEnv,
EAS_PROJECT_ROOT: projectRootOverride,
},
logger,
// This prevents printing stdout with job secrets and credentials to logs.
mode: PipeMode.STDERR_ONLY_AS_STDOUT,
}
);
const stdout = result.stdout.toString();
const parsed = JSON.parse(stdout);
return validateEasBuildInternalResult({
result: parsed,
oldJob: job,
});
} catch (err: any) {
throw new Error('Failed to run eas build:internal');
}
}
export async function resolveEnvFromBuildProfileAsync<TJob extends BuildJob>(
ctx: BuildContext<TJob>,
{ cwd }: { cwd: string }
): Promise<Env> {
const { cmd, args, extraEnv } = await resolveEasCommandPrefixAndEnvAsync();
const { buildProfile } = ctx.job;
assert(buildProfile, 'build profile is missing in a build from git-based integration.');
let spawnResult;
try {
spawnResult = await spawn(
cmd,
[
...args,
'config',
'--platform',
ctx.job.platform,
'--profile',
buildProfile,
'--non-interactive',
'--json',
'--eas-json-only',
],
{
cwd,
env: { ...ctx.env, ...extraEnv },
}
);
} catch (err: any) {
ctx.logger.error(`Failed to the read build profile ${buildProfile} from eas.json.`);
ctx.logger.error(err.stderr?.toString());
throw Error(`Failed to read the build profile ${buildProfile} from eas.json.`);
}
const stdout = spawnResult.stdout.toString();
const parsed = JSON.parse(stdout);
const env = validateEnvs(parsed.buildProfile);
return env;
}
function validateEasBuildInternalResult<TJob extends BuildJob>({
oldJob,
result,
}: {
oldJob: TJob;
result: any;
}): { newJob: TJob; newMetadata: Metadata } {
const { value, error } = EasBuildInternalResultSchema.validate(result, {
stripUnknown: true,
convert: true,
abortEarly: false,
});
if (error) {
throw error;
}
const newJob = sanitizeBuildJob({
...value.job,
// We want to retain values that we have set on the job.
appId: oldJob.appId,
initiatingUserId: oldJob.initiatingUserId,
}) as TJob;
assert(newJob.platform === oldJob.platform, 'eas-cli returned a job for a wrong platform');
const newMetadata = sanitizeMetadata(value.metadata);
return { newJob, newMetadata };
}
function validateEnvs(result: any): Env {
const { value, error } = Joi.object({
env: Joi.object().pattern(Joi.string(), Joi.string()),
}).validate(result, {
stripUnknown: true,
convert: true,
abortEarly: false,
});
if (error) {
throw error;
}
return value?.env;
}
@@ -0,0 +1,7 @@
export const COMMON_FASTLANE_ENV = {
FASTLANE_DISABLE_COLORS: '1',
FASTLANE_SKIP_UPDATE_CHECK: '1',
SKIP_SLOW_FASTLANE_WARNING: 'true',
FASTLANE_HIDE_TIMESTAMP: 'true',
LC_ALL: 'en_US.UTF-8',
};
+98
View File
@@ -0,0 +1,98 @@
import { ArchiveSource, ArchiveSourceType } from '@expo/eas-build-job';
import { bunyan } from '@expo/logger';
import spawn from '@expo/turtle-spawn';
export async function shallowCloneRepositoryAsync({
logger,
archiveSource,
destinationDirectory,
}: {
logger: bunyan;
archiveSource: ArchiveSource & { type: ArchiveSourceType.GIT };
destinationDirectory: string;
}): Promise<void> {
const { repositoryUrl } = archiveSource;
try {
await spawn('git', ['init'], { cwd: destinationDirectory });
await spawn('git', ['remote', 'add', 'origin', repositoryUrl], { cwd: destinationDirectory });
const { gitRef, gitCommitHash } = archiveSource;
await spawn('git', ['fetch', 'origin', '--depth', '1', '--no-tags', gitCommitHash], {
cwd: destinationDirectory,
});
await spawn('git', ['checkout', gitCommitHash], { cwd: destinationDirectory });
// If we have a gitRef, we try to add it to the repo.
if (gitRef) {
const { name, type } = getStrippedBranchOrTagName(gitRef);
switch (type) {
// If the gitRef is for a tag, we add a lightweight tag to current commit.
case 'tag': {
await spawn('git', ['tag', name], { cwd: destinationDirectory });
break;
}
// gitRef for a branch may come as:
// - qualified ref (e.g. refs/heads/feature/add-icon), detected as "branch" for a push,
// - unqualified ref (e.g. feature/add-icon), detected as "other" for a pull request.
case 'branch':
case 'other': {
await spawn('git', ['checkout', '-b', name], { cwd: destinationDirectory });
break;
}
}
}
} catch (err: any) {
const sanitizedUrl = getSanitizedGitUrl(repositoryUrl);
if (sanitizedUrl) {
logger.error(`Failed to clone git repository: ${sanitizedUrl}.`);
} else {
logger.error('Failed to clone git repository.');
}
logger.error(err.stderr);
throw err;
}
}
function getSanitizedGitUrl(maybeGitUrl: string): string | null {
try {
const url = new URL(maybeGitUrl);
if (url.password) {
url.password = '*******';
}
return url.toString();
} catch {
return null;
}
}
function getStrippedBranchOrTagName(ref: string): {
name: string;
type: 'branch' | 'tag' | 'other';
} {
const branchRegex = /(\/?refs)?\/?heads\/(.+)/;
const branchMatch = ref.match(branchRegex);
if (branchMatch) {
return {
name: branchMatch[2],
type: 'branch',
};
}
const tagRegex = /(\/?refs)?\/?tags\/(.+)/;
const tagMatch = ref.match(tagRegex);
if (tagMatch) {
return {
name: tagMatch[2],
type: 'tag',
};
}
return {
name: ref,
type: 'other',
};
}
@@ -0,0 +1,89 @@
import { Job } from '@expo/eas-build-job';
import spawn, { SpawnOptions, SpawnPromise, SpawnResult } from '@expo/turtle-spawn';
import path from 'path';
import { BuildContext } from '../context';
import { PackageManager, findPackagerRootDir } from '../utils/packageManager';
import { isUsingModernYarnVersion } from '../utils/project';
export async function installDependenciesAsync({
packageManager,
env,
logger,
infoCallbackFn,
cwd,
useFrozenLockfile,
}: {
packageManager: PackageManager;
env: Record<string, string | undefined>;
cwd: string;
logger: Exclude<SpawnOptions['logger'], undefined>;
infoCallbackFn?: SpawnOptions['infoCallbackFn'];
useFrozenLockfile: boolean;
}): Promise<{ spawnPromise: SpawnPromise<SpawnResult> }> {
let args: string[];
switch (packageManager) {
case PackageManager.NPM: {
args = useFrozenLockfile ? ['ci'] : ['install'];
args.push('--include=dev');
break;
}
case PackageManager.PNPM: {
args = ['install', useFrozenLockfile ? '--frozen-lockfile' : '--no-frozen-lockfile'];
break;
}
case PackageManager.YARN: {
const isModernYarnVersion = await isUsingModernYarnVersion(cwd);
if (isModernYarnVersion) {
if (env['EAS_YARN_FOCUS_WORKSPACE']) {
args = ['workspaces', 'focus', env['EAS_YARN_FOCUS_WORKSPACE']];
} else {
args = [
'install',
'--inline-builds',
useFrozenLockfile ? '--immutable' : '--no-immutable',
];
}
} else {
args = [
'install',
...(useFrozenLockfile ? ['--frozen-lockfile'] : []),
'--production',
'false',
];
}
break;
}
case PackageManager.BUN:
args = ['install', ...(useFrozenLockfile ? ['--frozen-lockfile'] : [])];
break;
default:
throw new Error(`Unsupported package manager: ${packageManager}`);
}
if (env['EAS_VERBOSE'] === '1') {
args = [...args, '--verbose'];
}
logger.info(`Running "${packageManager} ${args.join(' ')}" in ${cwd} directory`);
return {
spawnPromise: spawn(packageManager, args, {
cwd,
logger,
infoCallbackFn,
env,
}),
};
}
export function resolvePackagerDir(ctx: BuildContext<Job>): string {
const packagerRunDir = findPackagerRootDir(ctx.getReactNativeProjectDirectory());
if (packagerRunDir !== ctx.getReactNativeProjectDirectory()) {
const relativeReactNativeProjectDirectory = path.relative(
ctx.buildDirectory,
ctx.getReactNativeProjectDirectory()
);
ctx.logger.info(
`We detected that '${relativeReactNativeProjectDirectory}' is a ${ctx.packageManager} workspace`
);
}
return packagerRunDir;
}
@@ -0,0 +1,66 @@
import { BuildJob } from '@expo/eas-build-job';
import { bunyan } from '@expo/logger';
import { SpawnOptions } from '@expo/turtle-spawn';
import { installDependenciesAsync, resolvePackagerDir } from './installDependencies';
import { BuildContext } from '../context';
import { runExpoCliCommand } from '../utils/project';
export interface PrebuildOptions {
extraEnvs?: Record<string, string>;
}
export async function prebuildAsync<TJob extends BuildJob>(
ctx: BuildContext<TJob>,
{ logger, workingDir, options }: { logger: bunyan; workingDir: string; options?: PrebuildOptions }
): Promise<void> {
const spawnOptions: SpawnOptions = {
cwd: workingDir,
logger,
env: {
EXPO_IMAGE_UTILS_NO_SHARP: '1',
...options?.extraEnvs,
...ctx.env,
},
};
const prebuildCommandArgs = getPrebuildCommandArgs(ctx);
await runExpoCliCommand({
args: prebuildCommandArgs,
options: spawnOptions,
packageManager: ctx.packageManager,
});
const installDependenciesSpawnPromise = (
await installDependenciesAsync({
packageManager: ctx.packageManager,
env: ctx.env,
logger,
cwd: resolvePackagerDir(ctx),
// prebuild sometimes modifies package.json, so we don't want to use frozen lockfile
useFrozenLockfile: false,
})
).spawnPromise;
await installDependenciesSpawnPromise;
}
function getPrebuildCommandArgs<TJob extends BuildJob>(ctx: BuildContext<TJob>): string[] {
let prebuildCommand =
ctx.job.experimental?.prebuildCommand ?? `prebuild --no-install --platform ${ctx.job.platform}`;
if (!prebuildCommand.match(/(?:--platform| -p)/)) {
prebuildCommand = `${prebuildCommand} --platform ${ctx.job.platform}`;
}
const npxCommandPrefix = 'npx ';
const expoCommandPrefix = 'expo ';
const expoCliCommandPrefix = 'expo-cli ';
if (prebuildCommand.startsWith(npxCommandPrefix)) {
prebuildCommand = prebuildCommand.substring(npxCommandPrefix.length).trim();
}
if (prebuildCommand.startsWith(expoCommandPrefix)) {
prebuildCommand = prebuildCommand.substring(expoCommandPrefix.length).trim();
}
if (prebuildCommand.startsWith(expoCliCommandPrefix)) {
prebuildCommand = prebuildCommand.substring(expoCliCommandPrefix.length).trim();
}
return prebuildCommand.split(' ');
}
@@ -0,0 +1,296 @@
import downloadFile from '@expo/downloader';
import { ArchiveSource, ArchiveSourceSchemaZ, ArchiveSourceType, Job } from '@expo/eas-build-job';
import { bunyan } from '@expo/logger';
import { asyncResult } from '@expo/results';
import spawn from '@expo/turtle-spawn';
import fs from 'fs/promises';
import { graphql } from 'gql.tada';
import fetch from 'node-fetch';
import nullthrows from 'nullthrows';
import path from 'path';
import { z } from 'zod';
import { shallowCloneRepositoryAsync } from './git';
import { BuildContext } from '../context';
import { turtleFetch } from '../utils/turtleFetch';
export async function prepareProjectSourcesAsync<TJob extends Job>(
ctx: BuildContext<TJob>,
destinationDirectory: string
): // Return type required to make switch exhaustive.
Promise<{ handled: boolean }> {
let projectArchive: ArchiveSource = ctx.job.projectArchive;
if (ctx.isLocal) {
console.warn('Local build, skipping project archive refresh');
} else {
const projectArchiveResult = await asyncResult(fetchProjectArchiveSourceAsync(ctx));
if (!projectArchiveResult.ok) {
ctx.logger.error(
{ err: projectArchiveResult.reason },
'Failed to refresh project archive, falling back to the original one'
);
}
projectArchive = projectArchiveResult.value ?? ctx.job.projectArchive;
}
switch (projectArchive.type) {
case ArchiveSourceType.R2:
case ArchiveSourceType.GCS: {
throw new Error('Remote project sources should be resolved earlier to URL');
}
case ArchiveSourceType.PATH: {
await prepareProjectSourcesLocallyAsync(ctx, projectArchive.path, destinationDirectory); // used in eas build --local
return { handled: true };
}
case ArchiveSourceType.NONE: {
// May be used in no-sources jobs like submission jobs.
return { handled: true };
}
case ArchiveSourceType.URL: {
await downloadAndUnpackProjectFromTarGzAsync(ctx, projectArchive.url, destinationDirectory);
uploadProjectMetadataAsFireAndForget(ctx, { projectDirectory: destinationDirectory });
return { handled: true };
}
case ArchiveSourceType.GIT: {
await shallowCloneRepositoryAsync({
logger: ctx.logger,
archiveSource: projectArchive,
destinationDirectory,
});
uploadProjectMetadataAsFireAndForget(ctx, { projectDirectory: destinationDirectory });
return { handled: true };
}
}
}
export async function downloadAndUnpackProjectFromTarGzAsync<TJob extends Job>(
ctx: BuildContext<TJob>,
projectArchiveUrl: string,
destinationDirectory: string
): Promise<void> {
const projectTarball = path.join(ctx.workingdir, 'project.tar.gz');
try {
await downloadFile(projectArchiveUrl, projectTarball, { retry: 3 });
} catch (err: any) {
ctx.reportError?.('Failed to download project archive', err, {
extras: { buildId: ctx.env.EAS_BUILD_ID },
});
throw err;
}
await unpackTarGzAsync({
destination: destinationDirectory,
source: projectTarball,
logger: ctx.logger,
});
}
async function prepareProjectSourcesLocallyAsync<TJob extends Job>(
ctx: BuildContext<TJob>,
projectArchivePath: string,
destinationDirectory: string
): Promise<void> {
const projectTarball = path.join(ctx.workingdir, 'project.tar.gz');
await fs.copyFile(projectArchivePath, projectTarball);
await unpackTarGzAsync({
destination: destinationDirectory,
source: projectTarball,
logger: ctx.logger,
});
}
async function unpackTarGzAsync({
logger,
source,
destination,
}: {
logger: bunyan;
source: string;
destination: string;
}): Promise<void> {
await spawn('tar', ['-C', destination, '--strip-components', '1', '-zxf', source], {
logger,
});
}
function uploadProjectMetadataAsFireAndForget(
ctx: BuildContext<Job>,
{ projectDirectory }: { projectDirectory: string }
): void {
void (async () => {
const uploadResult = await asyncResult(uploadProjectMetadataAsync(ctx, { projectDirectory }));
if (!uploadResult.ok) {
ctx.logger.warn(`Failed to upload project metadata: ${uploadResult.reason}`);
}
})();
}
async function uploadProjectMetadataAsync(
ctx: BuildContext<Job>,
{ projectDirectory }: { projectDirectory: string }
): Promise<void> {
if (!ctx.job.platform) {
// Not a build job, skip.
return;
} else if (
ctx.job.projectArchive.type === ArchiveSourceType.GCS &&
ctx.job.projectArchive.metadataLocation
) {
// Build already has project metadata, skip.
return;
}
const files: string[] = [];
const directoriesToScan: { dir: string; relativePath: string }[] = [
{ dir: projectDirectory, relativePath: '' },
];
while (directoriesToScan.length > 0) {
const { dir, relativePath } = directoriesToScan.shift()!;
if (relativePath === '.git') {
// Do not include whole `.git` directory in the archive, just that it exists.
files.push('.git/...');
continue;
}
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relativeFilePath = path.join(relativePath, entry.name);
if (entry.isDirectory()) {
directoriesToScan.push({ dir: fullPath, relativePath: relativeFilePath });
} else {
files.push(relativeFilePath);
}
}
}
const sortedFiles = files
.map(
// Prepend entries with "project/"
f => path.join('project', f)
)
.sort(); // Sort for consistent ordering
const result = await ctx.graphqlClient
.mutation(
graphql(`
mutation {
uploadSession {
createUploadSession(type: EAS_BUILD_GCS_PROJECT_METADATA)
}
}
`),
{}
)
.toPromise();
if (result.error) {
throw result.error;
}
const uploadSession = result.data!.uploadSession.createUploadSession as {
url: string;
bucketKey: string;
headers: Record<string, string>;
};
await fetch(uploadSession.url, {
method: 'PUT',
body: JSON.stringify({ archiveContent: sortedFiles }),
headers: uploadSession.headers,
});
const updateMetadataResult = await ctx.graphqlClient
.mutation(
graphql(`
mutation UpdateTurtleBuildMetadataMutation(
$buildId: ID!
$projectMetadataFile: ProjectMetadataFileInput!
) {
build {
updateBuildMetadata(
buildId: $buildId
metadata: { projectMetadataFile: $projectMetadataFile }
) {
id
}
}
}
`),
{
buildId: ctx.env.EAS_BUILD_ID,
projectMetadataFile: {
type: 'GCS',
bucketKey: uploadSession.bucketKey,
},
}
)
.toPromise();
if (updateMetadataResult.error) {
throw updateMetadataResult.error;
}
}
async function fetchProjectArchiveSourceAsync(ctx: BuildContext<Job>): Promise<ArchiveSource> {
const taskId = nullthrows(ctx.env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
const expoApiServerURL = nullthrows(ctx.env.__API_SERVER_URL, '__API_SERVER_URL is not set');
const robotAccessToken = nullthrows(
ctx.job.secrets?.robotAccessToken,
'robot access token is not set'
);
const response = await turtleFetch(
new URL(
ctx.job.platform
? `/v2/turtle-builds/${taskId}/download-project-archive`
: `/v2/turtle-job-runs/${taskId}/download-project-archive`,
expoApiServerURL
).toString(),
'POST',
{
headers: {
Authorization: `Bearer ${robotAccessToken}`,
},
timeout: 20000,
retries: 3,
logger: ctx.logger,
}
);
if (!response.ok) {
const textResult = await asyncResult(response.text());
throw new Error(`Unexpected response from server (${response.status}): ${textResult.value}`);
}
const jsonResult = await asyncResult(response.json());
if (!jsonResult.ok) {
throw new Error(
`Expected JSON response from server (${response.status}): ${jsonResult.reason}`
);
}
const dataResult = z.object({ data: ArchiveSourceSchemaZ }).safeParse(jsonResult.value);
if (!dataResult.success) {
throw new Error(
`Unexpected data from server (${response.status}): ${z.prettifyError(dataResult.error)}`
);
}
return dataResult.data.data;
}
+296
View File
@@ -0,0 +1,296 @@
import { ExpoConfig } from '@expo/config';
import {
BuildJob,
BuildPhase,
BuildTrigger,
Ios,
Job,
Platform,
UserError,
} from '@expo/eas-build-job';
import spawn, { SpawnResult } from '@expo/turtle-spawn';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import path from 'path';
import { resolveEnvFromBuildProfileAsync, runEasBuildInternalAsync } from './easBuildInternal';
import { installDependenciesAsync, resolvePackagerDir } from './installDependencies';
import { prepareProjectSourcesAsync } from './projectSources';
import { BuildContext } from '../context';
import { deleteXcodeEnvLocalIfExistsAsync } from '../ios/xcodeEnv';
import { Hook, runHookIfPresent } from '../utils/hooks';
import { setUpNpmrcAsync } from '../utils/npmrc';
import {
getPackageVersionFromPackageJson,
isAtLeastNpm7Async,
shouldUseFrozenLockfile,
} from '../utils/packageManager';
import { getParentAndDescendantProcessPidsAsync } from '../utils/processes';
import { readAndLogPackageJson, readEasJsonContents } from '../utils/project';
import { retryAsync } from '../utils/retry';
const MAX_EXPO_DOCTOR_TIMEOUT_MS = 30 * 1000;
const INSTALL_DEPENDENCIES_WARN_TIMEOUT_MS = 15 * 60 * 1000;
const INSTALL_DEPENDENCIES_KILL_TIMEOUT_MS = 30 * 60 * 1000;
class DoctorTimeoutError extends Error {}
class InstallDependenciesTimeoutError extends Error {}
export async function setupAsync<TJob extends BuildJob>(ctx: BuildContext<TJob>): Promise<void> {
await ctx.runBuildPhase(BuildPhase.PREPARE_PROJECT, async () => {
await retryAsync(
async () => {
await fs.rm(ctx.buildDirectory, { recursive: true, force: true });
await fs.mkdir(ctx.buildDirectory, { recursive: true });
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
},
{
retryOptions: {
retries: 3,
retryIntervalMs: 1_000,
},
}
);
await setUpNpmrcAsync(ctx, ctx.logger);
if (ctx.job.platform === Platform.IOS && ctx.env.EAS_BUILD_RUNNER === 'eas-build') {
await deleteXcodeEnvLocalIfExistsAsync(ctx as BuildContext<Ios.Job>);
}
if (ctx.job.triggeredBy === BuildTrigger.GIT_BASED_INTEGRATION) {
// We need to setup envs from eas.json before
// eas-build-pre-install hook is called.
const env = await resolveEnvFromBuildProfileAsync(ctx, {
cwd: ctx.getReactNativeProjectDirectory(),
});
ctx.updateEnv(env);
}
});
await ctx.runBuildPhase(BuildPhase.PRE_INSTALL_HOOK, async () => {
await runHookIfPresent(ctx, Hook.PRE_INSTALL);
});
await ctx.runBuildPhase(BuildPhase.READ_EAS_JSON, async () => {
try {
const easJsonContents = readEasJsonContents(ctx.getReactNativeProjectDirectory());
ctx.logger.info('Using eas.json:');
ctx.logger.info(easJsonContents);
} catch (err) {
ctx.logger.error({ err }, `Failed to read eas.json.`);
}
});
const packageJson = await ctx.runBuildPhase(BuildPhase.READ_PACKAGE_JSON, async () => {
return readAndLogPackageJson(ctx.logger, ctx.getReactNativeProjectDirectory());
});
await ctx.runBuildPhase(BuildPhase.INSTALL_DEPENDENCIES, async () => {
const expoVersion =
ctx.metadata?.sdkVersion ??
getPackageVersionFromPackageJson({
packageJson,
packageName: 'expo',
});
const reactNativeVersion =
ctx.metadata?.reactNativeVersion ??
getPackageVersionFromPackageJson({
packageJson,
packageName: 'react-native',
});
await runInstallDependenciesAsync(ctx, {
useFrozenLockfile: shouldUseFrozenLockfile({
env: ctx.env,
sdkVersion: expoVersion,
reactNativeVersion,
}),
});
});
await ctx.runBuildPhase(BuildPhase.READ_APP_CONFIG, async () => {
const appConfig = await ctx.appConfig;
ctx.logger.info('Using app configuration:');
ctx.logger.info(JSON.stringify(appConfig, null, 2));
await validateAppConfigAsync(ctx, appConfig);
});
if (ctx.job.triggeredBy === BuildTrigger.GIT_BASED_INTEGRATION) {
await ctx.runBuildPhase(BuildPhase.EAS_BUILD_INTERNAL, async () => {
const appConfig = await ctx.appConfig;
if (!appConfig.ios?.bundleIdentifier && ctx.job.platform === Platform.IOS) {
throw new Error(
'The "ios.bundleIdentifier" is required to be set in app config for builds triggered by GitHub integration. Learn more: https://docs.expo.dev/versions/latest/config/app/#bundleidentifier.'
);
}
if (!appConfig.android?.package && ctx.job.platform === Platform.ANDROID) {
throw new Error(
'The "android.package" is required to be set in app config for builds triggered by GitHub integration. Learn more: https://docs.expo.dev/versions/latest/config/app/#package.'
);
}
const { newJob, newMetadata } = await runEasBuildInternalAsync({
job: ctx.job,
env: ctx.env,
logger: ctx.logger,
cwd: ctx.getReactNativeProjectDirectory(),
projectRootOverride: ctx.env.EAS_NO_VCS ? ctx.buildDirectory : undefined,
});
ctx.updateJobInformation(newJob, newMetadata);
});
}
const hasExpoPackage = !!packageJson.dependencies?.expo;
if (!ctx.env.EAS_BUILD_DISABLE_EXPO_DOCTOR_STEP && hasExpoPackage) {
await ctx.runBuildPhase(BuildPhase.RUN_EXPO_DOCTOR, async () => {
try {
await runExpoDoctor(ctx);
} catch (err) {
if (err instanceof DoctorTimeoutError) {
ctx.logger.error(err.message);
} else {
ctx.logger.error({ err }, 'Command "expo doctor" failed.');
}
ctx.markBuildPhaseHasWarnings();
}
});
}
}
async function runExpoDoctor<TJob extends Job>(ctx: BuildContext<TJob>): Promise<SpawnResult> {
ctx.logger.info('Running "expo doctor"');
let timeout: NodeJS.Timeout | undefined;
let timedOut = false;
const isAtLeastNpm7 = await isAtLeastNpm7Async();
try {
const argsPrefix = isAtLeastNpm7 ? ['-y'] : [];
const promise = spawn('npx', [...argsPrefix, 'expo-doctor'], {
cwd: ctx.getReactNativeProjectDirectory(),
logger: ctx.logger,
env: ctx.env,
});
timeout = setTimeout(async () => {
timedOut = true;
const ppid = nullthrows(promise.child.pid);
const pids = await getParentAndDescendantProcessPidsAsync(ppid);
pids.forEach(pid => {
process.kill(pid);
});
ctx.reportError?.(`"expo doctor" timed out`, undefined, {
extras: { buildId: ctx.env.EAS_BUILD_ID },
});
}, MAX_EXPO_DOCTOR_TIMEOUT_MS);
return await promise;
} catch (err: any) {
if (timedOut) {
throw new DoctorTimeoutError('"expo doctor" timed out, skipping...');
}
throw err;
} finally {
if (timeout) {
clearTimeout(timeout);
}
}
}
async function runInstallDependenciesAsync<TJob extends Job>(
ctx: BuildContext<TJob>,
{
useFrozenLockfile,
}: {
useFrozenLockfile: boolean;
}
): Promise<void> {
let warnTimeout: NodeJS.Timeout | undefined;
let killTimeout: NodeJS.Timeout | undefined;
let killTimedOut: boolean = false;
try {
const installDependenciesSpawnPromise = (
await installDependenciesAsync({
packageManager: ctx.packageManager,
env: ctx.env,
logger: ctx.logger,
infoCallbackFn: () => {
if (warnTimeout) {
warnTimeout.refresh();
}
if (killTimeout) {
killTimeout.refresh();
}
},
cwd: resolvePackagerDir(ctx),
useFrozenLockfile,
})
).spawnPromise;
warnTimeout = setTimeout(() => {
ctx.logger.warn(
'"Install dependencies" phase takes longer then expected and it did not produce any logs in the past 15 minutes. Consider evaluating your package.json file for possible issues with dependencies'
);
}, INSTALL_DEPENDENCIES_WARN_TIMEOUT_MS);
killTimeout = setTimeout(async () => {
killTimedOut = true;
ctx.logger.error(
'"Install dependencies" phase takes a very long time and it did not produce any logs in the past 30 minutes. Most likely an unexpected error happened with your dependencies which caused the process to hang and it will be terminated'
);
const ppid = nullthrows(installDependenciesSpawnPromise.child.pid);
const pids = await getParentAndDescendantProcessPidsAsync(ppid);
pids.forEach(pid => {
process.kill(pid);
});
ctx.reportError?.('"Install dependencies" phase takes a very long time', undefined, {
extras: { buildId: ctx.env.EAS_BUILD_ID },
});
}, INSTALL_DEPENDENCIES_KILL_TIMEOUT_MS);
await installDependenciesSpawnPromise;
} catch (err: any) {
if (killTimedOut) {
throw new InstallDependenciesTimeoutError(
'"Install dependencies" phase was inactive for over 30 minutes. Please evaluate your package.json file'
);
}
throw err;
} finally {
if (warnTimeout) {
clearTimeout(warnTimeout);
}
if (killTimeout) {
clearTimeout(killTimeout);
}
}
}
async function validateAppConfigAsync(
ctx: BuildContext<Job>,
appConfig: ExpoConfig
): Promise<void> {
if (
appConfig?.extra?.eas?.projectId &&
ctx.env.EAS_BUILD_PROJECT_ID &&
appConfig.extra.eas.projectId !== ctx.env.EAS_BUILD_PROJECT_ID
) {
const isUsingDynamicConfig =
(await fs.pathExists(path.join(ctx.getReactNativeProjectDirectory(), 'app.config.ts'))) ||
(await fs.pathExists(path.join(ctx.getReactNativeProjectDirectory(), 'app.config.js')));
const isGitHubBuild = ctx.job.triggeredBy === BuildTrigger.GIT_BASED_INTEGRATION;
let extraMessage = '';
if (isGitHubBuild && isUsingDynamicConfig) {
extraMessage =
'Make sure you connected your GitHub repository to the correct Expo project and if you are using environment variables to switch between projects in app.config.js/app.config.ts remember to set those variables in eas.json too. ';
} else if (isGitHubBuild) {
extraMessage = 'Make sure you connected your GitHub repository to the correct Expo project. ';
} else if (isUsingDynamicConfig) {
extraMessage =
'If you are using environment variables to switch between projects in app.config.js/app.config.ts, make sure those variables are also set inside EAS Build. You can do that using "env" field in eas.json or EAS environment variables. ';
}
throw new UserError(
'EAS_BUILD_PROJECT_ID_MISMATCH',
`The value of the "extra.eas.projectId" field (${appConfig.extra.eas.projectId}) in the app config does not match the current project id (${ctx.env.EAS_BUILD_PROJECT_ID}). ${extraMessage}Learn more: https://expo.fyi/eas-config-mismatch.`
);
} else if (ctx.env.EAS_BUILD_PROJECT_ID && !appConfig?.extra?.eas?.projectId) {
ctx.logger.error(`The "extra.eas.projectId" field is missing from your app config.`);
ctx.markBuildPhaseHasWarnings();
}
}
+423
View File
@@ -0,0 +1,423 @@
import { ExpoConfig } from '@expo/config';
import {
BuildPhase,
BuildPhaseResult,
BuildPhaseStats,
Env,
EnvironmentSecretType,
GenericArtifactType,
Job,
LogMarker,
ManagedArtifactType,
Metadata,
errors,
isGenericArtifact,
} from '@expo/eas-build-job';
import { BuildTrigger } from '@expo/eas-build-job/dist/common';
import { bunyan } from '@expo/logger';
import { Client, fetchExchange } from '@urql/core';
import fs from 'fs-extra';
import path from 'path';
import { resolveBuildPhaseErrorAsync } from './buildErrors/detectError';
import { Datadog } from './datadog';
import { readAppConfig } from './utils/appConfig';
import { createTemporaryEnvironmentSecretFile } from './utils/environmentSecrets';
import { PackageManager, resolvePackageManager } from './utils/packageManager';
export type Artifacts = Partial<Record<ManagedArtifactType, string>>;
export interface CacheManager {
saveCache(ctx: BuildContext<Job>): Promise<void>;
restoreCache(ctx: BuildContext<Job>): Promise<void>;
}
export interface LogBuffer {
getLogs(): string[];
getPhaseLogs(buildPhase: string): string[];
}
export type ArtifactToUpload =
| {
type: ManagedArtifactType;
paths: string[];
}
| {
type: GenericArtifactType;
name: string;
paths: string[];
};
export interface BuildContextOptions {
workingdir: string;
logger: bunyan;
logBuffer: LogBuffer;
env: Env;
cacheManager?: CacheManager;
uploadArtifact: (spec: { artifact: ArtifactToUpload; logger: bunyan }) => Promise<
/** Workflow Artifact ID */
| { artifactId: string | null; filename?: never }
/** This remains from the time we relied on Launcher to rename the GCS object. */
| { artifactId?: never; filename: string | null }
>;
reportError?: (
msg: string,
err?: Error,
options?: { tags?: Record<string, string>; extras?: Record<string, string> }
) => void;
reportBuildPhaseStats?: (stats: BuildPhaseStats) => void;
skipNativeBuild?: boolean;
metadata?: Metadata;
expoApiV2BaseUrl?: string;
}
export class SkipNativeBuildError extends Error {}
export class BuildContext<TJob extends Job = Job> {
public readonly workingdir: string;
public logger: bunyan;
public readonly logBuffer: LogBuffer;
public readonly cacheManager?: CacheManager;
public readonly reportError?: (
msg: string,
err?: Error,
options?: { tags?: Record<string, string>; extras?: Record<string, string> }
) => void;
public readonly skipNativeBuild?: boolean;
public readonly expoApiV2BaseUrl?: string;
public artifacts: Artifacts = {};
private readonly _isLocal: boolean;
private _env: Env;
private _job: TJob;
private _metadata?: Metadata;
private readonly defaultLogger: bunyan;
private readonly _uploadArtifact: BuildContextOptions['uploadArtifact'];
private buildPhase?: BuildPhase;
private buildPhaseSkipped = false;
private buildPhaseHasWarnings = false;
private _appConfig?: Promise<ExpoConfig>;
private readonly reportBuildPhaseStats?: (stats: BuildPhaseStats) => void;
public readonly graphqlClient: Client;
constructor(job: TJob, options: BuildContextOptions) {
this.workingdir = options.workingdir;
this.defaultLogger = options.logger;
this.logger = this.defaultLogger;
this.logBuffer = options.logBuffer;
this.cacheManager = options.cacheManager;
this._uploadArtifact = options.uploadArtifact;
this.reportError = options.reportError;
this._job = job;
this._metadata = options.metadata;
this.skipNativeBuild = options.skipNativeBuild;
this.expoApiV2BaseUrl = options.expoApiV2BaseUrl;
this.reportBuildPhaseStats = options.reportBuildPhaseStats;
const environmentSecrets = this.getEnvironmentSecrets(job);
this._env = {
...options.env,
...job?.builderEnvironment?.env,
...environmentSecrets,
__EAS_BUILD_ENVS_DIR: this.buildEnvsDirectory,
};
this._env.PATH = this._env.PATH
? [this.buildExecutablesDirectory, this._env.PATH].join(':')
: this.buildExecutablesDirectory;
this._isLocal = this._env.EAS_BUILD_RUNNER !== 'eas-build';
this.graphqlClient = new Client({
url: new URL('graphql', this.env.__API_SERVER_URL).toString(),
exchanges: [fetchExchange],
preferGetMethod: false,
fetchOptions: {
headers: {
Authorization: `Bearer ${job.secrets?.robotAccessToken}`,
},
},
});
}
public get job(): TJob {
return this._job;
}
public get metadata(): Metadata | undefined {
return this._metadata;
}
public get env(): Env {
return this._env;
}
public get buildDirectory(): string {
return path.join(this.workingdir, 'build');
}
public get buildLogsDirectory(): string {
return path.join(this.workingdir, 'logs');
}
public get isLocal(): boolean {
return this._isLocal;
}
/**
* Directory used to store executables used during regular (non-custom) builds.
*/
public get buildExecutablesDirectory(): string {
return path.join(this.workingdir, 'bin');
}
/**
* Directory used to store env variables registered in the current build step.
* All values stored here will be available in the next build phase as env variables.
*/
public get buildEnvsDirectory(): string {
return path.join(this.workingdir, 'env');
}
public get packageManager(): PackageManager {
return resolvePackageManager(this.getReactNativeProjectDirectory(), {
env: this.env,
});
}
public get appConfig(): Promise<ExpoConfig> {
if (!this._appConfig) {
this._appConfig = readAppConfig({
projectDir: this.getReactNativeProjectDirectory(),
env: this.env,
logger: this.logger,
sdkVersion: this.metadata?.sdkVersion,
}).then(config => config.exp);
}
return this._appConfig;
}
public async runBuildPhase<T>(
buildPhase: BuildPhase,
phase: () => Promise<T>,
{
doNotMarkStart = false,
doNotMarkEnd = false,
}: {
doNotMarkStart?: boolean;
doNotMarkEnd?: boolean;
} = {}
): Promise<T> {
let startTimestamp = Date.now();
try {
this.setBuildPhase(buildPhase, { doNotMarkStart });
startTimestamp = Date.now();
const result = await phase();
const durationMs = Date.now() - startTimestamp;
const buildPhaseResult: BuildPhaseResult = this.buildPhaseSkipped
? BuildPhaseResult.SKIPPED
: this.buildPhaseHasWarnings
? BuildPhaseResult.WARNING
: BuildPhaseResult.SUCCESS;
await this.endCurrentBuildPhaseAsync({ result: buildPhaseResult, doNotMarkEnd, durationMs });
return result;
} catch (err: any) {
const durationMs = Date.now() - startTimestamp;
const resolvedError = await this.handleBuildPhaseErrorAsync(err, buildPhase);
await this.endCurrentBuildPhaseAsync({ result: BuildPhaseResult.FAIL, durationMs });
throw resolvedError;
}
}
public markBuildPhaseSkipped(): void {
this.buildPhaseSkipped = true;
}
public markBuildPhaseHasWarnings(): void {
this.buildPhaseHasWarnings = true;
}
public async uploadArtifact({
artifact,
logger,
}: {
artifact: ArtifactToUpload;
logger: bunyan;
}): Promise<{ artifactId: string | null }> {
const result = await this._uploadArtifact({ artifact, logger });
if (result.filename && !isGenericArtifact(artifact)) {
this.artifacts[artifact.type] = result.filename;
}
return { artifactId: result.artifactId ?? null };
}
public updateEnv(env: Env): void {
if (this._job.triggeredBy !== BuildTrigger.GIT_BASED_INTEGRATION) {
throw new Error(
'Updating environment variables is only allowed when build was triggered by a git-based integration.'
);
}
this._env = {
...env,
...this._env,
__EAS_BUILD_ENVS_DIR: this.buildEnvsDirectory,
};
this._env.PATH = this._env.PATH
? [this.buildExecutablesDirectory, this._env.PATH].join(':')
: this.buildExecutablesDirectory;
}
public updateJobInformation(job: TJob, metadata: Metadata): void {
if (this._job.triggeredBy !== BuildTrigger.GIT_BASED_INTEGRATION) {
throw new Error(
'Updating job information is only allowed when build was triggered by a git-based integration.'
);
}
this._job = {
...this._job,
...job,
workflowInterpolationContext:
job.workflowInterpolationContext ?? this.job.workflowInterpolationContext,
triggeredBy: this._job.triggeredBy,
secrets: {
...this.job.secrets,
...job.secrets,
robotAccessToken: job.secrets?.robotAccessToken ?? this.job.secrets?.robotAccessToken,
environmentSecrets: [
// Latter secrets override former ones.
...(this.job.secrets?.environmentSecrets ?? []),
...(job.secrets?.environmentSecrets ?? []),
],
},
...(this._job.platform ? { expoBuildUrl: this._job.expoBuildUrl } : null),
};
this._metadata = metadata;
}
private async handleBuildPhaseErrorAsync(
err: any,
buildPhase: BuildPhase
): Promise<errors.ExpoError> {
const buildError = await resolveBuildPhaseErrorAsync(
err,
this.logBuffer.getPhaseLogs(buildPhase),
{
job: this.job,
phase: buildPhase,
env: this.env,
},
this.buildLogsDirectory
);
if (buildError.errorCode === errors.ErrorCode.UNKNOWN_ERROR) {
// leaving message empty, website will display err.stack which already includes err.message
this.logger.error({ err }, '');
} else {
this.logger.error(`Error: ${buildError.message}`);
}
return buildError;
}
public getReactNativeProjectDirectory(baseDirectory = this.buildDirectory): string {
if (!this.job.platform) {
return path.join(
baseDirectory,
// NOTE: We may want to add projectRootDirectory to generic jobs in the future.
this.job.builderEnvironment.env.__EXPO_RELATIVE_BASE_DIRECTORY || '.'
);
}
return path.join(baseDirectory, this.job.projectRootDirectory ?? '.');
}
private setBuildPhase(buildPhase: BuildPhase, { doNotMarkStart = false } = {}): void {
if (this.buildPhase) {
if (this.buildPhase === buildPhase) {
return;
} else {
this.logger.info(
{ marker: LogMarker.END_PHASE, result: BuildPhaseResult.UNKNOWN },
`End phase: ${this.buildPhase}`
);
this.logger = this.defaultLogger;
}
}
this.buildPhase = buildPhase;
this.logger = this.defaultLogger.child({ phase: buildPhase });
if (!doNotMarkStart) {
this.logger.info({ marker: LogMarker.START_PHASE }, `Start phase: ${this.buildPhase}`);
}
}
private async endCurrentBuildPhaseAsync({
result,
doNotMarkEnd = false,
durationMs,
}: {
result: BuildPhaseResult;
doNotMarkEnd?: boolean;
durationMs: number;
}): Promise<void> {
if (!this.buildPhase) {
return;
}
await this.collectAndUpdateEnvVariablesAsync();
this.reportBuildPhaseStats?.({ buildPhase: this.buildPhase, result, durationMs });
if (this.job.platform) {
Datadog.distribution('eas.build.phase_duration', durationMs, {
build_phase: this.buildPhase.toLowerCase(),
platform: this.job.platform,
result,
});
}
if (!doNotMarkEnd) {
this.logger.info(
{ marker: LogMarker.END_PHASE, result, durationMs },
`End phase: ${this.buildPhase}`
);
}
this.logger = this.defaultLogger;
this.buildPhase = undefined;
this.buildPhaseSkipped = false;
this.buildPhaseHasWarnings = false;
}
private async collectAndUpdateEnvVariablesAsync(): Promise<void> {
const filenames = await fs.readdir(this.buildEnvsDirectory);
const entries = await Promise.all(
filenames.map(async basename => {
const rawContents = await fs.readFile(
path.join(this.buildEnvsDirectory, basename),
'utf-8'
);
return [basename, rawContents];
})
);
await Promise.all(
filenames.map(async basename => {
await fs.remove(path.join(this.buildEnvsDirectory, basename));
})
);
this._env = {
...this._env,
...Object.fromEntries(entries),
};
}
private getEnvironmentSecrets(job: TJob): Record<string, string> {
if (!job?.secrets?.environmentSecrets) {
return {};
}
const environmentSecretsDirectory = path.join(this.workingdir, 'eas-environment-secrets');
const environmentSecrets: Record<string, string> = {};
for (const { name, type, value } of job.secrets.environmentSecrets) {
if (type === EnvironmentSecretType.STRING) {
environmentSecrets[name] = value;
} else {
environmentSecrets[name] = createTemporaryEnvironmentSecretFile({
secretsDir: environmentSecretsDirectory,
name,
contents_base64: value,
});
}
}
return environmentSecrets;
}
}
@@ -0,0 +1,171 @@
import {
BuildJob,
BuildPhase,
BuildTrigger,
Env,
Job,
Metadata,
Platform,
StaticJobInterpolationContext,
} from '@expo/eas-build-job';
import { bunyan } from '@expo/logger';
import { BuildRuntimePlatform, ExternalBuildContextProvider, StepMetric } from '@expo/steps';
import { Client } from '@urql/core';
import assert from 'assert';
import path from 'path';
import { ArtifactToUpload, BuildContext } from './context';
import { uploadStepMetricsToWwwAsync } from './utils/stepMetrics';
const platformToBuildRuntimePlatform: Record<Platform, BuildRuntimePlatform> = {
[Platform.ANDROID]: BuildRuntimePlatform.LINUX,
[Platform.IOS]: BuildRuntimePlatform.DARWIN,
};
export interface BuilderRuntimeApi {
uploadArtifact: (spec: { artifact: ArtifactToUpload; logger: bunyan }) => Promise<{
artifactId: string | null;
}>;
}
export class CustomBuildContext<TJob extends Job = Job> implements ExternalBuildContextProvider {
/*
* Directory that contains project sources before eas/checkout.
*/
public readonly projectSourceDirectory: string;
/*
* Directory where build is executed. eas/checkout will copy sources here.
*/
public readonly projectTargetDirectory: string;
/*
* Directory where all build steps will be executed unless configured otherwise.
*/
public readonly defaultWorkingDirectory: string;
/*
* Directory where build logs will be stored unless configure otherwise.
*/
public readonly buildLogsDirectory: string;
/**
* Time of the start of the build.
*/
public readonly startTime: Date;
public readonly logger: bunyan;
public readonly graphqlClient: Client;
public readonly runtimeApi: BuilderRuntimeApi;
public job: TJob;
public metadata?: Metadata;
private _env: Env;
private readonly expoApiV2BaseUrl?: string;
private readonly pendingMetricUploads: Promise<void>[] = [];
constructor(buildCtx: BuildContext<TJob>) {
this._env = buildCtx.env;
this.job = buildCtx.job;
this.metadata = buildCtx.metadata;
this.logger = buildCtx.logger.child({ phase: BuildPhase.CUSTOM });
this.graphqlClient = buildCtx.graphqlClient;
this.projectSourceDirectory = path.join(buildCtx.workingdir, 'temporary-custom-build');
this.projectTargetDirectory = path.join(buildCtx.workingdir, 'build');
this.defaultWorkingDirectory = buildCtx.getReactNativeProjectDirectory();
this.buildLogsDirectory = path.join(buildCtx.workingdir, 'logs');
this.runtimeApi = {
uploadArtifact: (...args) => buildCtx['uploadArtifact'](...args),
};
this.expoApiV2BaseUrl = buildCtx.expoApiV2BaseUrl;
this.startTime = new Date();
}
public hasBuildJob(): this is CustomBuildContext<BuildJob> {
return Boolean(this.job.platform);
}
public get runtimePlatform(): BuildRuntimePlatform {
// Generic jobs are not per-platform.
if (!this.job.platform) {
assert(
process.platform === 'linux' || process.platform === 'darwin',
`Invalid platform, expected linux or darwin, got: ${process.platform}`
);
return {
linux: BuildRuntimePlatform.LINUX,
darwin: BuildRuntimePlatform.DARWIN,
}[process.platform];
}
return platformToBuildRuntimePlatform[this.job.platform];
}
public get env(): Env {
return this._env;
}
// We omit steps, because CustomBuildContext does not have steps.
public staticContext(): Omit<StaticJobInterpolationContext, 'steps'> {
return {
...this.job.workflowInterpolationContext,
expoApiServerURL: this.env.__API_SERVER_URL,
job: this.job,
metadata: this.metadata ?? null,
};
}
public updateEnv(env: Env): void {
this._env = env;
}
public updateJobInformation(job: TJob, metadata: Metadata): void {
if (this.job.triggeredBy !== BuildTrigger.GIT_BASED_INTEGRATION) {
throw new Error(
'Updating job information is only allowed when build was triggered by a git-based integration.'
);
}
this.job = {
...this.job,
...job,
workflowInterpolationContext:
job.workflowInterpolationContext ?? this.job.workflowInterpolationContext,
triggeredBy: this.job.triggeredBy,
secrets: {
...this.job.secrets,
...job.secrets,
robotAccessToken: job.secrets?.robotAccessToken ?? this.job.secrets?.robotAccessToken,
environmentSecrets: [
// Latter secrets override former ones.
...(this.job.secrets?.environmentSecrets ?? []),
...(job.secrets?.environmentSecrets ?? []),
],
},
...(this.job.platform ? { expoBuildUrl: this.job.expoBuildUrl } : null),
};
this.metadata = metadata;
}
public reportStepMetric(metric: StepMetric): void {
const workflowJobId = this._env.__WORKFLOW_JOB_ID;
const robotAccessToken = this.job.secrets?.robotAccessToken;
if (!workflowJobId || !robotAccessToken || !this.expoApiV2BaseUrl) {
return;
}
const p = uploadStepMetricsToWwwAsync({
workflowJobId,
robotAccessToken,
expoApiV2BaseUrl: this.expoApiV2BaseUrl,
stepMetrics: [metric],
logger: this.logger,
});
this.pendingMetricUploads.push(p);
}
public async drainPendingMetricUploads(): Promise<void> {
await Promise.allSettled(this.pendingMetricUploads);
}
}
+54
View File
@@ -0,0 +1,54 @@
import { Sentry } from './sentry';
import { turtleFetch } from './utils/turtleFetch';
type DatadogSetupOptions = {
expoApiV2BaseUrl: string;
turtleBuildId: string;
robotAccessToken: string;
};
let setupOptions: DatadogSetupOptions | null = null;
let pendingMetricUploads: Promise<unknown>[] = [];
export const Datadog = {
setup(opts: DatadogSetupOptions | null): void {
setupOptions = opts;
},
distribution(name: string, value: number, tags: Record<string, string> = {}): void {
if (!setupOptions) {
return;
}
const { expoApiV2BaseUrl, turtleBuildId, robotAccessToken } = setupOptions;
const metrics = [
{
name,
type: 'distribution' as const,
value,
tags,
},
];
const uploadPromise = turtleFetch(
new URL(`turtle-builds/${turtleBuildId}/metrics`, expoApiV2BaseUrl).toString(),
'POST',
{
json: { metrics },
headers: {
Authorization: `Bearer ${robotAccessToken}`,
},
retries: 2,
}
).catch(err => {
Sentry.capture('Failed to report turtle build metric', err, {
extras: { metrics },
});
});
pendingMetricUploads.push(uploadPromise);
},
async flushAsync(): Promise<void> {
await Promise.allSettled(pendingMetricUploads);
},
};
@@ -0,0 +1,237 @@
import { bunyan } from '@expo/logger';
import assert from 'assert';
import { randomUUID } from 'crypto';
import fs from 'fs-extra';
import os from 'os';
import path from 'path';
import { Readable, Writable, pipeline } from 'stream';
import { promisify } from 'util';
import zlib from 'zlib';
import GCS from './client';
type PromiseResolveFn = (value?: void | PromiseLike<void> | undefined) => void;
const pipe = promisify(pipeline);
class GCSLoggerStream extends Writable {
public writable = true;
private readonly logger: bunyan;
private readonly uploadMethod?: GCSLoggerStream.UploadMethod;
private readonly options: GCSLoggerStream.Options;
private readonly temporaryLogsPath: string;
private readonly temporaryCompressedLogsPath: string;
private readonly compress: string | null;
private fileHandle?: number;
private uploadingPromise?: Promise<void>;
private hasChangesToUpload = false;
private flushInterval?: NodeJS.Timeout;
private buffer: string[] = [];
private writePromise?: Promise<any>;
private cleanUpCalled: boolean = false;
constructor({ logger, uploadMethod, options }: GCSLoggerStream.Config) {
super();
this.logger = logger;
this.uploadMethod = uploadMethod;
this.options = options;
this.compress = options?.compress ?? this.findNormalizedHeader('contentencoding');
this.temporaryLogsPath = path.join(os.tmpdir(), `logs-${randomUUID()}`);
this.temporaryCompressedLogsPath = `${this.temporaryLogsPath}.compressed`;
}
private findNormalizedHeader(name: string): string | null {
if (!this.uploadMethod || 'client' in this.uploadMethod) {
return null;
}
const normalizedName = name.toLowerCase().replace('-', '');
const fields = this.uploadMethod.signedUrl.headers;
for (const key in fields) {
if (key.toLowerCase().replace('-', '') === normalizedName) {
return fields[key];
}
}
return null;
}
public async init(): Promise<string> {
this.fileHandle = await fs.open(this.temporaryLogsPath, 'w+', 0o660);
this.flushInterval = setInterval(() => this.flush(), this.options.uploadIntervalMs);
return (await this.flush(true)) as string;
}
public async cleanUp(): Promise<void> {
if (this.cleanUpCalled) {
this.logger.info('Cleanup already called');
return;
}
this.cleanUpCalled = true;
if (!this.fileHandle) {
throw new Error('You have to init the stream first!');
}
if (this.flushInterval) {
clearInterval(this.flushInterval);
this.flushInterval = undefined;
}
await this.safeWriteToFile(true);
await fs.close(this.fileHandle);
this.fileHandle = undefined;
await this.flush(true);
await fs.remove(this.temporaryLogsPath);
await fs.remove(this.temporaryCompressedLogsPath);
this.logger.info('Cleaning up GCS log stream');
}
public write(rec: any): boolean {
if (!this.fileHandle) {
return true;
}
const logLine = `${JSON.stringify(rec)}\n`;
this.buffer.push(logLine);
void this.safeWriteToFile();
return true;
}
private async safeWriteToFile(force = false): Promise<void> {
if (this.writePromise && force) {
await this.writePromise;
await this.safeWriteToFile(force);
return;
}
if (!this.fileHandle || Boolean(this.writePromise) || this.buffer.length === 0) {
return;
}
const buffer = this.buffer.slice();
try {
this.writePromise = fs.write(this.fileHandle, buffer.join(''));
await this.writePromise;
this.buffer = this.buffer.slice(buffer.length);
this.hasChangesToUpload = true;
} catch (err) {
this.logger.error({ err, origin: 'gcs-logger' }, 'Failed to write logs to file');
} finally {
this.writePromise = undefined;
}
}
private async flush(force = false): Promise<string | void> {
if (force || this.hasChangesToUpload) {
if (this.uploadingPromise) {
await this.uploadingPromise;
return await this.flush(force);
}
return await this.flushInternal();
}
}
private async flushInternal(): Promise<string | void> {
await this.safeWriteToFile();
let resolveFn: PromiseResolveFn;
this.hasChangesToUpload = false;
this.uploadingPromise = new Promise(res => {
resolveFn = res;
});
return await this.upload()
.then(result => {
return result;
})
.catch(err => {
this.logger.error({ err }, 'Failed to upload logs file to GCS');
})
.then(result => {
this.uploadingPromise = undefined;
resolveFn();
return result;
});
}
private async upload(): Promise<string | void> {
if (!this.uploadMethod) {
return;
}
const { size } = await fs.stat(this.temporaryLogsPath);
const srcGeneratorAsync = async (): Promise<Readable> => {
return await this.createCompressedStream(
fs.createReadStream(this.temporaryLogsPath, { end: size })
);
};
if ('signedUrl' in this.uploadMethod) {
return await GCS.uploadWithSignedUrl({
signedUrl: this.uploadMethod.signedUrl,
srcGeneratorAsync,
});
}
const { Location } = await this.uploadMethod.client.uploadFile({
key: this.uploadMethod.key,
src: await srcGeneratorAsync(),
streamOptions: {
metadata: {
contentType: 'text/plain;charset=utf-8',
...(this.compress !== null ? { contentEncoding: this.compress } : {}),
customTime: this.uploadMethod.customTime,
},
},
});
return Location;
}
private async createCompressedStream(src: Readable): Promise<Readable> {
if (!this.compress) {
return src;
}
const dst = fs.createWriteStream(this.temporaryCompressedLogsPath);
const encoder =
this.compress === 'gzip'
? zlib.createGzip()
: this.compress === 'br'
? zlib.createBrotliCompress({
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: 0,
},
})
: null;
assert(encoder, `unknown encoder ${encoder}`);
await pipe(src, encoder, dst);
const { size } = await fs.stat(this.temporaryCompressedLogsPath);
return fs.createReadStream(this.temporaryCompressedLogsPath, { end: size });
}
}
namespace GCSLoggerStream {
export enum CompressionMethod {
GZIP = 'gzip',
BR = 'br',
}
export interface Options {
uploadIntervalMs: number;
compress?: CompressionMethod;
}
export type UploadMethod =
| { client: GCS; key: string; customTime: Date | null }
| { signedUrl: GCS.SignedUrl };
export interface Config {
logger: bunyan;
uploadMethod?: UploadMethod;
options: Options;
}
}
export default GCSLoggerStream;
Loaded 100 of 1091 files, more files were not shown because too many files have changed in this diff. Show more