Compare commits

..
23 Commits
Author SHA1 Message Date
Expo CI e9ebc41413 v21.0.2 2026-07-17 06:00:55 +00:00
9313ef11ca [eas-cli] Show accurate build overage count in pre-build usage warning (#3932)
* [eas-cli] Show accurate build overage count in pre-build usage warning

The overage count shown to paid users ("N builds beyond your included
credits") summed the value of every overageMetrics row, including rows
without build metadata (e.g. rollup/summary rows), which could inflate the
number. Filter to genuine per-worker build overage rows (those carrying
billingResourceClass + platform metadata) before summing.

Fetch that metadata on overageMetrics in the warning query so the rows can
be distinguished.

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

* [eas-cli] Fix changelog PR reference for build overage warning entry

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

---------

Co-authored-by: Sarah Lane <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-16 16:43:31 -06:00
Stanisław Chmiela fd80532a7e [build-tools] Remove serve-sim stream URL output (#4018)
I found it sometimes isn't printed.

---

## Summary

- Stop waiting for `serve-sim` to emit a `Stream:` URL before reporting startup success.
- Upload only `previewUrl` in the serve-sim remote config payload.
- Remove `streamUrl` from the simulator session query selection and generated query result type.
- Simplify the serve-sim URL parser to match only the tunnel URL.

## Why

The stream URL is no longer part of the expected serve-sim output path. Keeping it in the startup contract made the build step wait for and upload a value that is not needed by the CLI display flow.

## Test plan

- `yarn --cwd packages/build-tools typecheck`
- `yarn --cwd packages/eas-cli typecheck`
- `yarn --cwd packages/build-tools jest-unit src/steps/utils/__tests__/remoteDeviceRunSession.test.ts`
- `yarn --cwd packages/eas-cli test src/commands/simulator/__tests__/get.test.ts src/commands/simulator/__tests__/start.test.ts --runInBand`
- `git diff --check`
2026-07-16 15:03:37 +02:00
Stanisław Chmiela f20ccebcad [build-tools] Clean up GCS logging and retry handling (#4026)
Remove unused functions from `GCS.Client`. Removes big `@google-cloud/storage` dependency!
2026-07-16 14:55:20 +02:00
Stanisław Chmiela e8602f7062 [build-tools] Use unique simulator recording artifact names (#4020)
## Summary

- include a shortened simulator UDID and first-frame timestamp in each screen recording artifact name
- format the timestamp as a readable, stable UTC value
- add `__eas_type: "screen-recording"` metadata for typed artifact detection
- cover recordings from a rebooted simulator and simultaneous same-model simulators

Artifact names now use this format:

```
iPhone 16 screen recording (01234567-…, started at Jul 10, 2026, 10:00:00.000 UTC)
```

## Why

A simulator can reboot during one device run session, producing multiple recordings with the same device name and UDID. A session can also contain two same-model simulators whose recordings start at precisely the same time. Device name alone, or device name plus timestamp alone, therefore does not reliably distinguish every recording.

The first eight UDID characters distinguish same-model simulators without putting the full UUID in the user-facing name. The millisecond first-frame timestamp distinguishes subsequent recordings after a reboot. The full UDID and canonical ISO timestamp remain in artifact metadata for exact identity, parsing, and ordering.

The new `__eas_type` marker provides a descriptive discriminator for screen recordings. The existing `__eas_screen_recording: "1"` marker remains temporarily so currently deployed consumers continue recognizing these artifacts during migration.

The artifact metadata also provides `udid`, `deviceName`, `runtimeDisplayName`, `firstFrameAt`, and encoded `width` and `height`.

## Validation

- focused screen recording upload tests covering rebooted and simultaneous same-model simulators
- build-tools TypeScript typecheck
2026-07-16 14:09:51 +02:00
Ash Wu d14c0b196b [eas-build-job] internal refactor general hook (#4023)
Signed-off-by: Ash Wu <[email protected]>
2026-07-16 19:59:08 +08:00
Stanisław Chmiela c24b9a037a [codemention] Limit default notifications to eas-cli (#4024) 2026-07-16 03:43:53 -07:00
Stanisław Chmiela c3c3f087ac [worker] Increase Node heap on capable workers (#4011)
## Summary

- set `NODE_OPTIONS=--max-old-space-size=3072` for build child processes when the worker has at least 6 GiB of constrained or physical memory and its effective V8 heap limit is below 3 GiB
- preserve existing worker `NODE_OPTIONS`; project-provided environment values retain their later override position
- leave workers with less memory or an existing heap limit of at least 3 GiB unchanged

## Why

V8 defaults to a roughly 2 GiB heap on our smaller build workers. Dependency installation and Expo export can exhaust that heap even though the VM still has enough memory. Raising the ceiling to 3 GiB gives those child processes additional headroom without preallocating memory or changing larger workers that already receive a higher V8 default.

## Validation

- `yarn build`
- `yarn workspace @expo/worker test:unit --runInBand src/__unit__/env.test.ts`
- `yarn workspace @expo/worker typecheck`
- focused Oxlint and Oxfmt checks
2026-07-16 12:25:44 +02:00
Stanisław Chmiela ac761edb5c Upgrade Lodash and GraphQL Code Generator (#4009)
# Why

Supersedes #3968.

Dependabot alert 354 requires `lodash` and `lodash-es` 4.18.1. The original PR upgraded the worker's direct dependency and enforced the patched transitive version with a root resolution. That resolution is no longer needed: this PR upgrades the EAS-owned declarations and the GraphQL Code Generator packages that brought in the remaining restrictive `lodash ~4.17.0` dependency.

# How

- Upgrade direct Lodash declarations in build-tools, local-build-plugin, template-file, and worker to 4.18.1.
- Upgrade the scripts project's `lodash-es` declaration and its independent lockfile to 4.18.1.
- Upgrade the complete GraphQL Code Generator family used by EAS CLI:
  - `@graphql-codegen/cli` 5.0.0 → 6.3.1
  - `@graphql-codegen/introspection` 4.0.0 → 5.0.2
  - `@graphql-codegen/typescript` 4.0.1 → 5.0.10
  - `@graphql-codegen/typescript-operations` 4.0.1 → 5.1.0
- Regenerate the checked-in GraphQL TypeScript output.
- Keep the root free of `resolutions` and regenerate both affected lockfiles.

These are the newest aligned Codegen releases before `typescript-operations` v6. That v6 upgrade changes generated operation type semantics (including nullable response-field presence) and causes broad fixture/type failures across EAS CLI, so it should be handled as a separate GraphQL contract migration rather than bundled into this security dependency update. The selected packages use `@graphql-codegen/plugin-helpers` 6.3.x, which no longer depends on Lodash.

The root dependency graph now reports only `[email protected].1`, selected through ordinary declared dependency ranges. The separate scripts project reports only `[email protected].1`. There is no resolution or override.

# Test Plan

- `yarn install`
- `yarn why lodash` — only `[email protected].1`; no Codegen consumer
- `cd scripts && yarn install && yarn why lodash-es` — only `[email protected].1`
- `yarn build` — all 13 projects passed
- `yarn workspace eas-cli verify-graphql-code` — generated output compiles against the current live schema
- `cd scripts && yarn typecheck`
- `yarn lint-changelog`
- template-file tests — 7 passed
- worker tests — 51 passed
- `yarn fmt`
2026-07-15 22:32:56 +02:00
Stanisław Chmiela 78c30acbc9 [build-tools] Improve simulator recording finalization diagnostics (#4019)
## Summary

- increase the graceful `record-sim` shutdown timeout from 30 to 70 seconds
- record readable finalization milestones after a termination signal
- include the recorder's recent stdout/stderr when build-tools must force-stop it

## Why

A production device run session finalized two simulator recordings concurrently. One recording completed normally, while the other was force-killed after exactly 30 seconds and was therefore unable to write its manifest or upload its MP4.

The parent timeout was shorter than the recorder's own valid finalization window: `record-sim` can spend up to 5 seconds appending its final frame and up to 60 seconds waiting for `AVAssetWriter.finishWriting()`. Killing it after 30 seconds could therefore interrupt legitimate finalization.

The existing logs also could not distinguish between a process that failed to observe `SIGINT` and one that observed it but stalled later. The new milestones identify whether the recorder:

1. observed the termination signal,
2. stopped screen capture and began saving the video,
3. saved the video and began writing recording details.

These messages stay buffered during successful recordings. They are added to the user-visible forced-stop warning only when finalization exceeds 70 seconds, keeping normal job logs concise while making failures diagnosable.

## Validation

- built the release `record-sim` executable
- ran the build-tools TypeScript typecheck
- ran all 88 build-tools unit test suites (831 tests)
2026-07-15 22:02:17 +02:00
Szymon Dziedzic a716925ed7 [codex] fix tmp dependabot alert (#3965)
* fix tmp dependabot alert

* add changelog for tmp security fix
2026-07-15 16:25:19 +02:00
Szymon Dziedzic e579441d5b [eas-cli] clean up build inspect temp directory (#3981)
* Clean up build inspect temp directory

* [eas-cli] Add build inspect cleanup changelog

* [eas-cli] Fix build inspect test types
2026-07-15 16:09:27 +02:00
Szymon Dziedzic 4f5831305b [codex] fix ws dependabot alert (#3963)
* fix ws dependabot alert

* add changelog for ws security fix

* [worker] Update ws to latest hardened patch releases
2026-07-15 15:53:01 +02:00
Szymon Dziedzic 98db228cc8 [codex] fix sigstore dependabot alert (#3961)
* fix sigstore dependabot alert

* add changelog for sigstore security fix
2026-07-15 14:12:47 +02:00
Szymon Dziedzic 5c762d300e [codex] fix handlebars dependabot alert (#3958)
* fix handlebars dependabot alert

* add changelog for handlebars security fix
2026-07-15 13:36:18 +02:00
Stanisław Chmiela 2a6bee7f5a [build-tools] Remove updateEnv merge-order Datadog log (#3935)
## Summary
- remove the temporary Datadog log that compared old and new updateEnv merge-order results
- keep the new updateEnv merge order behavior unchanged
- remove the log-specific test assertions

## Test plan
- yarn fmt packages/build-tools/src/context.ts packages/build-tools/src/__tests__/context.test.ts
- yarn --cwd packages/build-tools jest-unit src/__tests__/context.test.ts
- yarn --cwd packages/build-tools typecheck

This is intended to be merged after we confirm the migration looks good in Datadog.
2026-07-15 13:30:54 +02:00
Stanisław Chmiela 4d365e0355 [build-tools][eas-cli] Upgrade xmldom through parent dependencies (#4010)
# Why

Supersedes #3967.

Dependabot alert 372 requires `@xmldom/xmldom` 0.8.13. The original PR used a root resolution to force 0.8.13 through parents that declared the incompatible `~0.7.7` range. This replacement upgrades those parent dependencies instead, so xmldom can be updated without overriding any package's declared contract.

# How

- Upgrade EAS CLI's direct `@expo/plist` dependency from 0.2.0 to 0.3.5.
- Upgrade build-tools' `@expo/plist` range from `^0.2.0` to `^0.3.5`.
- Upgrade EAS CLI's `@expo/prebuild-config` from 8.0.17 to 9.0.12, moving its config-plugin chain to a version that uses the newer plist package.
- Upgrade the transitive lockfile entry to `@xmldom/xmldom` 0.8.13.

After these changes, every installed plist consumer declares `@xmldom/xmldom ^0.8.8`, and `yarn why @xmldom/xmldom` reports a single natural 0.8.13 installation. No root resolution is added.

The relevant `getPrebuildConfigAsync` iOS path used by EAS CLI for entitlement inspection is unchanged across the prebuild-config upgrade; the direct implementation change is confined to passing `projectRoot` to the Android plugin path.

# Test Plan

- `yarn install`
- `yarn why @xmldom/xmldom`
- `yarn build` — all 13 projects passed with xmldom 0.8.13 installed
- Four build-tools plist/configuration suites — 19 tests and 14 snapshots passed
- EAS CLI `UpdatesModule` iOS suite — 2 tests passed
- `yarn fmt`
- `yarn fmt:check`
- `yarn lint` — 0 errors; 13 pre-existing warnings also present on `main`
2026-07-15 13:30:32 +02:00
Stanisław Chmiela 48aa2ccde2 [eas-cli] Remove Express from image URL test (#4008)
## Why

The image URL test only needs to serve a single PNG over HTTP. Using Express for that adds an otherwise-unused framework and its dependency tree to EAS CLI test dependencies.

This supersedes #3969. That PR upgrades Express to address the vulnerable transitive `path-to-regexp` version, but after this change EAS CLI no longer depends on Express, so the affected dependency path is removed instead of upgraded.

## What changed

- replace the Express static server with Node's built-in `http` server
- bind to port `0` so the operating system selects an available port
- remove `express` and `@types/express` from `packages/eas-cli`
- regenerate the lockfile, removing the associated Express 4 dependency tree

## Impact

This only changes test infrastructure. The test continues to exercise the real remote-URL fetch and PNG transparency validation path. There is no EAS CLI runtime behavior change.

## Validation

- `yarn workspace eas-cli test src/utils/__tests__/image-test.ts --runInBand`
- `yarn workspace eas-cli typecheck`
- formatting check
- `git diff --check`
2026-07-15 13:28:51 +02:00
Stanisław Chmiela bdc04e3e6b [build-tools] Add width and height to screen recording metadata (#4017) 2026-07-15 13:09:00 +02:00
Expo Bot 935491f967 [license] Update BSL Change Date to 2029-07-15 2026-07-15 09:31:33 +00:00
Stanisław Chmiela 031198d1aa Restore automated BSL change date updates (#4016)
## Summary

- restore the quarterly Business Source License change-date workflow from `expo/eas-build`
- update `LICENSE-BUSL` instead of the former repository's root `LICENSE`
- run once when the workflow lands on `main`, then every three months

## Why

The EAS Build packages moved into this repository, but their scheduled BSL change-date automation was not moved with them. As a result, `LICENSE-BUSL` still has the last date written by the archived repository.

After this workflow lands on `main`, its path-based push trigger will advance the stale change date and future scheduled runs will keep it three years ahead.

The workflow deliberately does not run on pull requests because it uses the write-capable `EXPO_BOT_PAT`. Existing production evidence confirms that token can update the protected default branch: [`Release workflow` run 29394979387](https://github.com/expo/eas-cli/actions/runs/29394979387) successfully pushed `main -> main` on July 15, 2026 using the same repository secret.

## Validation

- `actionlint .github/workflows/license.yml`
- YAML parse check
- embedded Bash syntax check
- `git diff --check`
2026-07-15 09:31:24 +00:00
Szymon Dziedzic ac60d159f2 [codex] fix axios dependabot alert (#3964)
* fix axios dependabot alert

* add changelog for axios security fix

* align axios version with nx
2026-07-15 11:20:28 +02:00
Expo CI fc09fe4a34 update CHANGELOG.md 2026-07-15 06:45:02 +00:00
51 changed files with 2762 additions and 3638 deletions

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@ commentConfiguration:
preamble: 'Subscribed to pull request'
epilogue: 'Generated by [CodeMention](https://github.com/expo/expo/actions/workflows/codemention.yaml)'
rules:
- patterns: ['**/*']
- patterns: ['packages/eas-cli/**']
mentions: ['douglowder']
- patterns: ['packages/eas-cli/src/update/**']
mentions: ['wschurman', 'quinlanj']
+37
View File
@@ -0,0 +1,37 @@
name: License
on:
schedule:
- cron: '0 0 1 1-12/3 *' # Run every three months
push:
branches:
- main
paths:
- .github/workflows/license.yml
workflow_dispatch:
jobs:
update:
runs-on: ubuntu-latest
name: Update BSL change date
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
token: ${{ secrets.EXPO_BOT_PAT }}
- name: Check last commit date
run: |
# The date in the license
change_date=$(sed --quiet --regexp-extended 's/^Change Date:\s+([0-9]{4}-[0-9]{2}-[0-9]{2})/\1/p' < LICENSE-BUSL)
[ -z "$change_date" ] && exit 1
new_change_date=$(date --date "+3 years" --iso-8601)
if [ "$(date --date "$new_change_date" +%s)" -gt "$(date --date "$change_date" +%s)" ]; then
sed --in-place "s/$change_date/$new_change_date/" LICENSE-BUSL
git config user.name 'Expo Bot'
git config user.email [email protected]
git add LICENSE-BUSL
git commit -m "[license] Update BSL Change Date to $new_change_date"
if [ "$(git branch --show-current)" = main ]; then
git push
fi
fi
+19 -2
View File
@@ -8,13 +8,28 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
### 🐛 Bug fixes
- [eas-cli] Remove temporary working directories after `eas build:inspect` copies inspect output, avoiding leftover disk usage after failed builds. ([#3981](https://github.com/expo/eas-cli/pull/3981) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- [eas-cli] Bump `tmp` to `0.2.7` to resolve [Dependabot alert 423](https://github.com/expo/eas-cli/security/dependabot/423). ([#3965](https://github.com/expo/eas-cli/pull/3965) by [@szdziedzic](https://github.com/szdziedzic))
- [worker] Bump `ws` to patched releases to resolve [Dependabot alert 462](https://github.com/expo/eas-cli/security/dependabot/462). ([#3963](https://github.com/expo/eas-cli/pull/3963) by [@szdziedzic](https://github.com/szdziedzic))
- Bump `lodash` and `lodash-es` to `4.18.1` across workspaces and upgrade GraphQL Code Generator to remove the remaining vulnerable transitive copy for Dependabot alert 354. ([#4009](https://github.com/expo/eas-cli/pull/4009) by [@sjchmiela](https://github.com/sjchmiela))
- [eas-cli] Bump `sigstore` to `4.1.1` to resolve [Dependabot alert 475](https://github.com/expo/eas-cli/security/dependabot/475). ([#3961](https://github.com/expo/eas-cli/pull/3961) by [@szdziedzic](https://github.com/szdziedzic))
- [eas-cli] Bump `axios` to patched releases to resolve [Dependabot alert 439](https://github.com/expo/eas-cli/security/dependabot/439). ([#3964](https://github.com/expo/eas-cli/pull/3964) by [@szdziedzic](https://github.com/szdziedzic))
- [eas-cli] Bump `handlebars` to `4.7.9` to resolve [Dependabot alert 334](https://github.com/expo/eas-cli/security/dependabot/334). ([#3958](https://github.com/expo/eas-cli/pull/3958) by [@szdziedzic](https://github.com/szdziedzic))
## [21.0.1](https://github.com/expo/eas-cli/releases/tag/v21.0.1) - 2026-07-15
### 🎉 New features
- [eas-cli] Add `eas project:delete` command to delete a project — the current directory's project by default, or any project passed as an argument (`@account/slug` or ID). Confirmation requires the project's full name, typed at the interactive prompt or passed via `--dangerously-confirm-deletion` (required in non-interactive mode). Deletion requires sudo mode: the command prompts for your password (and OTP) to upgrade the session when the server requires it. Deletion was previously only possible from the website. ([#4012](https://github.com/expo/eas-cli/pull/4012) by [@brentvatne](https://github.com/brentvatne))
- [eas-cli] Add `eas browse` to open the project page on expo.dev in a web browser, with an optional page argument (e.g. `eas browse build`, `eas browse hosting`) for project subpages and a `--no-browser` flag to print the URL. `eas open` is now an alias of `eas browse`. ([#4003](https://github.com/expo/eas-cli/pull/4003) by [@jonsamp](https://github.com/jonsamp))
- [eas-cli] Add `--dev-domain` flag to `eas deploy` to choose the project's preview URL (`<name>.expo.app`) without prompting, allowing first-time deploys with `--non-interactive`. ([#4004](https://github.com/expo/eas-cli/pull/4004) by [@brentvatne](https://github.com/brentvatne))
- [eas-cli] Add `-p` as an alias for `--platform` in `eas simulator:start`. ([#3902](https://github.com/expo/eas-cli/pull/3902) by [@brentvatne](https://github.com/brentvatne))
### 🐛 Bug fixes
### 🧹 Chores
- [eas-cli][build-tools][local-build-plugin][worker] Bump `tar` to `7.5.19`. ([#3971](https://github.com/expo/eas-cli/pull/3971) by [@szdziedzic](https://github.com/szdziedzic))
@@ -45,6 +60,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores
- Upgrade Expo plist dependencies to use `@xmldom/xmldom` `0.8.13` without a root resolution, addressing Dependabot alert 372. ([#4010](https://github.com/expo/eas-cli/pull/4010) by [@sjchmiela](https://github.com/sjchmiela))
- [eas-cli] add agent detection to analytics events. ([#3983](https://github.com/expo/eas-cli/pull/3983) by [@davidmokos](https://github.com/davidmokos))
- Bump `glob` 10.x to a patched version in the root lockfile. ([#3979](https://github.com/expo/eas-cli/pull/3979) by [@szdziedzic](https://github.com/szdziedzic))
- [build-tools] Bump `jws` to a patched version in the root lockfile. ([#3978](https://github.com/expo/eas-cli/pull/3978) by [@szdziedzic](https://github.com/szdziedzic))
@@ -61,6 +77,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- [eas-cli] Fix Free plan users being warned they had reached their build limit while still well below it. Overage warnings are now shown only for paid plans with an actual billable overage cost. ([#3882](https://github.com/expo/eas-cli/pull/3882) by [@sarahlane8](https://github.com/sarahlane8))
- [eas-cli] Count only genuine per-worker build overages in the pre-build usage warning, so the reported number of builds beyond included credits is accurate. ([#3932](https://github.com/expo/eas-cli/pull/3932) by [@sarahlane8](https://github.com/sarahlane8))
- [eas-cli] Select correct tvOS build target for non-interactive Apple builds when EXPO_TV env variable is set. ([#3907](https://github.com/expo/eas-cli/pull/3907) by [@douglowder](https://github.com/douglowder))
### 🧹 Chores
+1 -1
View File
@@ -12,7 +12,7 @@ Additional Use Grant: You may make use of the Licensed Work, provided that you d
to access the functionality of and directly benefit from the
functionality of the Licensed Work.
Change Date: 2028-04-01
Change Date: 2029-07-15
Change License: MIT
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "21.0.1",
"version": "21.0.2",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+6 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/build-tools",
"version": "21.0.1",
"version": "21.0.2",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "BUSL-1.1",
"author": "Expo <[email protected]>",
@@ -39,18 +39,17 @@
"@expo/config": "55.0.10",
"@expo/config-plugins": "55.0.7",
"@expo/downloader": "21.0.0",
"@expo/eas-build-job": "21.0.1",
"@expo/eas-build-job": "21.0.2",
"@expo/env": "^0.4.0",
"@expo/logger": "21.0.0",
"@expo/package-manager": "1.9.10",
"@expo/plist": "^0.2.0",
"@expo/plist": "^0.3.5",
"@expo/results": "^1.0.0",
"@expo/spawn-async": "1.7.2",
"@expo/steps": "21.0.1",
"@expo/template-file": "21.0.0",
"@expo/steps": "21.0.2",
"@expo/template-file": "21.0.2",
"@expo/turtle-spawn": "21.0.0",
"@expo/xcpretty": "^4.3.1",
"@google-cloud/storage": "^7.11.2",
"@ngrok/ngrok": "1.7.0",
"@sentry/node": "7.77.0",
"@urql/core": "^6.0.1",
@@ -61,7 +60,7 @@
"gql.tada": "^1.8.13",
"joi": "^17.13.1",
"jose": "5.10.0",
"lodash": "^4.17.21",
"lodash": "^4.18.1",
"node-fetch": "^2.7.0",
"node-forge": "^1.3.1",
"node-stream-zip": "1.15.0",
@@ -59,8 +59,15 @@ public struct FirstFrameWallClock: Codable, Sendable {
public let iso8601: String
}
public enum RecordingFinalizationStage: Sendable {
case captureStopped
case videoSaved
}
public struct RecordingManifest: Codable, Sendable {
public let firstFrameWallClock: FirstFrameWallClock
public let width: Int
public let height: Int
public let hlsVersion: Int?
public let hlsTargetDurationSeconds: Int?
public let hlsMediaSequence: Int?
@@ -100,7 +100,9 @@ final class RecordingOutputWriter: NSObject, AVAssetWriterDelegate {
@discardableResult
func writeManifest(
configuration: SimulatorRecordingConfiguration,
firstFrameWallClock: Date
firstFrameWallClock: Date,
width: Int,
height: Int
) throws -> RecordingManifest {
try stateQueue.sync {
if let firstError {
@@ -121,6 +123,8 @@ final class RecordingOutputWriter: NSObject, AVAssetWriterDelegate {
unixMs: unixMs(firstFrameWallClock),
iso8601: iso8601(firstFrameWallClock)
),
width: width,
height: height,
hlsVersion: segmented ? 7 : nil,
hlsTargetDurationSeconds: targetDuration,
hlsMediaSequence: segmented ? 0 : nil,
@@ -7,6 +7,7 @@ import UniformTypeIdentifiers
public final class SimulatorRecorder {
public var onSegment: ((SegmentOutput) -> Void)?
public var onSimulatorStopped: ((String) -> Void)?
public var onFinalizationStage: ((RecordingFinalizationStage) -> Void)?
private static let callbackStalenessTimeout: TimeInterval = 5
private static let firstFrameRewireInterval: TimeInterval = 1
@@ -26,6 +27,8 @@ public final class SimulatorRecorder {
private var monotonicClock = MonotonicClock()
private var firstAcceptedCaptureTime: CMTime?
private var firstAcceptedWallClock: Date?
private var recordingWidth: Int?
private var recordingHeight: Int?
private var lastPTS: CMTime?
private var lastSeed: UInt32?
private var lastFrameCallbackElapsed: TimeInterval?
@@ -113,6 +116,7 @@ public final class SimulatorRecorder {
displaySource?.stop()
displaySource = nil
}
onFinalizationStage?(.captureStopped)
return try writerQueue.sync {
if let firstError {
@@ -139,12 +143,18 @@ public final class SimulatorRecorder {
if let error = outputWriter.error {
throw error
}
onFinalizationStage?(.videoSaved)
guard let firstAcceptedWallClock else {
throw RecorderError.make(25, "Missing first frame wall-clock timestamp")
}
guard let recordingWidth, let recordingHeight else {
throw RecorderError.make(27, "Missing recording dimensions")
}
return try outputWriter.writeManifest(
configuration: configuration,
firstFrameWallClock: firstAcceptedWallClock
firstFrameWallClock: firstAcceptedWallClock,
width: recordingWidth,
height: recordingHeight
)
}
}
@@ -590,6 +600,8 @@ public final class SimulatorRecorder {
self.writer = writer
self.input = input
self.adaptor = adaptor
recordingWidth = width
recordingHeight = height
}
private func assetWriterSettings(width: Int, height: Int) -> [String: Any] {
@@ -10,15 +10,15 @@ struct CLIOptions {
var codec: RecorderCodec = .h264
}
private var stopRequested: CInt = 0
private var requestedStopSignal: CInt = 0
private var simulatorStopped: CInt = 0
private func handleStopSignal(_: CInt) {
stopRequested = 1
private func handleStopSignal(_ signal: CInt) {
requestedStopSignal = signal
}
private func isStopRequested() -> Bool {
stopRequested != 0 || simulatorStopped != 0
requestedStopSignal != 0 || simulatorStopped != 0
}
func usage() -> String {
@@ -112,6 +112,14 @@ do {
simulatorStopped = 1
fputs("[record-sim] simulator stopped state=\(state); finalizing recording\n", stderr)
}
recorder.onFinalizationStage = { stage in
switch stage {
case .captureStopped:
fputs("[record-sim] screen capture stopped; saving final video\n", stderr)
case .videoSaved:
fputs("[record-sim] final video saved; writing recording details\n", stderr)
}
}
recorder.onSegment = { segment in
switch segment.kind {
case .initialization:
@@ -130,6 +138,14 @@ do {
Thread.sleep(forTimeInterval: 0.1)
}
if requestedStopSignal != 0 {
let signalName = switch requestedStopSignal {
case SIGINT: "SIGINT"
case SIGTERM: "SIGTERM"
default: "signal \(requestedStopSignal)"
}
fputs("[record-sim] received \(signalName); finishing recording\n", stderr)
}
let manifest = try recorder.stop()
if let recording = manifest.recording {
fputs("[record-sim] done recording=\(recording)\n", stderr)
@@ -19,12 +19,10 @@ jest.mock('fs-extra');
jest.mock('../datadog', () => ({
Datadog: {
distribution: jest.fn(),
log: jest.fn(),
},
}));
const datadogDistributionMock = jest.mocked(Datadog.distribution);
const datadogLogMock = jest.mocked(Datadog.log);
describe('BuildContext', () => {
beforeEach(() => {
@@ -171,45 +169,6 @@ describe('BuildContext', () => {
expect(ctx.env.EXISTING_ENV).toBe('new-value');
expect(ctx.env.REMOVED_ENV).toBeUndefined();
expect(ctx.env.NEW_ENV).toBe('new-env-value');
const logMessage = datadogLogMock.mock.calls[0][0];
expect(logMessage).toContain('BuildContext.updateEnv merge order produced different env');
expect(logMessage).toContain('different_env_names=');
expect(logMessage).toContain('EXISTING_ENV');
expect(logMessage).toContain('REMOVED_ENV');
expect(JSON.stringify(datadogLogMock.mock.calls)).not.toContain('old-value');
expect(JSON.stringify(datadogLogMock.mock.calls)).not.toContain('new-value');
expect(JSON.stringify(datadogLogMock.mock.calls)).not.toContain('new-env-value');
});
it('logs when updateEnv merge order produces the same env without values', async () => {
await vol.promises.mkdir('/workingdir/eas-environment-secrets/', { recursive: true });
const ctx = new BuildContext(
{
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
secrets: {},
} as Job,
{
env: {
__API_SERVER_URL: 'http://api.expo.test',
EXISTING_ENV: 'same-value',
},
workingdir: '/workingdir',
logger: createMockLogger(),
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
uploadArtifact: jest.fn(),
}
);
ctx.updateEnv({
EXISTING_ENV: 'same-value',
UNSET_ENV: undefined,
});
expect(datadogLogMock).toHaveBeenCalledWith(
'BuildContext.updateEnv merge order produced same env'
);
expect(JSON.stringify(datadogLogMock.mock.calls)).not.toContain('same-value');
});
it('emits build phase duration metrics for successful build phases', async () => {
+1 -48
View File
@@ -72,41 +72,6 @@ export interface BuildContextOptions {
export class SkipNativeBuildError extends Error {}
function logEnvComparison({
oldMergeOrderEnv,
newMergeOrderEnv,
}: {
oldMergeOrderEnv: Env;
newMergeOrderEnv: Env;
}): void {
const envNames = new Set([...Object.keys(oldMergeOrderEnv), ...Object.keys(newMergeOrderEnv)]);
const sameEnvNames: string[] = [];
const differentEnvNames: string[] = [];
for (const key of envNames) {
const hasOldValue = key in oldMergeOrderEnv;
const hasNewValue = key in newMergeOrderEnv;
if (hasOldValue === hasNewValue && oldMergeOrderEnv[key] === newMergeOrderEnv[key]) {
sameEnvNames.push(key);
} else {
differentEnvNames.push(key);
}
}
if (differentEnvNames.length === 0) {
Datadog.log('BuildContext.updateEnv merge order produced same env');
return;
}
Datadog.log(
`BuildContext.updateEnv merge order produced different env: compared_env_count=${
envNames.size
} different_env_count=${differentEnvNames.length} different_env_names=${differentEnvNames.join(
','
)} same_env_count=${sameEnvNames.length} same_env_names=${sameEnvNames.join(',')}`
);
}
export class BuildContext<TJob extends Job = Job> {
public readonly workingdir: string;
public logger: bunyan;
@@ -283,23 +248,11 @@ export class BuildContext<TJob extends Job = Job> {
);
}
const oldMergeOrderEnv: Env = {
...env,
...this._env,
__EAS_BUILD_ENVS_DIR: this.buildEnvsDirectory,
};
const newMergeOrderEnv: Env = {
this._env = {
...this._env,
...env,
__EAS_BUILD_ENVS_DIR: this.buildEnvsDirectory,
};
logEnvComparison({
oldMergeOrderEnv,
newMergeOrderEnv,
});
this._env = newMergeOrderEnv;
this._env.PATH = this._env.PATH
? [this.buildExecutablesDirectory, this._env.PATH].join(':')
: this.buildExecutablesDirectory;
+8 -23
View File
@@ -8,7 +8,7 @@ import { Readable, Writable, pipeline } from 'stream';
import { promisify } from 'util';
import zlib from 'zlib';
import GCS from './client';
import { GCS } from './client';
type PromiseResolveFn = (value?: void | PromiseLike<void> | undefined) => void;
@@ -41,7 +41,7 @@ class GCSLoggerStream extends Writable {
}
private findNormalizedHeader(name: string): string | null {
if (!this.uploadMethod || 'client' in this.uploadMethod) {
if (!this.uploadMethod) {
return null;
}
@@ -165,25 +165,10 @@ class GCSLoggerStream extends Writable {
);
};
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 await GCS.uploadWithSignedUrl({
signedUrl: this.uploadMethod.signedUrl,
srcGeneratorAsync,
});
return Location;
}
private async createCompressedStream(src: Readable): Promise<Readable> {
@@ -223,9 +208,9 @@ namespace GCSLoggerStream {
compress?: CompressionMethod;
}
export type UploadMethod =
| { client: GCS; key: string; customTime: Date | null }
| { signedUrl: GCS.SignedUrl };
export type UploadMethod = {
signedUrl: GCS.SignedUrl;
};
export interface Config {
logger: bunyan;
@@ -1,10 +1,8 @@
import { randomBytes } from 'crypto';
import fs from 'fs-extra';
import fetch, { RequestInit, Response } from 'node-fetch';
import path from 'path';
import { Readable } from 'stream';
import GCS from '../client';
import { GCS } from '../client';
jest.mock('node-fetch');
@@ -29,18 +27,15 @@ class DNSError extends ErrorWithCode {
const TEST_BUCKET = 'turtle-v2-test';
let googleApplicationCredentials: string | undefined;
beforeAll(() => {
googleApplicationCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS;
process.env.GOOGLE_APPLICATION_CREDENTIALS = path.join(__dirname, 'mock-credentials.json');
});
afterAll(() => {
process.env.GOOGLE_APPLICATION_CREDENTIALS = googleApplicationCredentials;
});
function createSignedUrl(key: string, contentType: string): GCS.SignedUrl {
return {
url: `https://storage.googleapis.com/${TEST_BUCKET}/${key}?signature=test`,
headers: { 'content-type': contentType },
};
}
describe('GCS client', () => {
describe('uploadWithPresignedURL function', () => {
describe('uploadWithSignedUrl', () => {
it('should throw an error if upload fails', async () => {
const fetchMock = jest.mocked(fetch);
const res = {
@@ -48,22 +43,16 @@ describe('GCS client', () => {
status: 500,
} as Response;
const localImagePath = path.join(__dirname, 'cat.jpg');
const key = 'cat.jpg';
const gcs = new GCS(TEST_BUCKET);
const signedUrl = await gcs.createSignedUploadUrl({
key,
expirationTime: 30000,
contentType: 'image/jpeg',
});
const signedUrl = createSignedUrl(key, 'image/jpeg');
fetchMock.mockImplementation(async () => res);
await expect(
GCS.uploadWithSignedUrl({
signedUrl,
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
retryIntervalMs: 500,
srcGeneratorAsync: async () => Readable.from('image'),
retryIntervalMs: 0,
})
).rejects.toThrow();
});
@@ -75,21 +64,15 @@ describe('GCS client', () => {
status: 200,
} as Response;
const localImagePath = path.join(__dirname, 'cat.jpg');
const key = 'cat.jpg';
const gcs = new GCS(TEST_BUCKET);
const signedUrl = await gcs.createSignedUploadUrl({
key,
expirationTime: 30000,
contentType: 'image/jpeg',
});
const signedUrl = createSignedUrl(key, 'image/jpeg');
fetchMock.mockImplementation(async () => res);
const result = await GCS.uploadWithSignedUrl({
signedUrl,
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
retryIntervalMs: 500,
srcGeneratorAsync: async () => Readable.from('image'),
retryIntervalMs: 0,
});
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
});
@@ -101,15 +84,9 @@ describe('GCS client', () => {
status: 200,
} as Response;
const localImagePath = path.join(__dirname, 'cat.jpg');
const key = 'cat.jpg';
const gcs = new GCS(TEST_BUCKET);
const signedUrl = await gcs.createSignedUploadUrl({
key,
expirationTime: 30000,
contentType: 'image/jpeg',
});
const signedUrl = createSignedUrl(key, 'image/jpeg');
fetchMock
.mockImplementationOnce(async () => {
@@ -121,8 +98,8 @@ describe('GCS client', () => {
.mockImplementation(async () => res);
const result = await GCS.uploadWithSignedUrl({
signedUrl,
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
retryIntervalMs: 500,
srcGeneratorAsync: async () => Readable.from('image'),
retryIntervalMs: 0,
});
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
expect(fetchMock).toHaveBeenCalledTimes(3);
@@ -135,15 +112,9 @@ describe('GCS client', () => {
status: 200,
} as Response;
const localImagePath = path.join(__dirname, 'cat.jpg');
const key = 'cat.jpg';
const gcs = new GCS(TEST_BUCKET);
const signedUrl = await gcs.createSignedUploadUrl({
key,
expirationTime: 30000,
contentType: 'image/jpeg',
});
const signedUrl = createSignedUrl(key, 'image/jpeg');
fetchMock
.mockImplementationOnce(async () => {
@@ -161,8 +132,8 @@ describe('GCS client', () => {
.mockImplementation(async () => res);
const result = await GCS.uploadWithSignedUrl({
signedUrl,
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
retryIntervalMs: 500,
srcGeneratorAsync: async () => Readable.from('image'),
retryIntervalMs: 0,
});
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
expect(fetchMock).toHaveBeenCalledTimes(3);
@@ -175,15 +146,9 @@ describe('GCS client', () => {
status: 200,
} as Response;
const localImagePath = path.join(__dirname, 'cat.jpg');
const key = 'cat.jpg';
const gcs = new GCS(TEST_BUCKET);
const signedUrl = await gcs.createSignedUploadUrl({
key,
expirationTime: 30000,
contentType: 'image/jpeg',
});
const signedUrl = createSignedUrl(key, 'image/jpeg');
fetchMock
.mockImplementationOnce(async () => {
@@ -198,8 +163,8 @@ describe('GCS client', () => {
.mockImplementation(async () => res);
const result = await GCS.uploadWithSignedUrl({
signedUrl,
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
retryIntervalMs: 500,
srcGeneratorAsync: async () => Readable.from('image'),
retryIntervalMs: 0,
});
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
expect(fetchMock).toHaveBeenCalledTimes(3);
@@ -212,15 +177,9 @@ describe('GCS client', () => {
status: 200,
} as Response;
const localImagePath = path.join(__dirname, 'cat.jpg');
const key = 'cat.jpg';
const gcs = new GCS(TEST_BUCKET);
const signedUrl = await gcs.createSignedUploadUrl({
key,
expirationTime: 30000,
contentType: 'image/jpeg',
});
const signedUrl = createSignedUrl(key, 'image/jpeg');
const lastDNSError = new DNSError('failed thrice');
fetchMock
@@ -237,8 +196,8 @@ describe('GCS client', () => {
await expect(
GCS.uploadWithSignedUrl({
signedUrl,
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
retryIntervalMs: 500,
srcGeneratorAsync: async () => Readable.from('image'),
retryIntervalMs: 0,
})
).rejects.toThrow(lastDNSError);
expect(fetchMock).toHaveBeenCalledTimes(3);
@@ -251,15 +210,9 @@ describe('GCS client', () => {
status: 200,
} as Response;
const localImagePath = path.join(__dirname, 'cat.jpg');
const key = 'cat.jpg';
const gcs = new GCS(TEST_BUCKET);
const signedUrl = await gcs.createSignedUploadUrl({
key,
expirationTime: 30000,
contentType: 'image/jpeg',
});
const signedUrl = createSignedUrl(key, 'image/jpeg');
fetchMock
.mockImplementationOnce(async () => {
@@ -284,8 +237,8 @@ describe('GCS client', () => {
await expect(
GCS.uploadWithSignedUrl({
signedUrl,
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
retryIntervalMs: 500,
srcGeneratorAsync: async () => Readable.from('image'),
retryIntervalMs: 0,
})
).rejects.toThrow();
expect(fetchMock).toHaveBeenCalledTimes(3);
@@ -298,15 +251,9 @@ describe('GCS client', () => {
status: 200,
} as Response;
const localImagePath = path.join(__dirname, 'cat.jpg');
const key = 'cat.jpg';
const gcs = new GCS(TEST_BUCKET);
const signedUrl = await gcs.createSignedUploadUrl({
key,
expirationTime: 30000,
contentType: 'image/jpeg',
});
const signedUrl = createSignedUrl(key, 'image/jpeg');
const nonDNSError = new ErrorWithCode('failed once', 'A_DIFFERENT_CODE');
fetchMock
@@ -317,8 +264,8 @@ describe('GCS client', () => {
await expect(
GCS.uploadWithSignedUrl({
signedUrl,
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
retryIntervalMs: 500,
srcGeneratorAsync: async () => Readable.from('image'),
retryIntervalMs: 0,
})
).rejects.toThrow(nonDNSError);
expect(fetchMock).toHaveBeenCalledTimes(1);
@@ -354,19 +301,14 @@ describe('GCS client', () => {
} as Response;
});
const gcs = new GCS(TEST_BUCKET);
const signedUrl = await gcs.createSignedUploadUrl({
key: 'text.txt',
expirationTime: 30000,
contentType: 'text/plain',
});
const signedUrl = createSignedUrl('text.txt', 'text/plain');
const bufferToUpload = randomBytes(16);
const result = await GCS.uploadWithSignedUrl({
signedUrl,
srcGeneratorAsync: async () => Readable.from(bufferToUpload),
retryIntervalMs: 500,
retryIntervalMs: 0,
});
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/text.txt');
expect(fetchMock).toHaveBeenCalledTimes(3);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

@@ -1,13 +0,0 @@
{
"type": "service_account",
"project_id": "mock-project",
"private_key_id": "mock-private-key-id",
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXQIBAAKBgQCMk5Qbd5E8RQeP+2vObJVJVnXWXfW/OStxkAYe7r+KVXzHyLYU\nD6Ejt94OiC3X2+tqrMIZgNAUR3VF5tSiP57Il0iWHSspmNIHz7cW6/FHiQsvN2oJ\n8Fzz03vK/FfaU50QWO+BwRnixJ0xG1kSX3MxNyRZ5omGJgcwx0l6Jt878QIDAQAB\nAoGAcGZbqO6ceNU068H6/A1D/GSeSa3NHX2np+ChlEAPdJtP7yojs7yfekC801+9\nT/gurpe9hsjBF0XflemwIJ6/5KucWHrcp2cCkdBHtkK6ScHeIVl3DcOYYhxqJrAA\nYiyGNh1+pf3FMPFBcPJ04pQT4PqOif61/W3p8eJFUke5x7UCQQDoyJ427wrUCZb+\nj2GD5uVyxQa4OJxDbRFriK5LTdwLeFeOmQhoo6kaGywILn6UtlPTUJ38ROA4Jv58\nEJf/jssDAkEAmpi/JkWlOzSM9mnb9blt/fH/DRKu9z0JPvl47V5YAzBhUtUMK15g\nSbEl+AJZyaklpJcgv6ShiGY0PLdoY7oQ+wJBAMF6VX4lKqPYIKcN9ygRlm3Q8ufV\nLZQhKBRvsyYl3Zmu+V8tNL78IEXxhaR7OHxUGtINNHKDsLUbO/NUO3GOdo8CQQCV\nV0sIIK0+JUSq4ZYvqKI9d6Fnso28noSpBfuwabvh0MGjb9Viq7eeWHeSPksYSMLp\nXWiwWMwGZJy/rnk0JVEzAkADiiARp70BZgRB8ogBx+DGTravfhPP64eFMHJnUgAE\nmDKlj3v0x3OZfdUz6Us5hfy3WqFyYbcCGLf7joVVZAEm\n-----END RSA PRIVATE KEY-----",
"client_email": "mock-client-email",
"client_id": "mock-client-id",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/mock-client-email",
"universe_domain": "googleapis.com"
}
+15 -166
View File
@@ -1,37 +1,23 @@
import { CreateWriteStreamOptions, File, Storage } from '@google-cloud/storage';
import fs from 'fs-extra';
import fetch, { FetchError } from 'node-fetch';
import stream from 'node:stream';
import { promisify } from 'node:util';
import { Readable } from 'stream';
import { URL } from 'url';
import { RetryOptions, retryOnGCSUploadFailure } from './retry';
const pipeline = promisify(stream.pipeline);
interface SignedUrlParams {
key: string;
expirationTime: number;
contentType?: string;
extensionHeaders?: { [key: string]: number | string | string[] };
}
import { retryOnGCSUploadFailure } from './retry';
interface UploadWithSignedUrlParams {
signedUrl: GCS.SignedUrl;
srcGeneratorAsync: () => Promise<Readable>;
retryIntervalMs?: RetryOptions['retryIntervalMs'];
retries?: RetryOptions['retries'];
retryIntervalMs?: number;
retries?: number;
}
class GCS {
private readonly client = new Storage();
export namespace GCS {
export type SignedUrl = {
url: string;
headers: Record<string, string>;
};
constructor(private readonly bucket: string) {
this.bucket = bucket;
}
public static async uploadWithSignedUrl({
export async function uploadWithSignedUrl({
signedUrl,
srcGeneratorAsync,
retries = 2,
@@ -48,18 +34,15 @@ class GCS {
body: src,
});
},
{
retries,
retryIntervalMs,
}
{ retries, retryIntervalMs }
);
} catch (err: any) {
} catch (err: unknown) {
if (err instanceof FetchError) {
throw new Error(`Failed to upload the file, reason: ${err.code}`);
} else {
throw err;
}
throw err;
}
if (!resp.ok) {
let body: string | undefined;
try {
@@ -69,142 +52,8 @@ class GCS {
`Failed to upload file: status: ${resp.status} status text: ${resp.statusText}, body: ${body}`
);
}
const url = new URL(signedUrl.url);
return `${url.protocol}//${url.host}${url.pathname}`; // strip query string
}
public formatHttpUrl(key: string): string {
return this.client.bucket(this.bucket).file(key).publicUrl();
}
public async uploadFile({
key,
src,
streamOptions,
}: {
key: string;
src: Readable;
streamOptions?: CreateWriteStreamOptions;
}): Promise<{ Location: string }> {
const file = this.client.bucket(this.bucket).file(key);
await new Promise<void>((res, rej) => {
src.pipe(
file
.createWriteStream(streamOptions)
.on('error', err => {
rej(err);
})
.on('finish', () => {
res();
})
);
});
return { Location: file.publicUrl() };
}
public async deleteFile(key: string): Promise<void> {
try {
await this.client.bucket(this.bucket).file(key).delete();
} catch (err: any) {
if (err.response?.statusCode === 404) {
return;
}
throw err;
}
}
public async createSignedUploadUrl({
key,
expirationTime,
contentType,
extensionHeaders = {},
}: SignedUrlParams): Promise<GCS.SignedUrl> {
const config = {
version: 'v4' as const,
action: 'write' as const,
expires: Date.now() + expirationTime,
contentType,
extensionHeaders,
};
const [url] = await this.client.bucket(this.bucket).file(key).getSignedUrl(config);
return {
url,
headers: {
...(contentType ? { 'content-type': contentType } : {}),
...extensionHeaders,
},
};
}
public async createSignedDownloadUrl({
key,
expirationTime,
}: {
key: string;
expirationTime: number;
}): Promise<string> {
const options = {
version: 'v4' as const,
action: 'read' as const,
expires: Date.now() + expirationTime,
};
const [url] = await this.client.bucket(this.bucket).file(key).getSignedUrl(options);
return url;
}
public async checkIfFileExists(key: string, fileHash?: string): Promise<boolean> {
let metadata;
try {
[metadata] = await this.client.bucket(this.bucket).file(key).getMetadata();
} catch (error: any) {
if (error.code === 404) {
return false;
}
throw error;
}
return fileHash ? metadata.etag === fileHash : true;
}
public async listDirectory(prefix: string): Promise<string[]> {
const [files] = await this.client.bucket(this.bucket).getFiles({ prefix });
return files.map(x => this.formatHttpUrl(x.name));
}
public async moveFile(src: string, dest: string): Promise<void> {
await this.client.bucket(this.bucket).file(src).move(dest);
}
public async deleteFiles(keys: string[]): Promise<void> {
await Promise.all(keys.map(key => this.deleteFile(key)));
}
public async downloadFile(key: string, destinationPath: string): Promise<void> {
const stream = this.client.bucket(this.bucket).file(key).createReadStream();
await pipeline(stream, fs.createWriteStream(destinationPath));
}
public getFile(key: string): File {
return this.client.bucket(this.bucket).file(key);
return `${url.protocol}//${url.host}${url.pathname}`;
}
}
namespace GCS {
export interface SignedUrl {
url: string;
headers: { [key: string]: string };
}
export interface Config {
accessKeyId: string;
secretAccessKey: string;
region: string;
bucket: string;
}
}
export default GCS;
+16 -11
View File
@@ -1,11 +1,11 @@
import { Response } from 'node-fetch';
export interface RetryOptions {
type RetryOptions = {
retries: number;
retryIntervalMs: number;
shouldRetryOnError: (error: any) => boolean;
shouldRetryOnError: (error: unknown) => boolean;
shouldRetryOnResponse: (response: Response) => boolean;
}
};
// based on https://github.com/googleapis/nodejs-storage/blob/8ab50804fc7bae3bbd159bbb4adf65c02215b11b/src/storage.ts#L284-L320
export async function retryOnGCSUploadFailure(
@@ -18,13 +18,14 @@ export async function retryOnGCSUploadFailure(
return await retry(fn, {
retries,
retryIntervalMs,
shouldRetryOnError: e => {
shouldRetryOnError: err => {
return (
e.code === 'ENOTFOUND' ||
e.code === 'EAI_AGAIN' ||
e.code === 'ECONNRESET' ||
e.code === 'ETIMEDOUT' ||
e.code === 'EPIPE'
isErrorWithCode(err) &&
(err.code === 'ENOTFOUND' ||
err.code === 'EAI_AGAIN' ||
err.code === 'ECONNRESET' ||
err.code === 'ETIMEDOUT' ||
err.code === 'EPIPE')
);
},
shouldRetryOnResponse: resp => {
@@ -33,8 +34,12 @@ export async function retryOnGCSUploadFailure(
});
}
function isErrorWithCode(error: unknown): error is { code: string } {
return typeof error === 'object' && error !== null && 'code' in error;
}
/**
* Wrapper used to execute an inner function and possibly retry it if it throws and error
* Wrapper used to execute an inner function and possibly retry it if it throws an error
* @param fn Function to be executed and retried in case of error
* @param retries How many times at most should the function be retried
* @param retryIntervalMs Time interval between the retries
@@ -55,7 +60,7 @@ async function retry(
} else {
return resp;
}
} catch (err: any) {
} catch (err: unknown) {
if (attemptCount === retries || !shouldRetryOnError(err)) {
throw err;
}
+1 -1
View File
@@ -1,6 +1,6 @@
import * as Builders from './builders';
import GCSLoggerStream from './gcs/LoggerStream';
import GCS from './gcs/client';
import { GCS } from './gcs/client';
export { Builders, GCS, GCSLoggerStream };
@@ -12,7 +12,23 @@ jest.mock('../../utils/deviceRunSessionArtifacts', () => ({
uploadDeviceRunSessionArtifactAsync: jest.fn(),
}));
const SIMULATOR_UDID = '01234567-89AB-CDEF-0123-456789ABCDEF';
const SECOND_SIMULATOR_UDID = 'FEDCBA98-7654-3210-FEDC-BA9876543210';
describe(createUploadDeviceRunSessionScreenRecordingsBuildFunction, () => {
beforeEach(() => {
jest
.mocked(uploadDeviceRunSessionArtifactAsync)
.mockReset()
.mockImplementation(async (_ctx, { stream }) => {
await new Promise<void>((resolve, reject) => {
stream.once('end', resolve);
stream.once('error', reject);
stream.resume();
});
});
});
it('uploads simulator device metadata', async () => {
const recordingDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'ios-recording-test-'));
const recordingPath = path.join(recordingDirectory, 'recording.mp4');
@@ -22,6 +38,8 @@ describe(createUploadDeviceRunSessionScreenRecordingsBuildFunction, () => {
JSON.stringify({
recording: path.basename(recordingPath),
firstFrameWallClock: { iso8601: '2026-07-10T10:00:00.000Z' },
width: 1179,
height: 2556,
})
);
@@ -36,7 +54,7 @@ describe(createUploadDeviceRunSessionScreenRecordingsBuildFunction, () => {
callInputs: {
recordings_json: [
{
udid: 'simulator-udid',
udid: SIMULATOR_UDID,
deviceName: 'iPhone 16',
runtimeDisplayName: 'iOS 18.6',
directory: recordingDirectory,
@@ -50,13 +68,16 @@ describe(createUploadDeviceRunSessionScreenRecordingsBuildFunction, () => {
expect(uploadDeviceRunSessionArtifactAsync).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
name: 'iPhone 16 screen recording',
name: 'iPhone 16 screen recording (01234567-…, started at Jul 10, 2026, 10:00:00.000 UTC)',
metadata: {
__eas_type: 'screen-recording',
__eas_screen_recording: '1',
udid: 'simulator-udid',
udid: SIMULATOR_UDID,
deviceName: 'iPhone 16',
runtimeDisplayName: 'iOS 18.6',
firstFrameAt: '2026-07-10T10:00:00.000Z',
width: 1179,
height: 2556,
},
})
);
@@ -64,4 +85,62 @@ describe(createUploadDeviceRunSessionScreenRecordingsBuildFunction, () => {
await fs.rm(recordingDirectory, { recursive: true, force: true });
}
});
it('uses unique names for recordings from rebooted and same-model simulators', async () => {
const recordingDirectories = await Promise.all(
['2026-07-10T10:00:00.000Z', '2026-07-10T10:05:00.000Z', '2026-07-10T10:00:00.000Z'].map(
async firstFrameAt => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ios-recording-test-'));
await fs.writeFile(path.join(directory, 'recording.mp4'), 'recording');
await fs.writeFile(
path.join(directory, 'session.json'),
JSON.stringify({
recording: 'recording.mp4',
firstFrameWallClock: { iso8601: firstFrameAt },
width: 1179,
height: 2556,
})
);
return directory;
}
)
);
try {
const globalContext = createGlobalContextMock({
runtimePlatform: BuildRuntimePlatform.DARWIN,
});
const buildStep = createUploadDeviceRunSessionScreenRecordingsBuildFunction(
{} as CustomBuildContext
).createBuildStepFromFunctionCall(globalContext, {
env: { DEVICE_RUN_SESSION_ID: 'device-run-session-id' },
callInputs: {
recordings_json: recordingDirectories.map((directory, index) => ({
udid: index === 2 ? SECOND_SIMULATOR_UDID : SIMULATOR_UDID,
deviceName: 'iPhone 16',
runtimeDisplayName: 'iOS 18.6',
directory,
})),
},
});
await buildStep.executeAsync();
const names = jest
.mocked(uploadDeviceRunSessionArtifactAsync)
.mock.calls.map(([, options]) => options.name);
expect(names).toEqual(
expect.arrayContaining([
'iPhone 16 screen recording (01234567-…, started at Jul 10, 2026, 10:00:00.000 UTC)',
'iPhone 16 screen recording (01234567-…, started at Jul 10, 2026, 10:05:00.000 UTC)',
'iPhone 16 screen recording (FEDCBA98-…, started at Jul 10, 2026, 10:00:00.000 UTC)',
])
);
expect(new Set(names).size).toBe(3);
} finally {
await Promise.all(
recordingDirectories.map(directory => fs.rm(directory, { recursive: true, force: true }))
);
}
});
});
@@ -29,19 +29,18 @@ export function createStartServeSimRemoteSessionBuildFunction(
await selectXcodeDeveloperDirectoryAsync({ env, logger });
const { previewUrl, streamUrl } = await startServeSimWithTunnelAsync(ctx, {
const { previewUrl } = await startServeSimWithTunnelAsync(ctx, {
baseDomain: ngrokTunnelDomain,
env,
logger,
timeoutMs: STARTUP_TIMEOUT_MS,
});
logger.info(`Preview URL: ${previewUrl}`);
logger.info(`Stream URL: ${streamUrl}`);
await uploadRemoteSessionConfigAsync({
ctx,
deviceRunSessionId,
remoteConfig: { previewUrl, streamUrl },
remoteConfig: { previewUrl },
logger,
});
@@ -30,9 +30,24 @@ const RecordingManifestSchema = z.object({
firstFrameWallClock: z.object({
iso8601: z.string(),
}),
width: z.number().int().positive(),
height: z.number().int().positive(),
recording: z.string(),
});
const recordingStartTimeFormatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3,
hourCycle: 'h23',
timeZone: 'UTC',
timeZoneName: 'short',
});
export function createUploadDeviceRunSessionScreenRecordingsBuildFunction(
ctx: CustomBuildContext
): BuildFunction {
@@ -68,11 +83,15 @@ export function createUploadDeviceRunSessionScreenRecordingsBuildFunction(
await Promise.all(
recordings.map(recording =>
limit(async () => {
const displayName = `${recording.deviceName} screen recording`;
try {
const metadata = RecordingManifestSchema.parse(
JSON.parse(await readFile(path.join(recording.directory, 'session.json'), 'utf-8'))
);
const startedAt = recordingStartTimeFormatter.format(
new Date(metadata.firstFrameWallClock.iso8601)
);
const shortUdid = `${recording.udid.slice(0, 8)}-…`;
const displayName = `${recording.deviceName} screen recording (${shortUdid}, started at ${startedAt})`;
const recordingPath = path.join(recording.directory, metadata.recording);
const { size } = await stat(recordingPath);
const recordingId = path.basename(recording.directory);
@@ -86,11 +105,14 @@ export function createUploadDeviceRunSessionScreenRecordingsBuildFunction(
filename: `${recordingId}.mp4`,
kind: 'screen-recording',
metadata: {
__eas_type: 'screen-recording',
__eas_screen_recording: '1',
udid: recording.udid,
deviceName: recording.deviceName,
runtimeDisplayName: recording.runtimeDisplayName,
firstFrameAt: metadata.firstFrameWallClock.iso8601,
width: metadata.width,
height: metadata.height,
},
size,
stream: createReadStream(recordingPath),
@@ -12,7 +12,7 @@ import { Sentry } from '../../sentry';
import { IosSimulatorUtils, type IosSimulatorUuid } from '../../utils/IosSimulatorUtils';
const IOS_SIMULATOR_RECORDING_POLL_INTERVAL_MS = 2_000;
const RECORD_SIM_FINISH_TIMEOUT_MS = 30_000;
const RECORD_SIM_FINISH_TIMEOUT_MS = 70_000;
const RECORD_SIM_FORCE_STOP_TIMEOUT_MS = 5_000;
const RECORD_SIM_MAX_ATTEMPTS_PER_BOOT = 3;
const RECORD_SIM_COMMAND = 'record-sim';
@@ -114,7 +114,16 @@ export namespace IosSimulatorRecordingUtils {
return;
}
logger.warn(`Forcing iOS Simulator recording process for ${recording.deviceName} to stop.`);
const recordSimOutput = recording.getOutput().trim();
const finishTimeoutSeconds = Math.round(RECORD_SIM_FINISH_TIMEOUT_MS / 1_000);
logger.warn(
{ recordSimOutput },
`Screen recording for ${recording.deviceName} did not finish within ${finishTimeoutSeconds} seconds and will be stopped.${
recordSimOutput
? `\nRecent recorder messages:\n${recordSimOutput}`
: '\nNo recorder messages were captured.'
}`
);
recording.recordingProcess.kill('SIGKILL');
const killed = await Promise.race([
recording.completionPromise.then(() => true),
@@ -343,7 +343,7 @@ export async function startServeSimWithTunnelAsync(
logger: bunyan;
timeoutMs: number;
}
): Promise<{ previewUrl: string; streamUrl: string }> {
): Promise<{ previewUrl: string }> {
logger.info('Launching serve-sim with tunnel.');
const turnArgs = await fetchServeSimTurnArgsAsync(ctx, { env, logger });
const serveSim = spawnDetached({
@@ -366,33 +366,30 @@ export async function startServeSimWithTunnelAsync(
env,
});
logger.info('Waiting for serve-sim to report tunnel and stream URLs.');
logger.info('Waiting for serve-sim to report tunnel URL.');
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const output = serveSim.getOutput();
const previewUrl = matchLabeledUrl({ output, label: 'Tunnel', baseDomain });
const streamUrl = matchLabeledUrl({ output, label: 'Stream', baseDomain });
if (previewUrl && streamUrl) {
return { previewUrl, streamUrl };
const previewUrl = matchTunnelUrl({ output, baseDomain });
if (previewUrl) {
return { previewUrl };
}
await sleepAsync(1_000);
}
throw new SystemError(
`Timed out waiting for serve-sim to report Tunnel and Stream URLs. Last output:\n${serveSim.getOutput() || '<empty>'}`
`Timed out waiting for serve-sim to report Tunnel URL. Last output:\n${serveSim.getOutput() || '<empty>'}`
);
}
function matchLabeledUrl({
function matchTunnelUrl({
output,
label,
baseDomain,
}: {
output: string;
label: string;
baseDomain: string;
}): string | null {
const labelPattern = new RegExp(
`${label}:\\s*(https:\\/\\/[a-z0-9-]+\\.${escapeRegExp(baseDomain)})`
`Tunnel:\\s*(https:\\/\\/[a-z0-9-]+\\.${escapeRegExp(baseDomain)})`
);
const match = labelPattern.exec(output);
return match ? match[1] : null;
@@ -1,6 +1,6 @@
{
"name": "create-eas-build-function",
"version": "21.0.1",
"version": "21.0.2",
"description": "Create functions for use in EAS Build custom builds.",
"keywords": [
"expo",
@@ -35,7 +35,7 @@
"clean": "rimraf ./build/ \"*.tsbuildinfo\""
},
"dependencies": {
"@expo/steps": "21.0.1"
"@expo/steps": "21.0.2"
},
"devDependencies": {
"@expo/package-manager": "1.9.10",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/eas-build-job",
"version": "21.0.1",
"version": "21.0.2",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "MIT",
"author": "Expo <[email protected]>",
@@ -2,7 +2,7 @@ import { HOOK_ANCHORS, parseHookKey } from '../hooks';
describe('HOOK_ANCHORS', () => {
it('contains the v1 anchors', () => {
expect(Object.keys(HOOK_ANCHORS).sort()).toEqual([
expect([...HOOK_ANCHORS].sort()).toEqual([
'checkout',
'install_node_modules',
'maestro_cloud',
@@ -10,12 +10,6 @@ describe('HOOK_ANCHORS', () => {
'submit',
]);
});
it('every entry has a non-empty description', () => {
for (const entry of Object.values(HOOK_ANCHORS)) {
expect(entry.description.length).toBeGreaterThan(0);
}
});
});
describe(parseHookKey, () => {
+14 -21
View File
@@ -12,31 +12,24 @@
* inside a function-group expansion — is an occurrence of the anchor). Several
* steps and several functions may declare the same anchor.
*/
export const HOOK_ANCHORS = {
install_node_modules: {
description: 'The step that installs node modules.',
},
submit: {
description: 'The step that submits the application binary to the store.',
},
maestro_tests: {
description: 'The step that runs Maestro tests.',
},
maestro_cloud: {
description: 'The steps that upload to Maestro Cloud and gather its results.',
},
checkout: {
description: 'The step that checks out the project sources.',
},
} as const satisfies Record<string, { description: string }>;
export const HOOK_ANCHORS = [
'install_node_modules',
'submit',
'maestro_tests',
'maestro_cloud',
'checkout',
] as const;
export type HookAnchorId = keyof typeof HOOK_ANCHORS;
export type HookAnchorId = (typeof HOOK_ANCHORS)[number];
export type HookKey = `before_${HookAnchorId}` | `after_${HookAnchorId}`;
export const HOOK_KEYS: readonly HookKey[] = HOOK_ANCHORS.flatMap((anchor): HookKey[] => [
`before_${anchor}`,
`after_${anchor}`,
]);
export function isHookAnchorId(value: string): value is HookAnchorId {
// Own-property check: `in` would also match Object prototype names
// ("toString", "__proto__", …), breaking the unknown-anchors-are-inert rule.
return Object.hasOwn(HOOK_ANCHORS, value);
return HOOK_ANCHORS.some(anchor => anchor === value);
}
export function parseHookKey(
+114 -114
View File
@@ -205,7 +205,7 @@ DESCRIPTION
view the audit logs for an account
```
_See code: [packages/eas-cli/src/commands/account/audit.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/account/audit.ts)_
_See code: [packages/eas-cli/src/commands/account/audit.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/account/audit.ts)_
## `eas account:login`
@@ -226,7 +226,7 @@ ALIASES
$ eas login
```
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/account/login.ts)_
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -243,7 +243,7 @@ ALIASES
$ eas logout
```
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:usage [ACCOUNT_NAME]`
@@ -265,7 +265,7 @@ DESCRIPTION
view account usage and billing for the current cycle
```
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/account/usage.ts)_
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/account/usage.ts)_
## `eas account:view`
@@ -282,7 +282,7 @@ ALIASES
$ eas whoami
```
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/account/view.ts)_
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -296,7 +296,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/analytics.ts)_
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -348,7 +348,7 @@ DESCRIPTION
create a branch
```
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -369,7 +369,7 @@ DESCRIPTION
delete a branch
```
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -389,7 +389,7 @@ DESCRIPTION
list all branches
```
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -409,7 +409,7 @@ DESCRIPTION
rename a branch
```
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -432,7 +432,7 @@ DESCRIPTION
view a branch
```
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/branch/view.ts)_
## `eas browse [PAGE]`
@@ -455,7 +455,7 @@ DESCRIPTION
Transition from the terminal to the web browser to view and interact with your project on https://expo.dev
```
_See code: [packages/eas-cli/src/commands/browse.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/browse.ts)_
_See code: [packages/eas-cli/src/commands/browse.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/browse.ts)_
## `eas build`
@@ -496,7 +496,7 @@ DESCRIPTION
start a build
```
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/build/index.ts)_
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -516,7 +516,7 @@ DESCRIPTION
cancel a build
```
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -534,7 +534,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:delete [BUILD_ID]`
@@ -554,7 +554,7 @@ DESCRIPTION
delete a build
```
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/build/delete.ts)_
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/build/delete.ts)_
## `eas build:dev`
@@ -579,7 +579,7 @@ DESCRIPTION
run dev client simulator/emulator build with matching fingerprint or create a new one
```
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/build/dev.ts)_
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/build/dev.ts)_
## `eas build:download`
@@ -606,7 +606,7 @@ DESCRIPTION
download a simulator/emulator build by build ID or fingerprint hash
```
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/build/download.ts)_
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/build/download.ts)_
## `eas build:inspect`
@@ -644,7 +644,7 @@ DESCRIPTION
inspect the state of the project at specific build stages, useful for troubleshooting
```
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -684,7 +684,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/build/list.ts)_
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -714,7 +714,7 @@ DESCRIPTION
re-sign a build archive
```
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/build/resign.ts)_
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -742,7 +742,7 @@ DESCRIPTION
run simulator/emulator builds from eas-cli
```
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/build/run.ts)_
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -794,7 +794,7 @@ DESCRIPTION
get the latest version from EAS servers
```
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/build/version/get.ts)_
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/build/version/get.ts)_
## `eas build:version:set`
@@ -812,7 +812,7 @@ DESCRIPTION
update version of an app
```
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/build/version/set.ts)_
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -830,7 +830,7 @@ DESCRIPTION
update a version in native code with a value stored on EAS servers
```
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/build/version/sync.ts)_
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -847,7 +847,7 @@ DESCRIPTION
view a build for your project
```
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/build/view.ts)_
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -868,7 +868,7 @@ DESCRIPTION
create a channel
```
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:delete [NAME]`
@@ -889,7 +889,7 @@ DESCRIPTION
Delete a channel
```
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/channel/delete.ts)_
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/channel/delete.ts)_
## `eas channel:edit [NAME]`
@@ -911,7 +911,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:insights`
@@ -935,7 +935,7 @@ DESCRIPTION
display adoption, crash, and unique-user insights for a channel + runtime version
```
_See code: [packages/eas-cli/src/commands/channel/insights.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/channel/insights.ts)_
_See code: [packages/eas-cli/src/commands/channel/insights.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/channel/insights.ts)_
## `eas channel:list`
@@ -955,7 +955,7 @@ DESCRIPTION
list all channels
```
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:pause [NAME]`
@@ -977,7 +977,7 @@ DESCRIPTION
pause a channel to stop it from sending updates
```
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/channel/pause.ts)_
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/channel/pause.ts)_
## `eas channel:resume [NAME]`
@@ -999,7 +999,7 @@ DESCRIPTION
resume a channel to start sending updates
```
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/channel/resume.ts)_
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/channel/resume.ts)_
## `eas channel:rollout [CHANNEL]`
@@ -1034,7 +1034,7 @@ DESCRIPTION
Roll a new branch out on a channel incrementally.
```
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/channel/rollout.ts)_
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -1057,7 +1057,7 @@ DESCRIPTION
view a channel
```
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -1078,7 +1078,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/config.ts)_
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -1095,7 +1095,7 @@ DESCRIPTION
manage credentials
```
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/credentials/index.ts)_
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/credentials/index.ts)_
## `eas credentials:configure-build`
@@ -1113,7 +1113,7 @@ DESCRIPTION
Set up credentials for building your project.
```
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/credentials/configure-build.ts)_
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/credentials/configure-build.ts)_
## `eas deploy [options]`
@@ -1145,7 +1145,7 @@ ALIASES
$ eas worker:deploy
```
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/deploy/index.ts)_
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/deploy/index.ts)_
## `eas deploy:alias`
@@ -1170,7 +1170,7 @@ ALIASES
$ eas deploy:promote
```
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/deploy/alias/index.ts)_
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/deploy/alias/index.ts)_
## `eas deploy:alias:delete [ALIAS_NAME]`
@@ -1191,7 +1191,7 @@ ALIASES
$ eas worker:alias:delete
```
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
## `eas deploy:delete [DEPLOYMENT_ID]`
@@ -1212,7 +1212,7 @@ ALIASES
$ eas worker:delete
```
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/deploy/delete.ts)_
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/deploy/delete.ts)_
## `eas deploy:promote`
@@ -1249,7 +1249,7 @@ DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/device/create.ts)_
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -1269,7 +1269,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -1290,7 +1290,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/device/list.ts)_
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:rename`
@@ -1311,7 +1311,7 @@ DESCRIPTION
rename a registered device
```
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/device/rename.ts)_
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/device/rename.ts)_
## `eas device:view [UDID]`
@@ -1325,7 +1325,7 @@ DESCRIPTION
view a device for your project
```
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/device/view.ts)_
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -1339,7 +1339,7 @@ DESCRIPTION
display environment info
```
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas env:create [ENVIRONMENT]`
@@ -1371,7 +1371,7 @@ DESCRIPTION
create an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/env/create.ts)_
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/env/create.ts)_
## `eas env:delete [ENVIRONMENT]`
@@ -1397,7 +1397,7 @@ DESCRIPTION
delete an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/env/delete.ts)_
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/env/delete.ts)_
## `eas env:exec ENVIRONMENT BASH_COMMAND`
@@ -1419,7 +1419,7 @@ DESCRIPTION
execute a command with environment variables from the selected environment
```
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/env/exec.ts)_
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/env/exec.ts)_
## `eas env:get [ENVIRONMENT]`
@@ -1447,7 +1447,7 @@ DESCRIPTION
view an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/env/get.ts)_
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/env/get.ts)_
## `eas env:list [ENVIRONMENT]`
@@ -1475,7 +1475,7 @@ DESCRIPTION
list environment variables for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/env/list.ts)_
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/env/list.ts)_
## `eas env:pull [ENVIRONMENT]`
@@ -1498,7 +1498,7 @@ DESCRIPTION
pull environment variables for the selected environment to .env file
```
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/env/pull.ts)_
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/env/pull.ts)_
## `eas env:push [ENVIRONMENT]`
@@ -1520,7 +1520,7 @@ DESCRIPTION
push environment variables from .env file to the selected environment
```
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/env/push.ts)_
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/env/push.ts)_
## `eas env:update [ENVIRONMENT]`
@@ -1554,7 +1554,7 @@ DESCRIPTION
update an environment variable on the current project or account
```
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/env/update.ts)_
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/env/update.ts)_
## `eas fingerprint:compare [HASH1] [HASH2]`
@@ -1598,7 +1598,7 @@ EXAMPLES
$ eas fingerprint:compare <FINGERPRINT-HASH> --update-id <UPDATE-ID> # Compare fingerprint from update against provided fingerprint
```
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/fingerprint/compare.ts)_
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/fingerprint/compare.ts)_
## `eas fingerprint:generate`
@@ -1629,7 +1629,7 @@ EXAMPLES
$ eas fingerprint:generate --json --non-interactive --platform android # Output fingerprint json to stdout
```
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/fingerprint/generate.ts)_
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/fingerprint/generate.ts)_
## `eas help [COMMAND]`
@@ -1692,7 +1692,7 @@ DESCRIPTION
connect a project to an App Store Connect app
```
_See code: [packages/eas-cli/src/commands/integrations/asc/connect.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/asc/connect.ts)_
_See code: [packages/eas-cli/src/commands/integrations/asc/connect.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/asc/connect.ts)_
## `eas integrations:asc:disconnect`
@@ -1711,7 +1711,7 @@ DESCRIPTION
disconnect the current project from its App Store Connect app
```
_See code: [packages/eas-cli/src/commands/integrations/asc/disconnect.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/asc/disconnect.ts)_
_See code: [packages/eas-cli/src/commands/integrations/asc/disconnect.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/asc/disconnect.ts)_
## `eas integrations:asc:status`
@@ -1729,7 +1729,7 @@ DESCRIPTION
show the App Store Connect app link status for the current project
```
_See code: [packages/eas-cli/src/commands/integrations/asc/status.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/asc/status.ts)_
_See code: [packages/eas-cli/src/commands/integrations/asc/status.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/asc/status.ts)_
## `eas integrations:convex:connect`
@@ -1751,7 +1751,7 @@ DESCRIPTION
connect Convex to your Expo project
```
_See code: [packages/eas-cli/src/commands/integrations/convex/connect.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/convex/connect.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/connect.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/convex/connect.ts)_
## `eas integrations:convex:dashboard`
@@ -1765,7 +1765,7 @@ DESCRIPTION
open the Convex dashboard for the linked Convex project
```
_See code: [packages/eas-cli/src/commands/integrations/convex/dashboard.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/convex/dashboard.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/dashboard.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/convex/dashboard.ts)_
## `eas integrations:convex:project`
@@ -1779,7 +1779,7 @@ DESCRIPTION
display the Convex project linked to the current Expo app
```
_See code: [packages/eas-cli/src/commands/integrations/convex/project.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/convex/project.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/project.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/convex/project.ts)_
## `eas integrations:convex:project:delete`
@@ -1797,7 +1797,7 @@ DESCRIPTION
remove the Convex project link for the current Expo app from EAS servers
```
_See code: [packages/eas-cli/src/commands/integrations/convex/project/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/convex/project/delete.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/project/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/convex/project/delete.ts)_
## `eas integrations:convex:team`
@@ -1811,7 +1811,7 @@ DESCRIPTION
display Convex teams linked to the current Expo app's owner account
```
_See code: [packages/eas-cli/src/commands/integrations/convex/team.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/convex/team.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/team.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/convex/team.ts)_
## `eas integrations:convex:team:delete [CONVEX_TEAM]`
@@ -1832,7 +1832,7 @@ DESCRIPTION
remove a Convex team link from the current Expo app owner account's EAS servers
```
_See code: [packages/eas-cli/src/commands/integrations/convex/team/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/convex/team/delete.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/team/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/convex/team/delete.ts)_
## `eas integrations:convex:team:invite [CONVEX_TEAM]`
@@ -1852,7 +1852,7 @@ DESCRIPTION
send a Convex team invitation to your verified email address
```
_See code: [packages/eas-cli/src/commands/integrations/convex/team/invite.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/convex/team/invite.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/team/invite.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/convex/team/invite.ts)_
## `eas integrations:posthog:connect`
@@ -1879,7 +1879,7 @@ DESCRIPTION
connect PostHog to your Expo project
```
_See code: [packages/eas-cli/src/commands/integrations/posthog/connect.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/posthog/connect.ts)_
_See code: [packages/eas-cli/src/commands/integrations/posthog/connect.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/posthog/connect.ts)_
## `eas integrations:posthog:dashboard`
@@ -1899,7 +1899,7 @@ DESCRIPTION
open the PostHog dashboard for the linked PostHog project
```
_See code: [packages/eas-cli/src/commands/integrations/posthog/dashboard.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/posthog/dashboard.ts)_
_See code: [packages/eas-cli/src/commands/integrations/posthog/dashboard.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/posthog/dashboard.ts)_
## `eas integrations:posthog:disconnect`
@@ -1918,7 +1918,7 @@ DESCRIPTION
remove the PostHog project link for the current Expo app from EAS servers
```
_See code: [packages/eas-cli/src/commands/integrations/posthog/disconnect.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/posthog/disconnect.ts)_
_See code: [packages/eas-cli/src/commands/integrations/posthog/disconnect.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/posthog/disconnect.ts)_
## `eas login`
@@ -1970,7 +1970,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/metadata/lint.ts)_
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -1988,7 +1988,7 @@ DESCRIPTION
generate the local store configuration from the app stores
```
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -2006,7 +2006,7 @@ DESCRIPTION
sync the local store configuration to the app stores
```
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas new [PATH]`
@@ -2065,7 +2065,7 @@ DESCRIPTION
arguments, a list of the available event names and associated event counts is returned.
```
_See code: [packages/eas-cli/src/commands/observe/events.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/observe/events.ts)_
_See code: [packages/eas-cli/src/commands/observe/events.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/observe/events.ts)_
## `eas observe:metrics [METRIC]`
@@ -2101,7 +2101,7 @@ DESCRIPTION
display individual performance metric samples ordered by value
```
_See code: [packages/eas-cli/src/commands/observe/metrics.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/observe/metrics.ts)_
_See code: [packages/eas-cli/src/commands/observe/metrics.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/observe/metrics.ts)_
## `eas observe:metrics-summary`
@@ -2133,7 +2133,7 @@ DESCRIPTION
display aggregated performance metric statistics grouped by app version
```
_See code: [packages/eas-cli/src/commands/observe/metrics-summary.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/observe/metrics-summary.ts)_
_See code: [packages/eas-cli/src/commands/observe/metrics-summary.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/observe/metrics-summary.ts)_
## `eas observe:routes`
@@ -2170,7 +2170,7 @@ DESCRIPTION
display app navigation route metrics (Cold TTR, Warm TTR, TTI) grouped by route name
```
_See code: [packages/eas-cli/src/commands/observe/routes.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/observe/routes.ts)_
_See code: [packages/eas-cli/src/commands/observe/routes.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/observe/routes.ts)_
## `eas observe:session SESSIONID`
@@ -2192,7 +2192,7 @@ DESCRIPTION
display the timeline of metric and log events for a specific session
```
_See code: [packages/eas-cli/src/commands/observe/session.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/observe/session.ts)_
_See code: [packages/eas-cli/src/commands/observe/session.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/observe/session.ts)_
## `eas observe:versions`
@@ -2217,7 +2217,7 @@ DESCRIPTION
display app versions with build and update details
```
_See code: [packages/eas-cli/src/commands/observe/versions.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/observe/versions.ts)_
_See code: [packages/eas-cli/src/commands/observe/versions.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/observe/versions.ts)_
## `eas project:delete [NAME]`
@@ -2241,7 +2241,7 @@ DESCRIPTION
delete a project
```
_See code: [packages/eas-cli/src/commands/project/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/project/delete.ts)_
_See code: [packages/eas-cli/src/commands/project/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/project/delete.ts)_
## `eas project:info`
@@ -2255,7 +2255,7 @@ DESCRIPTION
information about the current project
```
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/project/info.ts)_
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -2278,7 +2278,7 @@ ALIASES
$ eas init
```
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/project/init.ts)_
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/project/init.ts)_
## `eas project:new [PATH]`
@@ -2302,7 +2302,7 @@ ALIASES
$ eas new
```
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/project/new.ts)_
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/project/new.ts)_
## `eas submit`
@@ -2335,7 +2335,7 @@ ALIASES
$ eas build:submit
```
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/submit.ts)_
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -2378,7 +2378,7 @@ DESCRIPTION
publish an update group
```
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/index.ts)_
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -2399,7 +2399,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -2420,7 +2420,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:edit [GROUPID]`
@@ -2445,7 +2445,7 @@ DESCRIPTION
edit all the updates in an update group
```
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/edit.ts)_
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/edit.ts)_
## `eas update:embedded:delete ID`
@@ -2466,7 +2466,7 @@ DESCRIPTION
delete an embedded update registered with EAS Update
```
_See code: [packages/eas-cli/src/commands/update/embedded/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/embedded/delete.ts)_
_See code: [packages/eas-cli/src/commands/update/embedded/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/embedded/delete.ts)_
## `eas update:embedded:list`
@@ -2492,7 +2492,7 @@ DESCRIPTION
list embedded updates registered with EAS Update for this project
```
_See code: [packages/eas-cli/src/commands/update/embedded/list.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/embedded/list.ts)_
_See code: [packages/eas-cli/src/commands/update/embedded/list.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/embedded/list.ts)_
## `eas update:embedded:upload`
@@ -2522,7 +2522,7 @@ EXAMPLES
$ eas update:embedded:upload --platform android --bundle android/app/src/main/assets/index.android.bundle --manifest android/app/src/main/assets/app.manifest --channel production --build-id <BUILD-ID>
```
_See code: [packages/eas-cli/src/commands/update/embedded/upload.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/embedded/upload.ts)_
_See code: [packages/eas-cli/src/commands/update/embedded/upload.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/embedded/upload.ts)_
## `eas update:embedded:view ID`
@@ -2542,7 +2542,7 @@ DESCRIPTION
view details of an embedded update registered with EAS Update
```
_See code: [packages/eas-cli/src/commands/update/embedded/view.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/embedded/view.ts)_
_See code: [packages/eas-cli/src/commands/update/embedded/view.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/embedded/view.ts)_
## `eas update:insights GROUPID`
@@ -2569,7 +2569,7 @@ DESCRIPTION
display launch, crash, unique-user, and size insights for an update group
```
_See code: [packages/eas-cli/src/commands/update/insights.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/insights.ts)_
_See code: [packages/eas-cli/src/commands/update/insights.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/insights.ts)_
## `eas update:list`
@@ -2596,7 +2596,7 @@ DESCRIPTION
view the recent updates
```
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/list.ts)_
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
@@ -2634,7 +2634,7 @@ DESCRIPTION
roll back to an existing update
```
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/republish.ts)_
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:revert-update-rollout`
@@ -2662,7 +2662,7 @@ DESCRIPTION
revert a rollout update for a project
```
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
## `eas update:roll-back-to-embedded`
@@ -2692,7 +2692,7 @@ DESCRIPTION
roll back to the embedded update
```
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
## `eas update:rollback [GROUPID]`
@@ -2724,7 +2724,7 @@ DESCRIPTION
roll back to an embedded update or an existing update
```
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/rollback.ts)_
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/rollback.ts)_
## `eas update:view GROUPID`
@@ -2748,7 +2748,7 @@ DESCRIPTION
update group details
```
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/view.ts)_
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/view.ts)_
## `eas upload`
@@ -2769,7 +2769,7 @@ DESCRIPTION
upload a local build and generate a sharable link
```
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/upload.ts)_
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/upload.ts)_
## `eas webhook:create`
@@ -2791,7 +2791,7 @@ DESCRIPTION
create a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -2811,7 +2811,7 @@ DESCRIPTION
delete a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -2830,7 +2830,7 @@ DESCRIPTION
list webhooks
```
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -2853,7 +2853,7 @@ DESCRIPTION
update a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -2870,7 +2870,7 @@ DESCRIPTION
view a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
@@ -2994,7 +2994,7 @@ DESCRIPTION
to cancel.
```
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/workflow/cancel.ts)_
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/workflow/cancel.ts)_
## `eas workflow:create [NAME]`
@@ -3016,7 +3016,7 @@ DESCRIPTION
create a new workflow configuration YAML file
```
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/workflow/create.ts)_
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/workflow/create.ts)_
## `eas workflow:logs [ID]`
@@ -3040,7 +3040,7 @@ DESCRIPTION
If no ID is passed in, you will be prompted to select from recent workflow runs for the current project.
```
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/workflow/logs.ts)_
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/workflow/logs.ts)_
## `eas workflow:run FILE`
@@ -3080,7 +3080,7 @@ FLAG DESCRIPTIONS
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
```
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/workflow/run.ts)_
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/workflow/run.ts)_
## `eas workflow:runs`
@@ -3102,7 +3102,7 @@ DESCRIPTION
list recent workflow runs for this project, with their IDs, statuses, and timestamps
```
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/workflow/runs.ts)_
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/workflow/runs.ts)_
## `eas workflow:status [WORKFLOW_RUN_ID]`
@@ -3130,7 +3130,7 @@ FLAG DESCRIPTIONS
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
```
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/workflow/status.ts)_
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/workflow/status.ts)_
## `eas workflow:validate PATH`
@@ -3150,7 +3150,7 @@ DESCRIPTION
validate a workflow configuration yaml file
```
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/workflow/validate.ts)_
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/workflow/validate.ts)_
## `eas workflow:view [ID]`
@@ -3172,5 +3172,5 @@ DESCRIPTION
workflow runs for the current project.
```
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/workflow/view.ts)_
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/workflow/view.ts)_
<!-- commandsstop -->
+6 -10
View File
@@ -67765,16 +67765,12 @@
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
"isDeprecated": true,
"deprecationReason": "Use previewUrl instead."
}
],
"inputFields": null,
@@ -93115,4 +93111,4 @@
}
]
}
}
}
+11 -13
View File
@@ -1,6 +1,6 @@
{
"name": "eas-cli",
"version": "21.0.1",
"version": "21.0.2",
"description": "EAS command line tool",
"keywords": [
"cli",
@@ -48,8 +48,8 @@
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "55.0.10",
"@expo/config-plugins": "55.0.7",
"@expo/eas-build-job": "21.0.1",
"@expo/eas-json": "21.0.1",
"@expo/eas-build-job": "21.0.2",
"@expo/eas-json": "21.0.2",
"@expo/env": "^1.0.0",
"@expo/json-file": "8.3.3",
"@expo/logger": "21.0.0",
@@ -57,14 +57,14 @@
"@expo/osascript": "2.1.4",
"@expo/package-manager": "1.9.10",
"@expo/pkcs12": "0.1.3",
"@expo/plist": "0.2.0",
"@expo/plist": "0.3.5",
"@expo/plugin-help": "5.1.23",
"@expo/plugin-warn-if-update-available": "2.5.1",
"@expo/prebuild-config": "8.0.17",
"@expo/prebuild-config": "9.0.12",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.2",
"@expo/steps": "21.0.1",
"@expo/steps": "21.0.2",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "^4.8.3",
"@oclif/plugin-autocomplete": "^3.2.40",
@@ -134,17 +134,16 @@
"zod": "^4.1.3"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/typescript": "4.0.1",
"@graphql-codegen/typescript-operations": "4.0.1",
"@graphql-codegen/cli": "6.3.1",
"@graphql-codegen/introspection": "5.0.2",
"@graphql-codegen/typescript": "5.0.10",
"@graphql-codegen/typescript-operations": "5.1.0",
"@tsconfig/node20": "20.1.8",
"@types/cli-progress": "3.11.5",
"@types/dateformat": "3.0.1",
"@types/debug": "4.1.13",
"@types/diff": "6.0.0",
"@types/envinfo": "7.8.3",
"@types/express": "4.17.15",
"@types/fs-extra": "11.0.4",
"@types/getenv": "^1.0.0",
"@types/invariant": "^2.2.37",
@@ -160,9 +159,8 @@
"@types/tough-cookie": "4.0.2",
"@types/uuid": "9.0.7",
"@types/wrap-ansi": "3.0.0",
"axios": "1.11.0",
"axios": "1.18.1",
"eslint-plugin-graphql": "4.0.0",
"express": "4.20.0",
"jest": "29.7.0",
"memfs": "3.4.13",
"mockdate": "3.0.5",
@@ -0,0 +1,92 @@
import fs from 'fs-extra';
import path from 'path';
import { getMockOclifConfig } from '../../../__tests__/commands/utils';
import { runBuildAndSubmitAsync } from '../../../build/runBuildAndSubmit';
import BuildInspect from '../inspect';
jest.mock('fs-extra', () => ({
pathExists: jest.fn(),
remove: jest.fn(),
mkdirp: jest.fn(),
copy: jest.fn(),
}));
jest.mock('../../../build/runBuildAndSubmit', () => ({
runBuildAndSubmitAsync: jest.fn(),
}));
jest.mock('../../../log');
jest.mock('../../../ora', () => ({
ora: jest.fn(() => ({
start: jest.fn(() => ({
succeed: jest.fn(),
fail: jest.fn(),
})),
})),
}));
jest.mock('../../../utils/paths', () => ({
...jest.requireActual('../../../utils/paths'),
getTmpDirectory: jest.fn(() => '/tmp/eas-cli'),
}));
jest.mock('uuid', () => ({
v4: jest.fn(() => 'tmp-id'),
}));
describe(BuildInspect, () => {
const mockConfig = getMockOclifConfig();
const tmpWorkingdir = '/tmp/eas-cli/tmp-id';
const tmpBuildDir = path.join(tmpWorkingdir, 'build');
const outputDirectory = path.join(process.cwd(), 'inspect-output');
const mockPathExists = fs.pathExists as jest.Mock;
const mockMkdirp = fs.mkdirp as jest.Mock;
const mockCopy = fs.copy as jest.Mock;
const mockRemove = fs.remove as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
mockPathExists.mockResolvedValue(false);
mockMkdirp.mockResolvedValue(undefined);
mockCopy.mockResolvedValue(undefined);
mockRemove.mockResolvedValue(undefined);
jest.mocked(runBuildAndSubmitAsync).mockResolvedValue({ buildIds: [] });
});
function createCommand(): BuildInspect {
const command = new BuildInspect(
['--platform', 'ios', '--stage', 'pre-build', '--output', 'inspect-output'],
mockConfig
);
jest.spyOn(command as any, 'getContextAsync').mockResolvedValue({
loggedIn: {
actor: {},
graphqlClient: {},
},
getDynamicPrivateProjectConfigAsync: jest.fn(),
projectDir: '/project',
analytics: {},
vcsClient: {},
} as any);
return command;
}
it('removes the temporary working directory after copying output when the build fails', async () => {
jest.mocked(runBuildAndSubmitAsync).mockRejectedValue(new Error('build failed'));
mockPathExists.mockResolvedValueOnce(false).mockResolvedValueOnce(true);
await createCommand().runAsync();
expect(fs.copy).toHaveBeenCalledWith(tmpBuildDir, outputDirectory);
expect(fs.remove).toHaveBeenCalledWith(tmpBuildDir);
expect(fs.remove).toHaveBeenCalledWith(tmpWorkingdir);
});
it('keeps the temporary working directory when copying output fails', async () => {
jest.mocked(runBuildAndSubmitAsync).mockRejectedValue(new Error('build failed'));
mockPathExists.mockResolvedValueOnce(false).mockResolvedValueOnce(true);
mockCopy.mockRejectedValue(new Error('copy failed'));
await expect(createCommand().runAsync()).rejects.toThrow('copy failed');
expect(fs.remove).not.toHaveBeenCalledWith(tmpBuildDir);
expect(fs.remove).not.toHaveBeenCalledWith(tmpWorkingdir);
});
});
@@ -135,6 +135,7 @@ export default class BuildInspect extends EasCommand {
}
} finally {
await this.copyToOutputDirAsync(path.join(tmpWorkingdir, 'build'), outputDirectory);
await this.cleanUpTmpWorkingdirAsync(tmpWorkingdir);
}
}
}
@@ -163,4 +164,13 @@ export default class BuildInspect extends EasCommand {
throw err;
}
}
private async cleanUpTmpWorkingdirAsync(tmpWorkingdir: string): Promise<void> {
try {
await fs.remove(tmpWorkingdir);
} catch (err) {
Log.warn(`Failed to remove temporary directory ${tmpWorkingdir}`);
Log.debug(err);
}
}
}
File diff suppressed because it is too large. Load diff
@@ -201,6 +201,12 @@ export const AccountQuery = {
overageMetrics {
id
value
metadata {
... on AccountUsageEASBuildMetadata {
billingResourceClass
platform
}
}
}
totalCost
}
@@ -62,7 +62,6 @@ export const DeviceRunSessionQuery = {
}
... on ServeSimRunSessionRemoteConfig {
previewUrl
streamUrl
}
}
turtleJobRun {
@@ -1,17 +1,13 @@
import { randomInt } from 'crypto';
import express from 'express';
import fs from 'fs';
import http from 'http';
import path from 'path';
import { ImageNonPngError, ImageTransparencyError, ensurePNGIsNotTransparentAsync } from '../image';
const TEST_SERVER_PORT = randomInt(9000, 9000 + 1000);
const fixturesPath = path.join(__dirname, 'fixtures');
const transparentPngPath = path.join(fixturesPath, 'icon-alpha.png');
const nonTransparentPngPath = path.join(fixturesPath, 'icon-no-alpha.png');
const jpgPath = path.join(fixturesPath, 'icon.jpg');
const nonTransparentPngURL = `http://127.0.0.1:${TEST_SERVER_PORT}/icon-alpha.png`;
describe(ensurePNGIsNotTransparentAsync, () => {
describe('local paths', () => {
@@ -32,15 +28,27 @@ describe(ensurePNGIsNotTransparentAsync, () => {
describe('remote URLs', () => {
let server: http.Server;
let transparentPngURL: string;
beforeAll(async () => {
await new Promise<void>(res => {
const app = express();
app.use(express.static(fixturesPath));
server = app.listen(TEST_SERVER_PORT, () => {
server = http.createServer((_request, response) => {
response.setHeader('Content-Type', 'image/png');
fs.createReadStream(transparentPngPath).pipe(response);
});
await new Promise<void>((res, rej) => {
const onError = (error: Error): void => rej(error);
server.once('error', onError);
server.listen(0, '127.0.0.1', () => {
server.off('error', onError);
res();
});
});
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('Failed to determine test server address');
}
transparentPngURL = `http://127.0.0.1:${address.port}/icon-alpha.png`;
});
afterAll(async () => {
@@ -52,7 +60,7 @@ describe(ensurePNGIsNotTransparentAsync, () => {
});
it('works with remote URLs', async () => {
await expect(ensurePNGIsNotTransparentAsync(nonTransparentPngURL)).rejects.toThrowError(
await expect(ensurePNGIsNotTransparentAsync(transparentPngURL)).rejects.toThrowError(
ImageTransparencyError
);
});
@@ -1,6 +1,8 @@
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import {
AccountUsageForOverageWarningQuery,
AppPlatform,
EasBuildBillingResourceClass,
EasService,
EasServiceMetric,
EstimatedUsage,
@@ -40,6 +42,30 @@ function createMockPlanMetric({
};
}
type MockOverageMetric =
AccountUsageForOverageWarningQuery['account']['byId']['usageMetrics']['EAS_BUILD']['overageMetrics'][number];
// A genuine per-worker build overage row, i.e. one carrying build metadata. Only rows
// shaped like this should be counted toward the "N builds beyond your credits" total.
function createMockBuildOverage({
id = 'overage-id',
value = 1,
platform = AppPlatform.Ios,
billingResourceClass = EasBuildBillingResourceClass.Medium,
}: {
id?: string;
value?: number;
platform?: AppPlatform;
billingResourceClass?: EasBuildBillingResourceClass;
} = {}): MockOverageMetric {
return {
__typename: 'EstimatedOverageAndCost',
id,
value,
metadata: { __typename: 'AccountUsageEASBuildMetadata', billingResourceClass, platform },
};
}
function createMockAccountUsage({
id = 'account-id',
name = 'test-account',
@@ -52,7 +78,7 @@ function createMockAccountUsage({
name?: string;
subscriptionName?: string;
buildPlanMetrics?: EstimatedUsage[];
overageMetrics?: { __typename: 'EstimatedOverageAndCost'; id: string; value: number }[];
overageMetrics?: MockOverageMetric[];
totalCost?: number;
} = {}): AccountUsageForOverageWarningQuery['account']['byId'] {
return {
@@ -184,7 +210,7 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
mockGetUsageForOverageWarningAsync.mockResolvedValue(
createMockAccountUsage({
buildPlanMetrics: [createMockPlanMetric({ value: 105 })],
overageMetrics: [{ __typename: 'EstimatedOverageAndCost', id: 'o1', value: 5 }],
overageMetrics: [createMockBuildOverage({ id: 'o1', value: 5 })],
totalCost: 0,
})
);
@@ -207,7 +233,7 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
createMockAccountUsage({
subscriptionName: 'Starter',
buildPlanMetrics: [createMockPlanMetric({ value: 110 })],
overageMetrics: [{ __typename: 'EstimatedOverageAndCost', id: 'o1', value: 10 }],
overageMetrics: [createMockBuildOverage({ id: 'o1', value: 10 })],
totalCost: 1500,
})
);
@@ -242,6 +268,32 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
expect(mockWarn).not.toHaveBeenCalled();
});
it('counts only per-worker build overage rows, ignoring rows without build metadata', async () => {
mockGetUsageForOverageWarningAsync.mockResolvedValue(
createMockAccountUsage({
subscriptionName: 'Starter',
buildPlanMetrics: [createMockPlanMetric({ value: 103 })],
overageMetrics: [
createMockBuildOverage({ id: 'real', value: 3 }),
// A rollup/summary row with no build metadata - must not be summed in.
{ __typename: 'EstimatedOverageAndCost', id: 'rollup', value: 50 },
],
totalCost: 450,
})
);
await maybeWarnAboutUsageOveragesAsync({
graphqlClient: mockGraphqlClient,
accountId: 'account-id',
});
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining(
"You've used 3 builds beyond your included credits this billing period ($4.50 in overages so far)."
)
);
});
it('does not warn a Free plan below its limit even if the usage API reports an overage', async () => {
// Regression: free users with only a handful of builds were told they'd reached
// their limit because a nonzero overage row was treated as billable usage.
@@ -249,7 +301,7 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
createMockAccountUsage({
subscriptionName: 'Free',
buildPlanMetrics: [createMockPlanMetric({ value: 6, limit: 30 })],
overageMetrics: [{ __typename: 'EstimatedOverageAndCost', id: 'o1', value: 5 }],
overageMetrics: [createMockBuildOverage({ id: 'o1', value: 5 })],
totalCost: 0,
})
);
@@ -33,7 +33,12 @@ export async function maybeWarnAboutUsageOveragesAsync({
return;
}
const overageCount = buildMetrics.overageMetrics.reduce((sum, o) => sum + o.value, 0);
// Count only genuine per-worker build overage rows. `overageMetrics` can also
// contain rows without build metadata (e.g. rollups), which must not be summed
// in — otherwise the displayed "N builds beyond your credits" count is inflated.
const overageCount = buildMetrics.overageMetrics
.filter(o => o.metadata?.billingResourceClass && o.metadata?.platform)
.reduce((sum, o) => sum + o.value, 0);
const overageCostCents = buildMetrics.totalCost;
const hasFreePlan = subscription.name === 'Free';
const tier = classifyUsageTier({
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/eas-json",
"version": "21.0.1",
"version": "21.0.2",
"description": "A library for interacting with eas.json",
"homepage": "https://github.com/expo/eas-cli",
"bugs": "https://github.com/expo/eas-cli/issues",
@@ -32,7 +32,7 @@
},
"dependencies": {
"@babel/code-frame": "7.23.5",
"@expo/eas-build-job": "21.0.1",
"@expo/eas-build-job": "21.0.2",
"chalk": "4.1.2",
"env-string": "1.0.1",
"fs-extra": "11.2.0",
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "eas-cli-local-build-plugin",
"version": "21.0.1",
"version": "21.0.2",
"description": "Tool for running EAS compatible builds on a local machine.",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "BUSL-1.1",
@@ -26,8 +26,8 @@
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
},
"dependencies": {
"@expo/build-tools": "21.0.1",
"@expo/eas-build-job": "21.0.1",
"@expo/build-tools": "21.0.2",
"@expo/eas-build-job": "21.0.2",
"@expo/logger": "21.0.0",
"@expo/spawn-async": "^1.7.2",
"@expo/turtle-spawn": "21.0.0",
@@ -36,7 +36,7 @@
"env-paths": "^2.2.1",
"fs-extra": "^11.2.0",
"joi": "^17.13.1",
"lodash": "^4.17.21",
"lodash": "^4.18.1",
"nullthrows": "^1.1.1",
"semver": "^7.6.2",
"tar": "7.5.19",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/steps",
"version": "21.0.1",
"version": "21.0.2",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "BUSL-1.1",
"author": "Expo <[email protected]>",
@@ -27,7 +27,7 @@
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
},
"dependencies": {
"@expo/eas-build-job": "21.0.1",
"@expo/eas-build-job": "21.0.2",
"@expo/logger": "21.0.0",
"@expo/spawn-async": "^1.7.2",
"arg": "^5.0.2",
+3 -29
View File
@@ -32,9 +32,6 @@ import {
export class StepsConfigParser extends AbstractConfigParser {
private readonly steps: Step[];
private readonly hooks: Hooks;
// Anchors present in the job's own steps, collected during construction.
// Used to warn about payload hook keys whose anchor never occurred.
private readonly encounteredHookAnchors = new Set<HookAnchorId>();
constructor(
ctx: BuildStepGlobalContext,
@@ -105,7 +102,6 @@ export class StepsConfigParser extends AbstractConfigParser {
if (anchorId === undefined) {
continue;
}
this.encounteredHookAnchors.add(anchorId);
const anchorHooks = this.constructAnchorHooks(anchorId, validatedHooks, {
buildFunctionById,
buildFunctionGroupById,
@@ -122,7 +118,6 @@ export class StepsConfigParser extends AbstractConfigParser {
buildSteps.push(this.createBuildStepFromNonGroupStepConfig(stepConfig, buildFunctionById));
continue;
}
this.encounteredHookAnchors.add(anchorId);
const maps = { buildFunctionById, buildFunctionGroupById };
const before = this.constructHookSideEntries(anchorId, 'before', validatedHooks, maps);
const anchorStep = this.createBuildStepFromNonGroupStepConfig(stepConfig, buildFunctionById);
@@ -133,9 +128,6 @@ export class StepsConfigParser extends AbstractConfigParser {
}
}
// After construction: group expansions also record encountered anchors.
this.warnAboutUnmatchedHookKeys();
return {
buildSteps,
buildFunctionById,
@@ -146,10 +138,9 @@ export class StepsConfigParser extends AbstractConfigParser {
private validateHooks(): Record<string, Step[]> {
const validatedHooks: Record<string, Step[]> = {};
for (const [hookKey, hookSteps] of Object.entries(this.hooks)) {
// Keys that don't name a registered anchor are fully inert — never
// validated, never constructed, reported by warnAboutUnmatchedHookKeys.
// A worker must not fail on hook keys newer than itself, even when their
// steps reference functions it doesn't have yet.
// A worker must not fail on a hook key newer than itself, so unregistered
// keys skip validation entirely (their steps may reference functions this
// worker lacks).
if (parseHookKey(hookKey) === null) {
continue;
}
@@ -223,23 +214,6 @@ export class StepsConfigParser extends AbstractConfigParser {
return constructHookEntriesFromValidatedSteps(this.ctx, hookSteps, maps);
}
private warnAboutUnmatchedHookKeys(): void {
for (const hookKey of Object.keys(this.hooks)) {
const parsedHookKey = parseHookKey(hookKey);
if (parsedHookKey === null) {
this.ctx.baseLogger.warn(
{ hookKey },
`Unknown hook key "${hookKey}" in the job payload; its steps did not run.`
);
} else if (!this.encounteredHookAnchors.has(parsedHookKey.anchorId)) {
this.ctx.baseLogger.warn(
{ hookKey, hookAnchor: parsedHookKey.anchorId },
`Hook key "${hookKey}" did not match any step in this job (anchor "${parsedHookKey.anchorId}" never occurred); its steps did not run.`
);
}
}
}
private createBuildStepFromNonGroupStepConfig(
stepConfig: Step,
buildFunctionById: BuildFunctionById
@@ -468,7 +468,7 @@ describe('StepsConfigParser hooks with function groups', () => {
externalFunctionGroups: [createGroup()],
});
// The group expands normally; the stamp never matches an anchor, so the
// submit hook stays unmatched (warned, not run).
// submit hook stays unmatched (ignored, not run).
expect(workflow.buildSteps).toHaveLength(2);
expect(workflow.hooksByAnchorStep.size).toBe(0);
});
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/template-file",
"version": "21.0.0",
"version": "21.0.2",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "BUSL-1.1",
"author": "Expo <[email protected]>",
@@ -25,7 +25,7 @@
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
},
"dependencies": {
"lodash": "^4.17.21"
"lodash": "^4.18.1"
},
"devDependencies": {
"@types/jest": "^29.5.12",
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/worker",
"version": "21.0.1",
"version": "21.0.2",
"private": true,
"scripts": {
"start": "yarn watch",
@@ -19,17 +19,17 @@
},
"dependencies": {
"@babel/core": "7.24.7",
"@expo/build-tools": "21.0.1",
"@expo/build-tools": "21.0.2",
"@expo/config-plugins": "55.0.7",
"@expo/config-types": "55.0.5",
"@expo/downloader": "21.0.0",
"@expo/eas-build-job": "21.0.1",
"@expo/eas-build-job": "21.0.2",
"@expo/logger": "21.0.0",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.2",
"@expo/steps": "21.0.1",
"@expo/template-file": "21.0.0",
"@expo/steps": "21.0.2",
"@expo/template-file": "21.0.2",
"@expo/turtle-spawn": "21.0.0",
"@hapi/boom": "10.0.1",
"check-disk-space": "3.4.0",
@@ -39,7 +39,7 @@
"koa": "3.1.2",
"koa-body": "7.0.1",
"koa-router": "14.0.0",
"lodash": "4.17.21",
"lodash": "4.18.1",
"micromatch": "4.0.8",
"node-fetch": "2.7.0",
"node-os-utils": "1.3.7",
@@ -48,7 +48,7 @@
"tar": "7.5.19",
"uuid": "3.4.0",
"uuidv7": "1.1.0",
"ws": "8.18.0",
"ws": "8.21.1",
"xml2js": "0.6.2",
"zod": "4.1.8"
},
+62 -1
View File
@@ -1,9 +1,10 @@
import { RuntimeSettings } from '@expo/build-tools';
import { Platform, Workflow } from '@expo/eas-build-job';
import os from 'os';
import v8 from 'v8';
import config from '../config';
import { getBuildEnv, getGradleMemoryOptions } from '../env';
import { getBuildEnv, getGradleMemoryOptions, getNodeMemoryOptions } from '../env';
describe(getBuildEnv.name, () => {
const originalSentryDsn = config.sentry.dsn;
@@ -19,6 +20,7 @@ describe(getBuildEnv.name, () => {
EAS_BUILD_MAVEN_CACHE_URL: process.env.EAS_BUILD_MAVEN_CACHE_URL,
EAS_BUILD_COCOAPODS_CACHE_URL: process.env.EAS_BUILD_COCOAPODS_CACHE_URL,
NPM_CONFIG_REGISTRY: process.env.NPM_CONFIG_REGISTRY,
NODE_OPTIONS: process.env.NODE_OPTIONS,
};
beforeEach(() => {
@@ -28,6 +30,7 @@ describe(getBuildEnv.name, () => {
delete process.env.EAS_BUILD_MAVEN_CACHE_URL;
delete process.env.EAS_BUILD_COCOAPODS_CACHE_URL;
delete process.env.NPM_CONFIG_REGISTRY;
delete process.env.NODE_OPTIONS;
jest.spyOn(RuntimeSettings, 'getNpmCacheUrl').mockReturnValue(null);
jest.spyOn(RuntimeSettings, 'getNodeJsCacheUrl').mockReturnValue(null);
jest.spyOn(RuntimeSettings, 'getMavenCacheUrl').mockReturnValue(null);
@@ -47,6 +50,7 @@ describe(getBuildEnv.name, () => {
restoreEnv('EAS_BUILD_MAVEN_CACHE_URL', originalCacheUrls.EAS_BUILD_MAVEN_CACHE_URL);
restoreEnv('EAS_BUILD_COCOAPODS_CACHE_URL', originalCacheUrls.EAS_BUILD_COCOAPODS_CACHE_URL);
restoreEnv('NPM_CONFIG_REGISTRY', originalCacheUrls.NPM_CONFIG_REGISTRY);
restoreEnv('NODE_OPTIONS', originalCacheUrls.NODE_OPTIONS);
mockProcessPlatform(originalPlatform);
jest.restoreAllMocks();
});
@@ -299,6 +303,63 @@ describe(getBuildEnv.name, () => {
maxHeapSize: '1g',
});
});
it('increases the Node.js heap limit on workers with enough memory', () => {
jest.spyOn(process, 'constrainedMemory').mockReturnValue(6 * 1024 ** 3);
jest.spyOn(v8, 'getHeapStatistics').mockReturnValue({
heap_size_limit: 2 * 1024 ** 3,
} as ReturnType<typeof v8.getHeapStatistics>);
expect(getNodeMemoryOptions()).toBe('--max-old-space-size=3072');
});
it('does not increase the Node.js heap limit on workers with less than 6 GiB of memory', () => {
jest.spyOn(process, 'constrainedMemory').mockReturnValue(6 * 1024 ** 3 - 1);
jest.spyOn(v8, 'getHeapStatistics').mockReturnValue({
heap_size_limit: 2 * 1024 ** 3,
} as ReturnType<typeof v8.getHeapStatistics>);
expect(getNodeMemoryOptions()).toBeUndefined();
});
it('does not increase an existing Node.js heap limit of at least 3 GiB', () => {
jest.spyOn(process, 'constrainedMemory').mockReturnValue(6 * 1024 ** 3);
jest.spyOn(v8, 'getHeapStatistics').mockReturnValue({
heap_size_limit: 3 * 1024 ** 3,
} as ReturnType<typeof v8.getHeapStatistics>);
expect(getNodeMemoryOptions()).toBeUndefined();
});
it('keeps Node.js options inherited from the worker environment in override position', () => {
process.env.NODE_OPTIONS = '--max-old-space-size=2048 --enable-source-maps';
jest.spyOn(process, 'constrainedMemory').mockReturnValue(6 * 1024 ** 3);
jest.spyOn(v8, 'getHeapStatistics').mockReturnValue({
heap_size_limit: 2 * 1024 ** 3,
} as ReturnType<typeof v8.getHeapStatistics>);
const env = getBuildEnv({
job: {
platform: Platform.ANDROID,
type: Workflow.MANAGED,
builderEnvironment: {
env: {},
},
username: 'expo-user',
} as any,
projectId: 'project-id',
metadata: {
buildProfile: 'production',
gitCommitHash: 'abc123',
username: 'expo-user',
} as any,
buildId: 'build-id',
});
expect(env.NODE_OPTIONS).toBe(
'--max-old-space-size=3072 --max-old-space-size=2048 --enable-source-maps'
);
});
});
function mockProcessPlatform(platform: NodeJS.Platform): void {
+20
View File
@@ -4,12 +4,15 @@ import { spawnSync } from 'child_process';
import micromatch from 'micromatch';
import os from 'os';
import path from 'path';
import v8 from 'v8';
import config from './config';
import { Environment } from './constants';
import { androidImagesWithJavaVersionLowerThen11 } from './external/turtle';
import { getAccessedEnvs } from './utils/env';
const GIB_IN_BYTES = 1024 ** 3;
// keep in sync with local-build-plugin env vars
// see packages/local-build-plugin/src/build.ts
export function getBuildEnv({
@@ -50,6 +53,9 @@ export function getBuildEnv({
setEnv(env, 'EAS_BUILD_WORKINGDIR', path.join(config.workingdir, 'build'));
setEnv(env, 'EAS_BUILD_PROJECT_ID', projectId);
// Keep inherited options last so they override singleton options supplied by the worker.
setEnv(env, 'NODE_OPTIONS', [getNodeMemoryOptions(), env.NODE_OPTIONS].filter(Boolean).join(' '));
const runnerPlatform = job.platform;
if (runnerPlatform === Platform.IOS) {
setEnv(env, 'EAS_BUILD_COCOAPODS_CACHE_URL', cocoapodsCacheUrl);
@@ -179,3 +185,17 @@ export function getGradleMemoryOptions() {
maxHeapSize: `${Math.max(1, Math.round(totalMemoryGb / 4))}g`,
};
}
export function getNodeMemoryOptions(): string | undefined {
// constrainedMemory() returns 0 when there is no container or process memory limit.
const memoryLimit = process.constrainedMemory() || os.totalmem();
const heapLimit = v8.getHeapStatistics().heap_size_limit;
// V8 defaults to a roughly 2 GiB heap on smaller workers. Raise the ceiling for build child
// processes only when the worker has enough memory and Node has not already selected a larger one.
if (memoryLimit < 6 * GIB_IN_BYTES || heapLimit >= 3 * GIB_IN_BYTES) {
return undefined;
}
return '--max-old-space-size=3072';
}
+1 -1
View File
@@ -18,7 +18,7 @@
"chalk": "5.2.0",
"dateformat": "4.5.1",
"fs-extra": "11.2.0",
"lodash-es": "4.17.21",
"lodash-es": "4.18.1",
"marked": "3.0.4",
"semver": "7.5.4",
"tslib": "2.6.2"
+5 -5
View File
@@ -213,10 +213,10 @@ __metadata:
languageName: node
linkType: hard
"lodash-es@npm:4.17.21":
version: 4.17.21
resolution: "lodash-es@npm:4.17.21"
checksum: 10c0/fb407355f7e6cd523a9383e76e6b455321f0f153a6c9625e21a8827d10c54c2a2341bd2ae8d034358b60e07325e1330c14c224ff582d04612a46a4f0479ff2f2
"lodash-es@npm:4.18.1":
version: 4.18.1
resolution: "lodash-es@npm:4.18.1"
checksum: 10c0/35d4dcf87ef07f8d090f409447575800108057e360b445f590d0d25d09e3d1e33a163d2fc100d4d072b0f901d5e2fc533cd7c4bfd8eeb38a06abec693823c8b8
languageName: node
linkType: hard
@@ -257,7 +257,7 @@ __metadata:
chalk: "npm:5.2.0"
dateformat: "npm:4.5.1"
fs-extra: "npm:11.2.0"
lodash-es: "npm:4.17.21"
lodash-es: "npm:4.18.1"
marked: "npm:3.0.4"
semver: "npm:7.5.4"
ts-node: "npm:10.9.2"
Generated
+1287 -2896
View File
File diff suppressed because it is too large. Load diff