Compare commits

...
Author SHA1 Message Date
Douglas Lowder b3bba882b1 Add mutation for getting workflows from git ref 2025-11-20 16:30:50 -08:00
Expo CI 4476c87962 update CHANGELOG.md 2025-11-20 19:58:01 +00:00
Expo CI 18ecd0844a v16.28.0 2025-11-20 19:39:20 +00:00
Colin MacKenzie cc3e7cdbe4 remove package json changes (#3263) 2025-11-20 13:36:15 -06:00
Colin MacKenzie aa7fecb9fe [eas-cli] Fix usage warning bug in eas:build (#3260)
* fix overages bug

* stub changelog

* update pr number

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

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

# How

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

# Test Plan

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

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

* update changelog

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

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

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

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

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

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

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

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

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

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

# How

Fixed the argument name.

# Test Plan

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

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

* remove hidden flag

* update readme

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

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

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

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

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

ARGUMENTS
  WORKFLOW_RUN_ID  A workflow run ID.

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

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

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

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

# How

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

# Test Plan

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

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

* cleanup

* clean up query

* show warning when builds are close to threshold

* cleaned up query

* tweak test

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

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

# How

Remove this step from the workflow.

# Test Plan

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

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

# Test Plan

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

* more feedback

* add changelog entry

* prompt for name

* update agents.md

* fix test

* add PR to changelog

* Update CHANGELOG.md

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

* remove dev warning

---------

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

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

# How

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

# Test Plan

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

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

# How

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

# Test Plan

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

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

# How

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

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

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

Other changes:

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

# Test Plan

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

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

# How

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

# Test Plan

- Tested with various workflow runs, including ones with custom, build, repack, and update jobs.
- CI should pass.
2025-10-16 14:05:26 -07:00
Expo CI 4bb1c784d8 update CHANGELOG.md 2025-10-16 15:42:51 +00:00
70 changed files with 6202 additions and 869 deletions

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
on:
workflow_dispatch:
inputs:
staging_only:
type: boolean
required: true
default: false
description: "Only move the latest-eas-build-staging tag, not the latest-eas-build tag"
version:
type: string
required: true
description: "Version number to move the tag to"
dry_run:
type: boolean
required: true
default: true
description: "Run the workflow in dry run mode, without making any NPM changes"
name: Move NPM tags used by EAS builds and workflows
jobs:
move-tag:
name: Move NPM tags used by EAS builds and workflows
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.EXPO_BOT_PAT }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
INPUT_VERSION: ${{ github.event.inputs.version }}
INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }}
INPUT_STAGING_ONLY: ${{ github.event.inputs.staging_only }}
steps:
- uses: actions/setup-node@v2
with:
registry-url: "https://registry.npmjs.org/"
scope: "expo"
node-version: 22
- name: Resolve version (fails if the version does not exist)
run: |
set -o pipefail
npm pack --dry-run eas-cli@$INPUT_VERSION
- name: Add latest-eas-build-staging tag
run: |
cmd="npm dist-tag add eas-cli@$INPUT_VERSION latest-eas-build-staging"
if [[ "$INPUT_DRY_RUN" == "true" ]]; then
echo "Running in dry run mode, skipping version check"
echo "cmd = $cmd"
exit 0
fi
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
$cmd
- name: Add latest-eas-build tag
run: |
cmd="npm dist-tag add eas-cli@$INPUT_VERSION latest-eas-build"
if [[ "$INPUT_DRY_RUN" == "true" ]]; then
echo "Running in dry run mode, skipping version check"
echo "cmd = $cmd"
exit 0
fi
if [[ "$INPUT_STAGING_ONLY" == "true" ]]; then
echo "Only moving the staging tag, skipping the production tag"
exit 0
fi
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
$cmd
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: ['18', '20', '22']
node: ['20', '22']
include:
- node: '20'
coverage: true
+83 -1
View File
@@ -10,10 +10,92 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- Multiple bugfixes and small features for the `eas:new` command: handle project name and directory generation more smoothly; support `bun` as a package manager; add `--package-manager` flag with a default to `npm`; ensure workflow files are in the correct location after build; update the generated project's README with the provided package manager; ensure `app.json`'s `scheme` is valid; silence lots of noisy output. ([#3205](https://github.com/expo/eas-cli/pull/3205) by [@mackenco](https://github.com/mackenco))
### 🧹 Chores
## [16.28.0](https://github.com/expo/eas-cli/releases/tag/v16.28.0) - 2025-11-20
### 🎉 New features
- `eas:new` sorts organization accounts first in account selection. ([#3256](https://github.com/expo/eas-cli/pull/3256) by [@mackenco](https://github.com/mackenco))
### 🐛 Bug fixes
- Fix `eas:build` bug where overages are checked before config is ready. ([#3260](https://github.com/expo/eas-cli/pull/3260) by [@mackenco](https://github.com/mackenco))
### 🧹 Chores
- Delete branch in background. ([#3258](https://github.com/expo/eas-cli/pull/3258) by [@quinlanj](https://github.com/quinlanj))
## [16.27.0](https://github.com/expo/eas-cli/releases/tag/v16.27.0) - 2025-11-13
### 🎉 New features
- Kick off update patch generation if enabled - this is feature is in preview, don't use it for prod yet please. ([#3250](https://github.com/expo/eas-cli/pull/3250) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Fix workflow:status when run ID passed in. ([#3253](https://github.com/expo/eas-cli/pull/3253) by [@douglowder](https://github.com/douglowder))
- Fix updating file env vars with `env:update --non-interactive`. ([#3249](https://github.com/expo/eas-cli/pull/3249) by [@kadikraman](https://github.com/kadikraman))
### 🧹 Chores
- Fix yarn watch. ([#3251](https://github.com/expo/eas-cli/pull/3251) by [@quinlanj](https://github.com/quinlanj))
- Remove the **node_modules** installation requirement for `workflow:run` command. ([#3254](https://github.com/expo/eas-cli/pull/3254) by [@kudo](https://github.com/kudo))
## [16.26.0](https://github.com/expo/eas-cli/releases/tag/v16.26.0) - 2025-10-29
### 🎉 New features
- New command `workflow:status`. ([#3242](https://github.com/expo/eas-cli/pull/3242) by [@douglowder](https://github.com/douglowder))
### 🧹 Chores
- `eas:new` cleanup (remove `hidden` tag, fix README error, output cleanup). ([#3243](https://github.com/expo/eas-cli/pull/3243) by [@mackenco](https://github.com/mackenco))
## [16.25.1](https://github.com/expo/eas-cli/releases/tag/v16.25.1) - 2025-10-28
### 🧹 Chores
- Remove preview label from EAS Hosting commands ([#3241](https://github.com/expo/eas-cli/pull/3241) by [@kitten](https://github.com/kitten))
## [16.25.0](https://github.com/expo/eas-cli/releases/tag/v16.25.0) - 2025-10-27
### 🎉 New features
- `eas:new` command changes (project name prompt, output cleanup, additional project files, additional prompts). ([#3235](https://github.com/expo/eas-cli/pull/3235) by [@mackenco](https://github.com/mackenco))
- `eas:build` command displays a warning when account is near it's build limit. ([#3236](https://github.com/expo/eas-cli/pull/3236) by [@mackenco](https://github.com/mackenco))
### 🐛 Bug fixes
- workflow:run can use --wait and --json together. ([#3239](https://github.com/expo/eas-cli/pull/3239) by [@douglowder](https://github.com/douglowder))
- Add missing env for local builds with latest plugin. ([#3237](https://github.com/expo/eas-cli/pull/3237) by [@douglowder](https://github.com/douglowder))
### 🧹 Chores
- Document the default values for `track` and `releaseStatus`. ([#3229](https://github.com/expo/eas-cli/pull/3229) by [@kadikraman](https://github.com/kadikraman))
## [16.24.1](https://github.com/expo/eas-cli/releases/tag/v16.24.1) - 2025-10-19
### 🐛 Bug fixes
- Revert change in ora version. ([#3231](https://github.com/expo/eas-cli/pull/3231) by [@douglowder](https://github.com/douglowder))
## [16.24.0](https://github.com/expo/eas-cli/releases/tag/v16.24.0) - 2025-10-19
### 🎉 New features
- workflow:run --wait shows periodic job and step status updates. ([#3227](https://github.com/expo/eas-cli/pull/3227) by [@douglowder](https://github.com/douglowder))
### 🐛 Bug fixes
- workflow:logs should show logs from build and repack jobs. ([#3226](https://github.com/expo/eas-cli/pull/3226) by [@douglowder](https://github.com/douglowder))
## [16.23.1](https://github.com/expo/eas-cli/releases/tag/v16.23.1) - 2025-10-16
### 🐛 Bug fixes
- Multiple bugfixes and small features for the `eas:new` command: handle project name and directory generation more smoothly; support `bun` as a package manager; add `--package-manager` flag with a default to `npm`; ensure workflow files are in the correct location after build; update the generated project's README with the provided package manager; ensure `app.json`'s `scheme` is valid; silence lots of noisy output. ([#3205](https://github.com/expo/eas-cli/pull/3205) by [@mackenco](https://github.com/mackenco))
## [16.23.0](https://github.com/expo/eas-cli/releases/tag/v16.23.0) - 2025-10-14
### 🎉 New features
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "16.23.1",
"version": "16.28.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+162 -87
View File
@@ -120,10 +120,12 @@ eas --help COMMAND
* [`eas metadata:lint`](#eas-metadatalint)
* [`eas metadata:pull`](#eas-metadatapull)
* [`eas metadata:push`](#eas-metadatapush)
* [`eas new [PATH]`](#eas-new-path)
* [`eas onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-onboarding-target_project_directory)
* [`eas open`](#eas-open)
* [`eas project:info`](#eas-projectinfo)
* [`eas project:init`](#eas-projectinit)
* [`eas project:new [PATH]`](#eas-projectnew-path)
* [`eas project:onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-projectonboarding-target_project_directory)
* [`eas submit`](#eas-submit)
* [`eas update`](#eas-update)
@@ -152,6 +154,7 @@ eas --help COMMAND
* [`eas workflow:logs [ID]`](#eas-workflowlogs-id)
* [`eas workflow:run [FILE]`](#eas-workflowrun-file)
* [`eas workflow:runs`](#eas-workflowruns)
* [`eas workflow:status [WORKFLOW_RUN_ID]`](#eas-workflowstatus-workflow_run_id)
* [`eas workflow:validate PATH`](#eas-workflowvalidate-path)
* [`eas workflow:view [ID]`](#eas-workflowview-id)
@@ -173,7 +176,7 @@ ALIASES
$ eas login
```
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -190,7 +193,7 @@ ALIASES
$ eas logout
```
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -207,7 +210,7 @@ ALIASES
$ eas whoami
```
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -221,7 +224,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v16.23.1/packages/eas-cli/src/commands/analytics.ts)_
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v16.28.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -273,7 +276,7 @@ DESCRIPTION
create a branch
```
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -294,7 +297,7 @@ DESCRIPTION
delete a branch
```
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -314,7 +317,7 @@ DESCRIPTION
list all branches
```
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -334,7 +337,7 @@ DESCRIPTION
rename a branch
```
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -357,7 +360,7 @@ DESCRIPTION
view a branch
```
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -398,7 +401,7 @@ DESCRIPTION
start a build
```
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -417,7 +420,7 @@ DESCRIPTION
cancel a build
```
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -434,7 +437,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:delete [BUILD_ID]`
@@ -453,7 +456,7 @@ DESCRIPTION
delete a build
```
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/build/delete.ts)_
## `eas build:dev`
@@ -473,7 +476,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/build/dev.ts)_
## `eas build:download`
@@ -494,7 +497,7 @@ DESCRIPTION
download simulator/emulator builds for a given fingerprint hash
```
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/build/download.ts)_
## `eas build:inspect`
@@ -529,7 +532,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -581,7 +584,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -610,7 +613,7 @@ DESCRIPTION
re-sign a build archive
```
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -636,7 +639,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -690,7 +693,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/build/version/get.ts)_
## `eas build:version:set`
@@ -709,7 +712,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -728,7 +731,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -745,7 +748,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -766,7 +769,7 @@ DESCRIPTION
create a channel
```
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:delete [NAME]`
@@ -787,7 +790,7 @@ DESCRIPTION
Delete a channel
```
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/channel/delete.ts)_
## `eas channel:edit [NAME]`
@@ -809,7 +812,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -829,7 +832,7 @@ DESCRIPTION
list all channels
```
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:pause [NAME]`
@@ -851,7 +854,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/channel/pause.ts)_
## `eas channel:resume [NAME]`
@@ -873,7 +876,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/channel/resume.ts)_
## `eas channel:rollout [CHANNEL]`
@@ -906,7 +909,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -929,7 +932,7 @@ DESCRIPTION
view a channel
```
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -950,7 +953,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/v16.23.1/packages/eas-cli/src/commands/config.ts)_
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v16.28.0/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -967,7 +970,7 @@ DESCRIPTION
manage credentials
```
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/credentials/index.ts)_
## `eas credentials:configure-build`
@@ -985,7 +988,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
## `eas deploy [options]`
@@ -1013,7 +1016,7 @@ ALIASES
$ eas worker:deploy
```
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/deploy/index.ts)_
## `eas deploy:alias`
@@ -1038,7 +1041,7 @@ ALIASES
$ eas deploy:promote
```
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
## `eas deploy:alias:delete [ALIAS_NAME]`
@@ -1059,7 +1062,7 @@ ALIASES
$ eas worker:alias:delete
```
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
## `eas deploy:delete [DEPLOYMENT_ID]`
@@ -1080,7 +1083,7 @@ ALIASES
$ eas worker:delete
```
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/deploy/delete.ts)_
## `eas deploy:promote`
@@ -1117,7 +1120,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -1137,7 +1140,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -1158,7 +1161,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:rename`
@@ -1179,7 +1182,7 @@ DESCRIPTION
rename a registered device
```
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/device/rename.ts)_
## `eas device:view [UDID]`
@@ -1193,7 +1196,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -1207,7 +1210,7 @@ DESCRIPTION
display environment info
```
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v16.23.1/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v16.28.0/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas env:create [ENVIRONMENT]`
@@ -1237,7 +1240,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/env/create.ts)_
## `eas env:delete [ENVIRONMENT]`
@@ -1262,7 +1265,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/env/delete.ts)_
## `eas env:exec ENVIRONMENT BASH_COMMAND`
@@ -1284,7 +1287,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/env/exec.ts)_
## `eas env:get [ENVIRONMENT]`
@@ -1309,7 +1312,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/env/get.ts)_
## `eas env:list [ENVIRONMENT]`
@@ -1335,7 +1338,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/env/list.ts)_
## `eas env:pull [ENVIRONMENT]`
@@ -1357,7 +1360,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/env/pull.ts)_
## `eas env:push [ENVIRONMENT]`
@@ -1379,7 +1382,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/env/push.ts)_
## `eas env:update [ENVIRONMENT]`
@@ -1411,7 +1414,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/env/update.ts)_
## `eas fingerprint:compare [HASH1] [HASH2]`
@@ -1455,7 +1458,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
## `eas fingerprint:generate`
@@ -1485,7 +1488,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
## `eas help [COMMAND]`
@@ -1593,7 +1596,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -1610,7 +1613,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -1627,7 +1630,28 @@ 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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas new [PATH]`
Create a new project configured with Expo Application Services (EAS)
```
USAGE
$ eas new [PATH] [-p bun|npm|pnpm|yarn]
ARGUMENTS
PATH Path to create the project (defaults to current directory)
FLAGS
-p, --package-manager=(bun|npm|pnpm|yarn) [default: npm] Package manager to use for installing dependencies
DESCRIPTION
Create a new project configured with Expo Application Services (EAS)
ALIASES
$ eas new
```
## `eas onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1657,7 +1681,7 @@ DESCRIPTION
open the project page in a web browser
```
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v16.23.1/packages/eas-cli/src/commands/open.ts)_
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v16.28.0/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -1671,7 +1695,7 @@ DESCRIPTION
information about the current project
```
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -1694,7 +1718,30 @@ ALIASES
$ eas init
```
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/project/init.ts)_
## `eas project:new [PATH]`
Create a new project configured with Expo Application Services (EAS)
```
USAGE
$ eas project:new [PATH] [-p bun|npm|pnpm|yarn]
ARGUMENTS
PATH Path to create the project (defaults to current directory)
FLAGS
-p, --package-manager=(bun|npm|pnpm|yarn) [default: npm] Package manager to use for installing dependencies
DESCRIPTION
Create a new project configured with Expo Application Services (EAS)
ALIASES
$ eas new
```
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v16.28.0/packages/eas-cli/src/commands/project/new.ts)_
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1712,7 +1759,7 @@ ALIASES
$ eas onboarding
```
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v16.23.1/packages/eas-cli/src/commands/project/onboarding.ts)_
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v16.28.0/packages/eas-cli/src/commands/project/onboarding.ts)_
## `eas submit`
@@ -1747,7 +1794,7 @@ ALIASES
$ eas build:submit
```
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v16.23.1/packages/eas-cli/src/commands/submit.ts)_
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v16.28.0/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1787,7 +1834,7 @@ DESCRIPTION
publish an update group
```
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1807,7 +1854,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1828,7 +1875,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:edit [GROUPID]`
@@ -1852,7 +1899,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/update/edit.ts)_
## `eas update:list`
@@ -1874,7 +1921,7 @@ DESCRIPTION
view the recent updates
```
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
@@ -1910,7 +1957,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:revert-update-rollout`
@@ -1937,7 +1984,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
## `eas update:roll-back-to-embedded`
@@ -1965,7 +2012,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
## `eas update:rollback`
@@ -1986,7 +2033,7 @@ DESCRIPTION
instead execute "eas update:republish" or "eas update:roll-back-to-embedded".
```
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/update/rollback.ts)_
## `eas update:view GROUPID`
@@ -2006,7 +2053,7 @@ DESCRIPTION
update group details
```
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/update/view.ts)_
## `eas upload`
@@ -2027,7 +2074,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/v16.23.1/packages/eas-cli/src/commands/upload.ts)_
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v16.28.0/packages/eas-cli/src/commands/upload.ts)_
## `eas webhook:create`
@@ -2048,7 +2095,7 @@ DESCRIPTION
create a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -2068,7 +2115,7 @@ DESCRIPTION
delete a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -2086,7 +2133,7 @@ DESCRIPTION
list webhooks
```
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -2108,7 +2155,7 @@ DESCRIPTION
update a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -2125,7 +2172,7 @@ DESCRIPTION
view a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
@@ -2245,7 +2292,7 @@ DESCRIPTION
to cancel.
```
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v16.23.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/v16.28.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
## `eas workflow:create [NAME]`
@@ -2265,7 +2312,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/workflow/create.ts)_
## `eas workflow:logs [ID]`
@@ -2289,7 +2336,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/workflow/logs.ts)_
## `eas workflow:run [FILE]`
@@ -2307,7 +2354,7 @@ FLAGS
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--ref=<value> Git reference to run the workflow on
--[no-]wait Wait for workflow run to complete
--[no-]wait Wait for workflow run to complete. Defaults to false.
DESCRIPTION
run an EAS workflow. The entire local project directory will be packaged and uploaded to EAS servers for the workflow
@@ -2324,12 +2371,12 @@ FLAG DESCRIPTIONS
When this flag is used, the local project is not uploaded; instead, the workflow is run from the exact state of the
project at the chosen reference.
--[no-]wait Wait for workflow run to complete
--[no-]wait Wait for workflow run to complete. Defaults to false.
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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/workflow/run.ts)_
## `eas workflow:runs`
@@ -2354,7 +2401,35 @@ 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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/workflow/runs.ts)_
## `eas workflow:status [WORKFLOW_RUN_ID]`
show the status of an existing workflow run. If no run ID is provided, you will be prompted to select from recent workflow runs for the current project.
```
USAGE
$ eas workflow:status [WORKFLOW_RUN_ID] [--non-interactive] [--wait] [--json]
ARGUMENTS
WORKFLOW_RUN_ID A workflow run ID.
FLAGS
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--[no-]wait Wait for workflow run to complete. Defaults to false.
DESCRIPTION
show the status of an existing workflow run. If no run ID is provided, you will be prompted to select from recent
workflow runs for the current project.
FLAG DESCRIPTIONS
--[no-]wait Wait for workflow run to complete. Defaults to false.
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
```
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v16.28.0/packages/eas-cli/src/commands/workflow/status.ts)_
## `eas workflow:validate PATH`
@@ -2374,7 +2449,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/workflow/validate.ts)_
## `eas workflow:view [ID]`
@@ -2396,5 +2471,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/v16.23.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/v16.28.0/packages/eas-cli/src/commands/workflow/view.ts)_
<!-- commandsstop -->
+3087 -13
View File
File diff suppressed because it is too large. Load diff
+6 -5
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "16.23.1",
"version": "16.28.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -12,8 +12,8 @@
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "10.0.6",
"@expo/config-plugins": "9.0.12",
"@expo/eas-build-job": "1.0.237",
"@expo/eas-json": "16.23.0",
"@expo/eas-build-job": "1.0.243",
"@expo/eas-json": "16.25.0",
"@expo/env": "^1.0.0",
"@expo/json-file": "8.3.3",
"@expo/logger": "1.0.221",
@@ -226,11 +226,12 @@
"prepack": "yarn rebuild && node ./scripts/prepack.js",
"rebuild": "rimraf build && yarn build",
"pretarball-ci": "./scripts/pretarball-ci.sh",
"build": "tsc --project tsconfig.build.json && yarn copy-new-templates",
"build": "yarn typecheck-for-build && yarn copy-new-templates",
"build-allow-unused": "tsc --project tsconfig.allowUnused.json",
"watch": "yarn build --watch --preserveWatchOutput",
"watch": "yarn typecheck-for-build --watch --preserveWatchOutput",
"watch-allow-unused": "yarn build-allow-unused --watch --preserveWatchOutput",
"typecheck": "tsc",
"typecheck-for-build": "tsc --project tsconfig.build.json",
"test": "jest",
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
@@ -192,9 +192,16 @@ export function getMockWorkflowRunsFragment(
failures?: number;
pending?: number;
withJobs?: number;
withBuildJobs?: number;
}
): WorkflowRunFragment[] {
const { successes = 0, failures = 0, pending = 0, withJobs = 0 } = params ?? {};
const {
successes = 0,
failures = 0,
pending = 0,
withJobs = 0,
withBuildJobs = 0,
} = params ?? {};
const runs: any[] = [];
for (let i = 0; i < successes; i++) {
runs.push({
@@ -247,10 +254,13 @@ export function getMockWorkflowRunsFragment(
for (let i = 0; i < withJobs; i++) {
runs.push(getMockWorkflowRunWithJobsFragment());
}
for (let i = 0; i < withBuildJobs; i++) {
runs.push(getMockWorkflowRunWithBuildJobsFragment());
}
return runs;
}
export function getMockWorkflowJobFragment(
export function getMockWorkflowCustomJobFragment(
runId?: string
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']['jobs'][number] {
return {
@@ -258,7 +268,7 @@ export function getMockWorkflowJobFragment(
key: 'job1',
name: 'job1',
status: WorkflowJobStatus.Success,
type: WorkflowJobType.Build,
type: WorkflowJobType.Custom,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
outputs: {},
@@ -275,6 +285,18 @@ export function getMockWorkflowJobFragment(
};
}
export function getMockWorkflowBuildJobFragment(
runId?: string
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']['jobs'][number] {
return {
...getMockWorkflowCustomJobFragment(runId),
type: WorkflowJobType.Build,
outputs: {
build_id: 'build1',
},
};
}
export function getMockWorkflowRunWithJobsFragment(
runID?: string
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'] {
@@ -303,6 +325,38 @@ export function getMockWorkflowRunWithJobsFragment(
},
},
},
jobs: [getMockWorkflowJobFragment(id)],
jobs: [getMockWorkflowCustomJobFragment(id)],
};
}
export function getMockWorkflowRunWithBuildJobsFragment(
runID?: string
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'] {
const id = runID ?? 'build1';
return {
id,
status: WorkflowRunStatus.Failure,
triggerEventType: WorkflowRunTriggerEventType.Manual,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
errors: [],
workflow: {
id: 'build',
name: 'build',
fileName: 'build.yml',
app: {
id: mockProjectId,
__typename: 'App',
name: 'App',
ownerAccount: {
id: 'account-id',
name: 'account-name',
__typename: 'Account',
},
},
},
jobs: [getMockWorkflowBuildJobFragment(id)],
};
}
@@ -1,15 +1,20 @@
import {
getMockEmptyWorkflowRunsFragment,
getMockWorkflowJobFragment,
getMockWorkflowCustomJobFragment,
getMockWorkflowRunWithJobsFragment,
getMockWorkflowRunsFragment,
mockCommandContext,
mockProjectId,
mockTestCommand,
} from './utils';
import { fetchRawLogsForJobAsync } from '../../commandUtils/workflow/fetchLogs';
import {
fetchRawLogsForBuildJobAsync,
fetchRawLogsForCustomJobAsync,
} from '../../commandUtils/workflow/fetchLogs';
import WorkflowLogView from '../../commands/workflow/logs';
import { AppPlatform, BuildPriority, BuildStatus } from '../../graphql/generated';
import { AppQuery } from '../../graphql/queries/AppQuery';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { WorkflowJobQuery } from '../../graphql/queries/WorkflowJobQuery';
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
import Log from '../../log';
@@ -20,6 +25,7 @@ jest.mock('../../build/ios/version');
jest.mock('../../project/applicationIdentifier');
jest.mock('../../graphql/queries/AppVersionQuery');
jest.mock('../../graphql/queries/AppQuery');
jest.mock('../../graphql/queries/BuildQuery');
jest.mock('../../graphql/queries/WorkflowJobQuery');
jest.mock('../../graphql/queries/WorkflowRunQuery');
jest.mock('../../graphql/mutations/AppVersionMutation');
@@ -35,7 +41,7 @@ jest.mock('../../commandUtils/workflow/fetchLogs');
describe(WorkflowLogView, () => {
beforeEach(() => {
jest.mocked(WorkflowJobQuery.byIdAsync).mockResolvedValue(getMockWorkflowJobFragment());
jest.mocked(WorkflowJobQuery.byIdAsync).mockResolvedValue(getMockWorkflowCustomJobFragment());
jest
.mocked(WorkflowRunQuery.withJobsByIdAsync)
.mockResolvedValue(getMockWorkflowRunWithJobsFragment());
@@ -98,7 +104,7 @@ describe(WorkflowLogView, () => {
return { selectedStep: 'step1' };
}
});
jest.mocked(fetchRawLogsForJobAsync).mockResolvedValue(null);
jest.mocked(fetchRawLogsForCustomJobAsync).mockResolvedValue(null);
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
@@ -130,7 +136,64 @@ describe(WorkflowLogView, () => {
}
});
jest
.mocked(fetchRawLogsForJobAsync)
.mocked(fetchRawLogsForCustomJobAsync)
.mockResolvedValue(
'{"result":"test","marker":"test","buildStepDisplayName":"step1","buildStepInternalId":"step1","time":"2022-01-01T00:00:00.000Z","msg":"test"}'
);
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
undefined,
20
);
expect(promptAsync).toHaveBeenCalledTimes(3);
expect(Log.error).not.toHaveBeenCalled();
expect(Log.log).toHaveBeenCalledWith(' 2022-01-01T00:00:00.000Z test');
});
test('view build logs, passing in no parameters, runs found, jobs found, step selected, logs found', async () => {
const ctx = mockCommandContext(WorkflowLogView, {
projectId: mockProjectId,
});
const cmd = mockTestCommand(WorkflowLogView, [], ctx);
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockWorkflowRunsFragment({ withBuildJobs: 1 }));
jest.mocked(BuildQuery.byIdAsync).mockResolvedValue({
id: 'build1',
status: BuildStatus.Finished,
priority: BuildPriority.Normal,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
isForIosSimulator: false,
project: {
id: mockProjectId,
__typename: 'App',
name: 'App',
slug: 'app',
ownerAccount: {
id: 'account-id',
name: 'account-name',
__typename: 'Account',
},
},
platform: AppPlatform.Android,
logFiles: ['https://example.com/log1'],
});
let promptCalls = 0;
jest.mocked(promptAsync).mockImplementation(async () => {
if (promptCalls === 0) {
promptCalls++;
return { selectedRun: 'build1' };
} else if (promptCalls === 1) {
promptCalls++;
return { selectedJob: 0 };
} else {
return { selectedStep: 'step1' };
}
});
jest
.mocked(fetchRawLogsForBuildJobAsync)
.mockResolvedValue(
'{"result":"test","marker":"test","buildStepDisplayName":"step1","buildStepInternalId":"step1","time":"2022-01-01T00:00:00.000Z","msg":"test"}'
);
+39
View File
@@ -0,0 +1,39 @@
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../graphql/client';
import {
BackgroundJobReceiptDataFragment,
ScheduleBranchDeletionMutation,
ScheduleBranchDeletionMutationVariables,
} from '../graphql/generated';
import { BackgroundJobReceiptNode } from '../graphql/types/BackgroundJobReceipt';
export async function scheduleBranchDeletionAsync(
graphqlClient: ExpoGraphqlClient,
{
branchId,
}: {
branchId: string;
}
): Promise<BackgroundJobReceiptDataFragment> {
const result = await withErrorHandlingAsync(
graphqlClient
.mutation<ScheduleBranchDeletionMutation, ScheduleBranchDeletionMutationVariables>(
gql`
mutation ScheduleBranchDeletion($branchId: ID!) {
updateBranch {
scheduleUpdateBranchDeletion(branchId: $branchId) {
id
...BackgroundJobReceiptData
}
}
}
${BackgroundJobReceiptNode}
`,
{ branchId }
)
.toPromise()
);
return result.updateBranch.scheduleUpdateBranchDeletion;
}
@@ -72,6 +72,7 @@ function createMockBuildFragment({
updatedAt: new Date().toISOString(),
platform: AppPlatform.Android,
id: buildId ?? uuid(),
logFiles: [],
priority: BuildPriority.Normal,
project: {
__typename: 'App',
@@ -70,6 +70,7 @@ function createMockBuildFragment({
return {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
logFiles: [],
platform: AppPlatform.Android,
id: buildId ?? uuid(),
priority: BuildPriority.Normal,
+1
View File
@@ -33,6 +33,7 @@ export interface IosBuildContext {
}
export interface BuildContext<T extends Platform> {
accountId: string;
accountName: string;
easJsonCliConfig: EasJson['cli'];
buildProfile: BuildProfile<T>;
@@ -125,6 +125,7 @@ export async function createBuildContextAsync<T extends Platform>({
);
const commonContext: CommonContext<T> = {
accountId,
accountName: account.name,
buildProfile,
buildProfileName,
+4 -2
View File
@@ -1,13 +1,14 @@
import { Job, Metadata } from '@expo/eas-build-job';
import { Job, Metadata, version } from '@expo/eas-build-job';
import spawnAsync from '@expo/spawn-async';
import { ChildProcess } from 'child_process';
import semver from 'semver';
import { getExpoApiBaseUrl } from '../api';
import Log from '../log';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '1.0.171';
const PLUGIN_PACKAGE_VERSION = version; // should match version of @expo/eas-build-job
export enum LocalBuildMode {
/**
@@ -60,6 +61,7 @@ export async function runLocalBuildAsync(
...env,
...process.env,
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
__API_SERVER_URL: getExpoApiBaseUrl(),
...(options.skipCleanup || options.skipNativeBuild
? { EAS_LOCAL_BUILD_SKIP_CLEANUP: '1' }
: {}),
@@ -78,10 +78,12 @@ import { downloadAndMaybeExtractAppAsync } from '../utils/download';
import { truthy } from '../utils/expodash/filter';
import { printJsonOnlyOutput } from '../utils/json';
import { ProfileData, getProfilesAsync } from '../utils/profiles';
import { maybeWarnAboutUsageOveragesAsync } from '../utils/usage/checkForOverages';
import { Client } from '../vcs/vcs';
let metroConfigValidated = false;
let sdkVersionChecked = false;
let hasWarnedAboutUsageOverages = false;
export interface BuildFlags {
requestedPlatform: RequestedPlatform;
@@ -409,6 +411,15 @@ async function prepareAndStartBuildAsync({
env,
});
if (!hasWarnedAboutUsageOverages && !flags.localBuildOptions.localBuildMode) {
hasWarnedAboutUsageOverages = true;
Log.newLine();
await maybeWarnAboutUsageOveragesAsync({ graphqlClient, accountId: buildCtx.accountId });
if (!moreBuilds) {
Log.newLine();
}
}
if (moreBuilds) {
Log.newLine();
const appPlatform = toAppPlatform(buildProfile.platform);
@@ -1,4 +1,3 @@
import { LogSpy } from './testUtils';
import { canAccessRepositoryUsingSshAsync, runGitCloneAsync } from '../../../onboarding/git';
import { installDependenciesAsync } from '../../../onboarding/installDependencies';
import { runCommandAsync } from '../../../onboarding/runCommand';
@@ -11,23 +10,14 @@ import {
jest.mock('../../../onboarding/git');
jest.mock('../../../onboarding/runCommand');
jest.mock('../../../onboarding/installDependencies');
jest.mock('../../../ora');
jest.mock('fs-extra');
describe('commands', () => {
let logSpy: LogSpy;
beforeAll(() => {
logSpy = new LogSpy('log');
});
beforeEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
logSpy.restore();
});
describe('cloneTemplateAsync', () => {
it('should clone the template project with ssh', async () => {
const targetProjectDir = '/test/target-project';
@@ -40,13 +30,12 @@ describe('commands', () => {
const result = await cloneTemplateAsync(targetProjectDir);
logSpy.expectLogToContain(`📂 Cloning the project to ${targetProjectDir}`);
expect(runGitCloneAsync).toHaveBeenCalledWith({
githubUsername: 'expo',
githubRepositoryName: 'expo-template-default',
targetProjectDir,
cloneMethod: 'ssh',
showOutput: false,
});
expect(result).toBe(finalTargetProjectDir);
@@ -63,13 +52,12 @@ describe('commands', () => {
const result = await cloneTemplateAsync(targetProjectDir);
logSpy.expectLogToContain(`📂 Cloning the project to ${targetProjectDir}`);
expect(runGitCloneAsync).toHaveBeenCalledWith({
githubUsername: 'expo',
githubRepositoryName: 'expo-template-default',
targetProjectDir,
cloneMethod: 'https',
showOutput: false,
});
expect(result).toBe(finalTargetProjectDir);
@@ -96,6 +84,7 @@ describe('commands', () => {
command: 'npx',
args: ['expo', 'install', 'expo-updates'],
showOutput: false,
showSpinner: false,
});
expect(runCommandAsync).toHaveBeenCalledWith({
@@ -103,6 +92,7 @@ describe('commands', () => {
command: 'npx',
args: ['expo', 'install', '@expo/metro-runtime'],
showOutput: false,
showSpinner: false,
});
});
});
@@ -118,6 +108,7 @@ describe('commands', () => {
args: ['init'],
cwd: projectDir,
showOutput: false,
showSpinner: false,
});
expect(runCommandAsync).toHaveBeenCalledWith({
@@ -125,6 +116,7 @@ describe('commands', () => {
args: ['add', '.'],
cwd: projectDir,
showOutput: false,
showSpinner: false,
});
expect(runCommandAsync).toHaveBeenCalledWith({
@@ -132,6 +124,7 @@ describe('commands', () => {
args: ['commit', '-m', 'Initial commit'],
cwd: projectDir,
showOutput: false,
showSpinner: false,
});
});
});
@@ -8,7 +8,6 @@ import { promptAsync } from '../../../prompts';
import {
findAvailableProjectNameAsync,
generateProjectConfigAsync,
generateProjectNameVariations,
getAccountChoices,
promptForProjectAccountAsync,
} from '../configs';
@@ -17,6 +16,9 @@ jest.mock('../../../prompts');
jest.mock('../../../log');
jest.mock('fs-extra');
jest.mock('../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
jest.mock('../utils', () => ({
printDirectory: jest.fn((dir: string) => dir),
}));
describe('configs', () => {
let logSpy: LogSpy;
@@ -53,49 +55,53 @@ describe('configs', () => {
};
describe('when no path is provided', () => {
it('should generate name and directory when base name is available', async () => {
it('should prompt for project name with default value', async () => {
(fs.pathExists as jest.Mock).mockResolvedValue(false);
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
mockUserInput({ name: 'expo-project' });
const result = await generateProjectConfigAsync(jester, undefined, mockOptions);
const result = await generateProjectConfigAsync(undefined, mockOptions);
expect(result).toEqual({
projectName: 'new-expo-project',
projectDirectory: expect.stringContaining('/new-expo-project'),
projectName: 'expo-project',
projectDirectory: expect.stringContaining('/expo-project'),
});
logSpy.expectLogToContain('Using project name: new-expo-project');
expectPromptToHaveMessage('What would you like to name your project?');
expect(promptAsync).toHaveBeenCalledWith(
expect.objectContaining({
initial: 'expo-project',
})
);
logSpy.expectLogToContain('Using project name: expo-project');
});
it('should use username-date when base name exists', async () => {
(fs.pathExists as jest.Mock)
.mockResolvedValueOnce(true) // base name exists
.mockResolvedValueOnce(false); // username-date doesn't exist
it('should prompt and use custom name entered by user', async () => {
(fs.pathExists as jest.Mock).mockResolvedValue(false);
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
mockUserInput({ name: 'user-custom-name' });
const result = await generateProjectConfigAsync(jester, undefined, mockOptions);
const result = await generateProjectConfigAsync(undefined, mockOptions);
expect(result).toEqual({
projectName: expect.stringMatching(/^new-expo-project-jester-\d{4}-\d{2}-\d{2}$/),
projectDirectory: expect.stringContaining(result.projectName),
projectName: 'user-custom-name',
projectDirectory: expect.stringContaining('/user-custom-name'),
});
logSpy.expectLogToContain('Using alternate project name:');
expectPromptToHaveMessage('What would you like to name your project?');
logSpy.expectLogToContain('Using project name: user-custom-name');
});
it('should use username-date-shortID when base name and username-date exist', async () => {
(fs.pathExists as jest.Mock)
.mockResolvedValueOnce(true) // base name exists
.mockResolvedValueOnce(true); // username-date exists
it('should use shortid when prompted name exists', async () => {
(fs.pathExists as jest.Mock).mockResolvedValue(true); // base name exists
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
mockUserInput({ name: 'existing-project' });
const result = await generateProjectConfigAsync(jester, undefined, mockOptions);
const result = await generateProjectConfigAsync(undefined, mockOptions);
expect(result).toEqual({
projectName: expect.stringMatching(
/^new-expo-project-jester-\d{4}-\d{2}-\d{2}-[a-zA-Z0-9_-]{6}$/
),
projectName: expect.stringMatching(/^existing-project-[a-zA-Z0-9_-]{6}$/),
projectDirectory: expect.stringContaining(result.projectName),
});
logSpy.expectLogToContain('Using alternate project name:');
logSpy.expectLogToContain('Using project name:');
});
});
@@ -104,12 +110,13 @@ describe('configs', () => {
(fs.pathExists as jest.Mock).mockResolvedValue(false);
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
const absolutePath = '/absolute/path/to/my-project';
const result = await generateProjectConfigAsync(jester, absolutePath, mockOptions);
const result = await generateProjectConfigAsync(absolutePath, mockOptions);
expect(result).toEqual({
projectName: 'my-project',
projectDirectory: absolutePath,
});
expect(promptAsync).not.toHaveBeenCalled();
logSpy.expectLogToContain('Using project directory:');
});
@@ -117,12 +124,13 @@ describe('configs', () => {
(fs.pathExists as jest.Mock).mockResolvedValue(false);
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
const relativePath = 'some/relative/my-app';
const result = await generateProjectConfigAsync(jester, relativePath, mockOptions);
const result = await generateProjectConfigAsync(relativePath, mockOptions);
expect(result).toEqual({
projectName: 'my-app',
projectDirectory: expect.stringContaining('some/relative/my-app'),
});
expect(promptAsync).not.toHaveBeenCalled();
logSpy.expectLogToContain('Using project directory:');
});
});
@@ -150,10 +158,6 @@ describe('configs', () => {
const result = getAccountChoices(jester);
expect(result).toEqual([
{
title: 'jester (personal account)',
value: { name: 'jester' },
},
{
title: 'other',
value: { name: 'other' },
@@ -161,19 +165,10 @@ describe('configs', () => {
description:
'You do not have the required permissions to create projects on this account.',
},
]);
});
});
describe('generateProjectNameVariations', () => {
it('should generate name variations', () => {
const baseName = 'my-project';
const result = generateProjectNameVariations(jester, baseName);
expect(result).toEqual([
'my-project',
expect.stringMatching(/^my-project-jester-\d{4}-\d{2}-\d{2}$/),
expect.stringMatching(/^my-project-jester-\d{4}-\d{2}-\d{2}-[a-zA-Z0-9_-]{6}$/),
{
title: 'jester',
value: { name: 'jester' },
},
]);
});
});
@@ -189,12 +184,7 @@ describe('configs', () => {
(fs.pathExists as jest.Mock).mockResolvedValue(false);
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
const result = await findAvailableProjectNameAsync(
jester,
'test-project',
'/base/path',
mockOptions
);
const result = await findAvailableProjectNameAsync('test-project', '/base/path', mockOptions);
expect(result).toEqual({
projectName: 'test-project',
@@ -208,63 +198,48 @@ describe('configs', () => {
);
});
it('should try alternatives when first name exists locally', async () => {
(fs.pathExists as jest.Mock)
.mockResolvedValueOnce(true) // First exists
.mockResolvedValueOnce(false); // Second available
it('should use shortid variant when base name exists locally', async () => {
(fs.pathExists as jest.Mock).mockResolvedValue(true); // Base name exists locally
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
const result = await findAvailableProjectNameAsync(
jester,
'test-project',
'/base/path',
mockOptions
);
const result = await findAvailableProjectNameAsync('test-project', '/base/path', mockOptions);
expect(result).toEqual({
projectName: expect.stringMatching(/^test-project-jester-\d{4}-\d{2}-\d{2}$/),
projectDirectory: expect.stringContaining('/base/path/test-project-jester-'),
projectName: expect.stringMatching(/^test-project-[a-zA-Z0-9_-]{6}$/),
projectDirectory: expect.stringContaining('/base/path/test-project-'),
});
expect(fs.pathExists).toHaveBeenCalledTimes(2);
expect(fs.pathExists).toHaveBeenCalledTimes(1);
expect(fs.pathExists).toHaveBeenCalledWith('/base/path/test-project');
});
it('should skip name if local is available but remote exists', async () => {
it('should use shortid variant when base name exists remotely', async () => {
(fs.pathExists as jest.Mock).mockResolvedValue(false);
jest
.mocked(findProjectIdByAccountNameAndSlugNullableAsync)
.mockResolvedValueOnce('project-id') // First name exists remotely
.mockResolvedValueOnce(null); // Second available remotely
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue('project-id'); // Base name exists remotely
const result = await findAvailableProjectNameAsync(
jester,
'test-project',
'/base/path',
mockOptions
);
const result = await findAvailableProjectNameAsync('test-project', '/base/path', mockOptions);
expect(result).toEqual({
projectName: expect.stringMatching(/^test-project-jester-\d{4}-\d{2}-\d{2}$/),
projectDirectory: expect.stringContaining('/base/path/test-project-jester-'),
projectName: expect.stringMatching(/^test-project-[a-zA-Z0-9_-]{6}$/),
projectDirectory: expect.stringContaining('/base/path/test-project-'),
});
expect(findProjectIdByAccountNameAndSlugNullableAsync).toHaveBeenCalledTimes(2);
expect(findProjectIdByAccountNameAndSlugNullableAsync).toHaveBeenCalledTimes(1);
expect(findProjectIdByAccountNameAndSlugNullableAsync).toHaveBeenCalledWith(
mockGraphqlClient,
'jester',
'test-project'
);
});
it('should throw error when all variations taken', async () => {
it('should use shortid variant when base name is taken both locally and remotely', async () => {
(fs.pathExists as jest.Mock).mockResolvedValue(true);
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue('project-id');
await expect(
findAvailableProjectNameAsync(jester, 'test-project', '/base/path', mockOptions)
).rejects.toThrow('Unable to find a unique project name');
});
const result = await findAvailableProjectNameAsync('test-project', '/base/path', mockOptions);
it('should log when using alternative name', async () => {
(fs.pathExists as jest.Mock).mockResolvedValueOnce(true).mockResolvedValueOnce(false);
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
await findAvailableProjectNameAsync(jester, 'test-project', '/base/path', mockOptions);
logSpy.expectLogToContain('Using alternate project name:');
expect(result).toEqual({
projectName: expect.stringMatching(/^test-project-[a-zA-Z0-9_-]{6}$/),
projectDirectory: expect.stringContaining('/base/path/test-project-'),
});
});
});
});
@@ -1,7 +1,6 @@
import { ExpoConfig } from '@expo/config';
import fs from 'fs-extra';
import { LogSpy } from './testUtils';
import { getEASUpdateURL } from '../../../api';
import { AppFragment } from '../../../graphql/generated';
import { easCliVersion } from '../../../utils/easCli';
@@ -18,12 +17,6 @@ jest.mock('../../../api');
jest.mock('fs-extra');
describe('projectFiles', () => {
let logSpy: LogSpy;
beforeAll(() => {
logSpy = new LogSpy('withTick');
});
beforeEach(() => {
jest.clearAllMocks();
@@ -33,10 +26,7 @@ describe('projectFiles', () => {
jest.mocked(fs.copy).mockImplementation(() => Promise.resolve());
jest.mocked(fs.remove).mockImplementation(() => Promise.resolve());
jest.mocked(fs.readFile).mockImplementation(() => Promise.resolve(''));
});
afterAll(() => {
logSpy.restore();
jest.mocked(fs.symlink).mockImplementation(() => Promise.resolve());
});
describe('cleanAndPrefix', () => {
@@ -128,8 +118,6 @@ describe('projectFiles', () => {
expect(fs.writeJson).toHaveBeenCalledWith(`${projectDir}/app.json`, expectedConfig, {
spaces: 2,
});
logSpy.expectLogToContain('Generated app.json');
});
it('should handle invalid characters in the bundle identifier', async () => {
@@ -247,8 +235,6 @@ describe('projectFiles', () => {
expect(fs.writeJson).toHaveBeenCalledWith(`${projectDir}/eas.json`, expectedEasConfig, {
spaces: 2,
});
logSpy.expectLogToContain('Generated eas.json');
});
});
@@ -265,7 +251,7 @@ describe('projectFiles', () => {
name: 'test-app',
version: '1.0.0',
scripts: {
preview: 'npx eas-cli@latest workflow:run publish-preview-update.yml',
draft: 'npx eas-cli@latest workflow:run create-draft.yml',
'development-builds': 'npx eas-cli@latest workflow:run create-development-builds.yml',
deploy: 'npx eas-cli@latest workflow:run deploy-to-production.yml',
},
@@ -274,8 +260,6 @@ describe('projectFiles', () => {
expect(fs.writeJson).toHaveBeenCalledWith(`${projectDir}/package.json`, expectedPackageJson, {
spaces: 2,
});
logSpy.expectLogToContain('Updated package.json with scripts');
});
});
@@ -286,12 +270,14 @@ describe('projectFiles', () => {
await copyProjectTemplatesAsync(projectDir);
expect(fs.copy).toHaveBeenCalledWith(
expect.stringContaining('templates/.eas/workflows'),
expect.stringContaining('.eas/workflows'),
{ errorOnExist: false, overwrite: true }
expect.stringContaining('templates'),
projectDir,
expect.objectContaining({
errorOnExist: false,
overwrite: true,
filter: expect.any(Function),
})
);
logSpy.expectLogToContain('Created EAS workflow files');
});
});
@@ -342,8 +328,6 @@ Install the dependencies first.`;
projectReadmePath,
expect.stringMatching(new RegExp(headings.join('[\\s\\S]*')))
);
logSpy.expectLogToContain('Updated README.md with EAS configuration details');
});
it('should replace npm run with package manager specific commands', async () => {
@@ -0,0 +1,85 @@
import path from 'path';
import { printDirectory } from '../utils';
describe('utils', () => {
describe('printDirectory', () => {
let cwdSpy: jest.SpyInstance;
beforeEach(() => {
// Mock process.cwd() to return a predictable value
cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue('/Users/test/projects');
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should return "." for the current directory', () => {
const result = printDirectory('/Users/test/projects');
expect(result).toBe('.');
});
it('should return relative path with "./" prefix for subdirectory', () => {
const result = printDirectory('/Users/test/projects/my-app');
expect(result).toBe('./my-app');
});
it('should return relative path with "./" prefix for nested subdirectory', () => {
const result = printDirectory('/Users/test/projects/nested/my-app');
expect(result).toBe('./nested/my-app');
});
it('should return absolute path for directory outside cwd', () => {
const result = printDirectory('/Users/other/projects/my-app');
expect(result).toBe('/Users/other/projects/my-app');
});
it('should return absolute path for parent directory', () => {
const result = printDirectory('/Users/test');
expect(result).toBe('/Users/test');
});
it('should handle relative paths and convert them correctly', () => {
// Relative path resolves to absolute first, then converts back to relative
const result = printDirectory('my-app');
expect(result).toBe('./my-app');
});
it('should handle relative paths with "./" already', () => {
const result = printDirectory('./my-app');
expect(result).toBe('./my-app');
});
it('should handle relative paths with "../" (goes outside cwd)', () => {
const result = printDirectory('../other-project');
// This goes outside cwd, so should return absolute path
expect(result).toBe('/Users/test/other-project');
});
it('should handle complex relative paths', () => {
const result = printDirectory('./nested/deep/project');
expect(result).toBe('./nested/deep/project');
});
it('should normalize paths correctly', () => {
const result = printDirectory('/Users/test/projects/./my-app/../my-app');
expect(result).toBe('./my-app');
});
it('should handle paths in real cwd', () => {
// Restore mock to test with real cwd
cwdSpy.mockRestore();
const realCwd = process.cwd();
// Test with current directory
const result1 = printDirectory(realCwd);
expect(result1).toBe('.');
// Test with subdirectory
const subDir = path.join(realCwd, 'test-dir');
const result2 = printDirectory(subDir);
expect(result2).toBe('./test-dir');
});
});
});
@@ -1,17 +1,20 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import Log from '../../log';
import { printDirectory } from './utils';
import { canAccessRepositoryUsingSshAsync, runGitCloneAsync } from '../../onboarding/git';
import { PackageManager, installDependenciesAsync } from '../../onboarding/installDependencies';
import { runCommandAsync } from '../../onboarding/runCommand';
import { ora } from '../../ora';
export async function cloneTemplateAsync(targetProjectDir: string): Promise<string> {
const githubUsername = 'expo';
const githubRepositoryName = 'expo-template-default';
Log.log(`📂 Cloning the project to ${targetProjectDir}`);
Log.newLine();
const spinner = ora(
`${chalk.bold(`Cloning the project to ${printDirectory(targetProjectDir)}`)}`
).start();
const cloneMethod = (await canAccessRepositoryUsingSshAsync({
githubUsername,
@@ -25,8 +28,11 @@ export async function cloneTemplateAsync(targetProjectDir: string): Promise<stri
githubRepositoryName,
targetProjectDir,
cloneMethod,
showOutput: false,
});
spinner.succeed(`Cloned the project to ${printDirectory(finalTargetProjectDirectory)}`);
return finalTargetProjectDirectory;
}
@@ -34,6 +40,7 @@ export async function installProjectDependenciesAsync(
projectDir: string,
packageManager: PackageManager
): Promise<void> {
const spinner = ora(`${chalk.bold('Installing project dependencies')}`).start();
await installDependenciesAsync({
outputLevel: 'none',
projectDir,
@@ -42,16 +49,20 @@ export async function installProjectDependenciesAsync(
const dependencies = ['expo-updates', '@expo/metro-runtime'];
for (const dependency of dependencies) {
spinner.text = `Installing ${chalk.bold(dependency)}`;
await runCommandAsync({
cwd: projectDir,
command: 'npx',
args: ['expo', 'install', dependency],
showOutput: false,
showSpinner: false,
});
}
spinner.succeed(`Installed project dependencies`);
}
export async function initializeGitRepositoryAsync(projectDir: string): Promise<void> {
const spinner = ora(`${chalk.bold('Initializing Git repository')}`).start();
await fs.remove(path.join(projectDir, '.git'));
const commands = [['init'], ['add', '.'], ['commit', '-m', 'Initial commit']];
@@ -62,7 +73,8 @@ export async function initializeGitRepositoryAsync(projectDir: string): Promise<
command: 'git',
args,
showOutput: false,
showSpinner: false,
});
Log.log();
}
spinner.succeed(`Initialized Git repository`);
}
@@ -2,11 +2,12 @@ import fs from 'fs-extra';
import { nanoid } from 'nanoid';
import path from 'path';
import { printDirectory } from './utils';
import { Role } from '../../graphql/generated';
import Log from '../../log';
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { Choice, promptAsync } from '../../prompts';
import { Actor, getActorUsername } from '../../user/User';
import { Actor } from '../../user/User';
import { ExpoGraphqlClient } from '../context/contextUtils/createGraphqlClient';
function validateProjectPath(resolvedPath: string): void {
@@ -31,7 +32,6 @@ function validateProjectPath(resolvedPath: string): void {
}
export async function generateProjectConfigAsync(
actor: Actor,
pathArg: string | undefined,
options: {
graphqlClient: ExpoGraphqlClient;
@@ -41,7 +41,7 @@ export async function generateProjectConfigAsync(
projectName: string;
projectDirectory: string;
}> {
let baseName = 'new-expo-project';
let baseName = 'expo-project';
let parentDirectory = process.cwd();
if (pathArg) {
@@ -49,17 +49,26 @@ export async function generateProjectConfigAsync(
validateProjectPath(resolvedPath);
baseName = path.basename(resolvedPath);
parentDirectory = path.dirname(resolvedPath);
} else {
baseName = (
await promptAsync({
type: 'text',
name: 'name',
message: 'What would you like to name your project?',
initial: 'expo-project',
})
).name;
}
// Find an available name checking both local filesystem and remote server
const { projectName, projectDirectory } = await findAvailableProjectNameAsync(
actor,
baseName,
parentDirectory,
options
);
Log.withInfo(`Using project directory: ${projectDirectory}`);
Log.withInfo(`Using project name: ${projectName}`);
Log.withInfo(`Using project directory: ${printDirectory(projectDirectory)}`);
return {
projectName,
@@ -108,15 +117,18 @@ export async function promptForProjectAccountAsync(actor: Actor): Promise<string
export function getAccountChoices(actor: Actor, permissionsMap?: Map<string, boolean>): Choice[] {
const permissions = permissionsMap ?? getAccountPermissionsMap(actor);
const sortedAccounts = [...actor.accounts].sort((a, _b) =>
actor.__typename === 'User' ? (a.name === actor.username ? -1 : 1) : 0
);
const sortedAccounts = [...actor.accounts].sort((a, _b) => (a.ownerUserActor ? 1 : -1));
return sortedAccounts.map(account => {
const isPersonalAccount = actor.__typename === 'User' && account.name === actor.username;
const isPersonalAccount = !!account.ownerUserActor && account.ownerUserActor.id === actor.id;
const isTeamAccount = !!account.ownerUserActor && account.ownerUserActor.id !== actor.id;
const accountDisplayName = isPersonalAccount
? `${account.name} (personal account)`
: account.name;
? `${account.name} (Limited - Personal Account)`
: isTeamAccount
? `${account.name} (Limited - Team Account)`
: account.name;
const disabled = !permissions.get(account.name);
return {
@@ -130,17 +142,6 @@ export function getAccountChoices(actor: Actor, permissionsMap?: Map<string, boo
});
}
export function generateProjectNameVariations(actor: Actor, baseName: string): string[] {
const username = getActorUsername(actor);
const date = new Date().toISOString().split('T')[0];
return [
baseName,
`${baseName}-${username}-${date}`,
`${baseName}-${username}-${date}-${nanoid(6)}`,
];
}
async function verifyProjectDoesNotExistAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string,
@@ -167,7 +168,6 @@ async function verifyProjectDoesNotExistAsync(
* Remote server (project already exists on Expo)
*/
export async function findAvailableProjectNameAsync(
actor: Actor,
baseName: string,
parentDirectory: string,
{
@@ -178,37 +178,24 @@ export async function findAvailableProjectNameAsync(
projectAccount: string;
}
): Promise<{ projectName: string; projectDirectory: string }> {
const nameVariations = generateProjectNameVariations(actor, baseName);
let projectName = baseName;
let projectDirectory = path.join(parentDirectory, projectName);
for (let i = 0; i < nameVariations.length; i++) {
const nameVariation = nameVariations[i];
const proposedDirectory = path.join(parentDirectory, nameVariation);
const usingVariant = i !== 0;
const localExists = await fs.pathExists(projectDirectory);
const localExists = await fs.pathExists(proposedDirectory);
if (localExists) {
continue;
}
const remoteAvailable = await verifyProjectDoesNotExistAsync(
graphqlClient,
projectAccount,
projectName
);
const remoteAvailable = await verifyProjectDoesNotExistAsync(
graphqlClient,
projectAccount,
nameVariation,
{ silent: usingVariant }
);
if (!remoteAvailable) {
continue;
}
Log.withInfo(`Using ${usingVariant ? 'alternate ' : ''}project name: ${nameVariation}`);
return {
projectName: nameVariation,
projectDirectory: proposedDirectory,
};
if (localExists || !remoteAvailable) {
projectName = `${baseName}-${nanoid(6)}`;
projectDirectory = path.join(parentDirectory, projectName);
}
throw new Error(
`Unable to find a unique project name for "${baseName}". All generated variations already exist.`
);
return {
projectName,
projectDirectory,
};
}
@@ -1,13 +1,11 @@
import { ExpoConfig } from '@expo/config';
import { AppVersionSource, EasJson } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import merge from 'ts-deepmerge';
import { getEASUpdateURL } from '../../api';
import { AppFragment } from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import { PackageManager } from '../../onboarding/installDependencies';
import { easCliVersion } from '../../utils/easCli';
@@ -63,12 +61,6 @@ export async function generateAppConfigAsync(projectDir: string, app: AppFragmen
const appJsonPath = path.join(projectDir, 'app.json');
await fs.writeJson(appJsonPath, { expo: mergedConfig }, { spaces: 2 });
Log.withTick(
`Generated ${chalk.bold('app.json')}. ${learnMore(
'https://docs.expo.dev/versions/latest/config/app/'
)}`
);
Log.log();
}
export async function generateEasConfigAsync(projectDir: string): Promise<void> {
@@ -116,12 +108,6 @@ export async function generateEasConfigAsync(projectDir: string): Promise<void>
const easJsonPath = path.join(projectDir, 'eas.json');
await fs.writeJson(easJsonPath, easJson, { spaces: 2 });
Log.withTick(
`Generated ${chalk.bold('eas.json')}. ${learnMore(
'https://docs.expo.dev/build-reference/eas-json/'
)}`
);
Log.log();
}
export async function updatePackageJsonAsync(projectDir: string): Promise<void> {
@@ -131,27 +117,25 @@ export async function updatePackageJsonAsync(projectDir: string): Promise<void>
if (!packageJson.scripts) {
packageJson.scripts = {};
}
packageJson.scripts.preview = 'npx eas-cli@latest workflow:run publish-preview-update.yml';
packageJson.scripts.draft = 'npx eas-cli@latest workflow:run create-draft.yml';
packageJson.scripts['development-builds'] =
'npx eas-cli@latest workflow:run create-development-builds.yml';
packageJson.scripts.deploy = 'npx eas-cli@latest workflow:run deploy-to-production.yml';
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
Log.withTick('Updated package.json with scripts');
Log.log();
}
export async function copyProjectTemplatesAsync(projectDir: string): Promise<void> {
const templatesSourceDir = path.join(__dirname, 'templates', '.eas', 'workflows');
const easWorkflowsTargetDir = path.join(projectDir, '.eas', 'workflows');
const templatesSourceDir = path.join(__dirname, 'templates');
await fs.copy(templatesSourceDir, easWorkflowsTargetDir, {
// Copy everything from templates to projectDir, skipping readme-additions.md
await fs.copy(templatesSourceDir, projectDir, {
overwrite: true,
errorOnExist: false,
filter: (src: string) => {
return !src.endsWith('readme-additions.md');
},
});
Log.withTick('Created EAS workflow files');
Log.log();
}
export async function updateReadmeAsync(
@@ -189,7 +173,4 @@ export async function updateReadmeAsync(
mergedReadme = mergedReadme.replaceAll('npm run', `${packageManager} run`);
await fs.writeFile(projectReadmePath, mergedReadme);
Log.withTick('Updated README.md with EAS configuration details');
Log.log();
}
@@ -1,4 +1,4 @@
name: Publish preview update
name: Create draft
on:
push:
@@ -10,3 +10,6 @@ jobs:
type: update
params:
branch: ${{ github.ref_name || 'test' }}
deploy_website:
name: Deploy website
type: deploy
@@ -0,0 +1,163 @@
# AGENTS.md
## Project Overview
This is an Expo/React Native mobile application. Prioritize mobile-first patterns, performance, and cross-platform compatibility.
## Documentation Resources
When working on this project, **always consult the official Expo documentation** available at:
- **https://docs.expo.dev/llms.txt** - Index of all available documentation files
- **https://docs.expo.dev/llms-full.txt** - Complete Expo documentation including Expo Router, Expo Modules API, development process
- **https://docs.expo.dev/llms-eas.txt** - Complete EAS (Expo Application Services) documentation
- **https://docs.expo.dev/llms-sdk.txt** - Complete Expo SDK documentation
- **https://reactnative.dev/docs/getting-started** - Complete React Native documentation
These documentation files are specifically formatted for AI agents and should be your **primary reference** for:
- Expo APIs and best practices
- Expo Router navigation patterns
- EAS Build, Submit, and Update workflows
- Expo SDK modules and their usage
- Development and deployment processes
## Project Structure
```
/
├── app/ # Expo Router file-based routing
│ ├── (tabs)/ # Tab-based navigation screens
│ │ ├── index.tsx # Home screen
│ │ ├── explore.tsx # Explore screen
│ │ └── _layout.tsx # Tabs layout
│ ├── _layout.tsx # Root layout with theme provider
│ └── modal.tsx # Modal screen example
├── components/ # Reusable React components
│ ├── ui/ # UI primitives (IconSymbol, Collapsible)
│ └── ... # Feature components (themed, haptic, parallax)
├── constants/ # App-wide constants (theme, colors)
├── hooks/ # Custom React hooks (color scheme, theme)
├── assets/ # Static assets (images, fonts)
├── scripts/ # Utility scripts (reset-project)
├── .eas/workflows/ # EAS Workflows (CI/CD automation)
├── app.json # Expo configuration
├── eas.json # EAS Build/Submit configuration
└── package.json # Dependencies and scripts
```
## Essential Commands
### Development
```bash
npx expo start # Start dev server
npx expo start --clear # Clear cache and start dev server
npx expo install <package> # Install packages with compatible versions
npx expo install --check # Check which installed packages need to be updated
npx expo install --fix # Automatically update any invalid package versions
npm run development-builds # Create development builds (workflow)
npm run reset-project # Reset to blank template
```
### Building & Testing
```bash
npx expo doctor # Check project health and dependencies
npx expo lint # Run ESLint
npm run draft # Publish preview update and website (workflow)
```
### Production
```bash
npx eas-cli@latest build --platform ios -s # Use EAS to build for iOS platform and submit to App Store
npx eas-cli@latest build --platform android -s # Use EAS to build for Android platform and submit to Google Play Store
npm run deploy # Deploy to production (workflow)
```
## Development Guidelines
### Code Style & Standards
- **TypeScript First**: Use TypeScript for all new code with strict type checking
- **Naming Conventions**: Use meaningful, descriptive names for variables, functions, and components
- **Self-Documenting Code**: Write clear, readable code that explains itself; only add comments for complex business logic or design decisions
- **React 19 Patterns**: Follow modern React patterns including:
- Function components with hooks
- Enable React Compiler
- Proper dependency arrays in useEffect
- Memoization when appropriate (useMemo, useCallback)
- Error boundaries for better error handling
### Navigation & Routing
- Use **Expo Router** for all navigation
- Import `Link`, `router`, and `useLocalSearchParams` from `expo-router`
- Docs: https://docs.expo.dev/router/introduction/
### Recommended Libraries
- **Navigation**: `expo-router` for navigation
- **Images**: `expo-image` for optimized image handling and caching
- **Animations**: `react-native-reanimated` for performant animations on native thread
- **Gestures**: `react-native-gesture-handler` for native gesture recognition
- **Storage**: Use `expo-sqlite` for persistent storage, `expo-sqlite/kv-store` for simple key-value storage
## Debugging & Development Tools
### DevTools Integration
- **React Native DevTools**: Use MCP `open_devtools` command to launch debugging tools
- **Network Inspection**: Monitor API calls and network requests in DevTools
- **Element Inspector**: Debug component hierarchy and styles
- **Performance Profiler**: Identify performance bottlenecks
- **Logging**: Use `console.log` for debugging (remove before production), `console.warn` for deprecation notices, `console.error` for actual errors, and implement error boundaries for production error handling
### Testing & Quality Assurance
#### Automated Testing with MCP Tools
Developers can configure the Expo MCP server with the following doc: https://docs.expo.dev/eas/ai/mcp/
- **Component Testing**: Add `testID` props to components for automation
- **Visual Testing**: Use MCP `automation_take_screenshot` to verify UI appearance
- **Interaction Testing**: Use MCP `automation_tap_by_testid` to simulate user interactions
- **View Verification**: Use MCP `automation_find_view_by_testid` to validate component rendering
## EAS Workflows CI/CD
This project is pre-configured with **EAS Workflows** for automating development and release processes. Workflows are defined in `.eas/workflows/` directory.
When working with EAS Workflows, **always refer to**:
- https://docs.expo.dev/eas/workflows/ for workflow examples
- The `.eas/workflows/` directory for existing workflow configurations
- You can check that a workflow YAML is valid using the workflows schema: https://exp.host/--/api/v2/workflows/schema
### Build Profiles (eas.json)
- **development**: Development builds with dev client
- **development-simulator**: Development builds for iOS simulator
- **preview**: Internal distribution preview builds
- **production**: Production builds with auto-increment
## Troubleshooting
### Expo Go Errors & Development Builds
If there are errors in **Expo Go** or the project is not running, create a **development build**. **Expo Go** is a sandbox environment with a limited set of native modules. To create development builds, run `eas build:dev`. Additionally, after installing new packages or adding config plugins, new development builds are often required.
## AI Agent Instructions
When working on this project:
1. **Always start by consulting the appropriate documentation**:
- For general Expo questions: https://docs.expo.dev/llms-full.txt
- For EAS/deployment questions: https://docs.expo.dev/llms-eas.txt
- For SDK/API questions: https://docs.expo.dev/llms-sdk.txt
2. **Understand before implementing**: Read the relevant docs section before writing code
3. **Follow existing patterns**: Look at existing components and screens for patterns to follow
@@ -0,0 +1,9 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
**Note**: This project uses `AGENTS.md` files for detailed guidance.
## Primary Reference
Please see `AGENTS.md` in this same directory for the main project documentation and guidance.
@@ -21,7 +21,7 @@ This project is configured to use [EAS Workflows](https://docs.expo.dev/eas/work
### Previews
Run `npm run preview` to [publish a preview update](https://docs.expo.dev/eas/workflows/examples/publish-preview-update/) of your project, which can be viewed in Expo Go or in a development build.
Run `npm run draft` to [publish a preview update](https://docs.expo.dev/eas/workflows/examples/publish-preview-update/) of your project, which can be viewed in Expo Go or in a development build.
### Development Builds
@@ -0,0 +1,20 @@
import path from 'path';
/**
* Formats a directory path for display.
* If the directory is within the current working directory, returns a relative path.
* Otherwise, returns the absolute path.
*/
export function printDirectory(directory: string): string {
const cwd = process.cwd();
const absoluteDir = path.isAbsolute(directory) ? directory : path.resolve(cwd, directory);
const relativePath = path.relative(cwd, absoluteDir);
// If the relative path doesn't start with '..' it means it's within or at the cwd
if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
return relativePath !== '' ? `./${relativePath}` : '.';
}
return absoluteDir;
}
@@ -1,8 +1,12 @@
import { WorkflowJobResult } from './types';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { ExpoGraphqlClient } from '../context/contextUtils/createGraphqlClient';
// This function is in a separate module for testing purposes
export async function fetchRawLogsForJobAsync(job: WorkflowJobResult): Promise<string | null> {
export async function fetchRawLogsForCustomJobAsync(
job: WorkflowJobResult
): Promise<string | null> {
const firstLogFileUrl = job.turtleJobRun?.logFileUrls?.[0];
if (!firstLogFileUrl) {
return null;
@@ -13,3 +17,25 @@ export async function fetchRawLogsForJobAsync(job: WorkflowJobResult): Promise<s
const rawLogs = await response.text();
return rawLogs;
}
export async function fetchRawLogsForBuildJobAsync(
state: { graphqlClient: ExpoGraphqlClient },
job: WorkflowJobResult
): Promise<string | null> {
const buildId = job.outputs?.build_id;
if (!buildId) {
return null;
}
const buildFragment = await BuildQuery.byIdAsync(state.graphqlClient, buildId, {
useCache: false,
});
const firstLogFileUrl = buildFragment.logFiles?.[0];
if (!firstLogFileUrl) {
return null;
}
const response = await fetch(firstLogFileUrl, {
method: 'GET',
});
const rawLogs = await response.text();
return rawLogs;
}
@@ -0,0 +1,192 @@
import * as YAML from 'yaml';
import { z } from 'zod';
import { DefaultEnvironment } from '../../build/utils/environment';
import { booleanLike, stringLike } from '../../credentials/ios/types';
import Log from '../../log';
import { promptAsync } from '../../prompts';
const inputExtraProperties = {
description: stringLike.optional().describe('Description of the input'),
required: booleanLike.default(false).optional().describe('Whether the input is required.'),
};
// Adapted from the input definition on the server (https://github.com/expo/universe/pull/21950)
export const WorkflowDispatchInputZ = z.discriminatedUnion('type', [
z.object({
...inputExtraProperties,
type: z.literal('string').default('string').optional(),
default: stringLike.optional().describe('Default value for the input'),
}),
z.object({
...inputExtraProperties,
type: z.literal('boolean'),
default: booleanLike.optional().describe('Default value for the input'),
}),
z.object({
...inputExtraProperties,
type: z.literal('number'),
default: z.number().optional().describe('Default value for the input'),
}),
z.object({
...inputExtraProperties,
type: z.literal('choice'),
default: stringLike.optional().describe('Default value for the input'),
options: z.array(stringLike).min(1).describe('Options for choice type inputs'),
}),
z.object({
...inputExtraProperties,
type: z.literal('environment'),
default: z
.enum(Object.values(DefaultEnvironment))
.optional()
.describe('Default value for the input'),
}),
]);
export function parseInputs(inputFlags: string[]): Record<string, string> {
const inputs: Record<string, string> = {};
for (const inputFlag of inputFlags) {
const equalIndex = inputFlag.indexOf('=');
if (equalIndex === -1) {
throw new Error(`Invalid input format: ${inputFlag}. Expected key=value format.`);
}
const key = inputFlag.substring(0, equalIndex);
const value = inputFlag.substring(equalIndex + 1);
if (!key) {
throw new Error(`Invalid input format: ${inputFlag}. Key cannot be empty.`);
}
inputs[key] = value;
}
return inputs;
}
export function parseJsonInputs(jsonString: string): Record<string, string> {
try {
const parsed = JSON.parse(jsonString);
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw new Error('JSON input must be an object.');
}
return parsed;
} catch (error) {
throw new Error(`Invalid JSON input.`, { cause: error });
}
}
export function parseWorkflowInputsFromYaml(
yamlConfig: string
): Record<string, z.infer<typeof WorkflowDispatchInputZ>> {
try {
const parsed = YAML.parse(yamlConfig);
return z
.record(z.string(), WorkflowDispatchInputZ)
.default({})
.parse(parsed?.on?.workflow_dispatch?.inputs);
} catch (error) {
Log.warn('Failed to parse workflow inputs from YAML:', error);
return {};
}
}
export async function maybePromptForMissingInputsAsync({
inputSpecs,
inputs,
}: {
inputSpecs: Record<string, z.infer<typeof WorkflowDispatchInputZ>>;
inputs: Record<string, unknown>;
}): Promise<Record<string, unknown>> {
const requiredInputs = Object.entries(inputSpecs).filter(([_, spec]) => spec.required);
const missingRequiredInputs = requiredInputs.filter(([key]) => inputs[key] === undefined);
if (missingRequiredInputs.length === 0) {
return inputs;
}
Log.addNewLineIfNone();
Log.log('Some required inputs are missing. Please provide them:');
const nextInputs = { ...inputs };
for (const [key, spec] of missingRequiredInputs) {
const value = await promptForMissingInputAsync({ key, spec });
nextInputs[key] = value;
}
return nextInputs;
}
async function promptForMissingInputAsync({
key,
spec,
}: {
key: string;
spec: z.infer<typeof WorkflowDispatchInputZ>;
}): Promise<unknown> {
const message = spec.description ? `${key} (${spec.description})` : key;
switch (spec.type) {
case 'boolean': {
const { value } = await promptAsync({
type: 'confirm',
name: 'value',
message,
initial: spec.default,
});
return value;
}
case 'number': {
const { value } = await promptAsync({
type: 'number',
name: 'value',
message,
initial: spec.default,
validate: (val: number) => {
if (isNaN(val)) {
return 'Please enter a valid number';
}
return true;
},
});
return value;
}
case 'choice': {
const { value } = await promptAsync({
type: 'select',
name: 'value',
message,
choices: spec.options.map(option => ({
title: option,
value: option,
})),
initial: spec.default,
});
return value;
}
case 'string':
case 'environment':
default: {
const { value } = await promptAsync({
type: 'text',
name: 'value',
message,
initial: spec.default,
validate: (val: string) => {
if (spec.required && (!val || val.trim() === '')) {
return 'This field is required';
}
return true;
},
});
return value;
}
}
}
@@ -5,7 +5,7 @@ import {
choiceFromWorkflowJob,
choiceFromWorkflowRun,
choicesFromWorkflowLogs,
processLogsFromJobAsync,
fetchAndProcessLogsFromJobAsync,
processWorkflowRuns,
} from './utils';
import { AppQuery } from '../../graphql/queries/AppQuery';
@@ -268,7 +268,7 @@ export const workflowStepSelectionAction: WorkflowCommandSelectionAction = async
if (!job) {
return moveToWorkflowSelectionErrorState(prevState, 'No job found');
}
const logs = await processLogsFromJobAsync(job);
const logs = await fetchAndProcessLogsFromJobAsync(prevState, job);
if (!logs) {
return moveToWorkflowSelectionErrorState(prevState, 'No logs found');
}
@@ -1,7 +1,7 @@
import { WorkflowJobByIdQuery, WorkflowRunByIdWithJobsQuery } from '../../graphql/generated';
/*
* Utility types for workflow commands
* Utility types and Zod definitions for workflow commands
*/
export enum WorkflowTriggerType {
@@ -42,5 +42,12 @@ export type WorkflowRunWithJobsResult = WorkflowRunResult & {
logs?: string;
};
export type WorkflowLogLine = { time: string; msg: string; result?: string; marker?: string };
export type WorkflowLogLine = {
time: string;
msg: string;
result?: string;
marker?: string;
err?: any;
};
export type WorkflowLogs = Map<string, WorkflowLogLine[]>;
@@ -1,4 +1,7 @@
import { fetchRawLogsForJobAsync } from './fetchLogs';
import chalk from 'chalk';
import * as fs from 'node:fs';
import { fetchRawLogsForBuildJobAsync, fetchRawLogsForCustomJobAsync } from './fetchLogs';
import {
WorkflowJobResult,
WorkflowLogLine,
@@ -7,11 +10,21 @@ import {
WorkflowTriggerType,
} from './types';
import {
WorkflowJobStatus,
WorkflowJobType,
WorkflowRunByIdQuery,
WorkflowRunByIdWithJobsQuery,
WorkflowRunFragment,
WorkflowRunStatus,
WorkflowRunTriggerEventType,
} from '../../graphql/generated';
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
import Log from '../../log';
import { ora } from '../../ora';
import { Choice } from '../../prompts';
import formatFields from '../../utils/formatFields';
import { sleepAsync } from '../../utils/promise';
import { ExpoGraphqlClient } from '../context/contextUtils/createGraphqlClient';
export function computeTriggerInfoForWorkflowRun(run: WorkflowRunFragment): {
triggerType: WorkflowTriggerType;
@@ -68,16 +81,23 @@ export function choiceFromWorkflowJob(job: WorkflowJobResult, index: number): Ch
};
}
export function choicesFromWorkflowLogs(logs: WorkflowLogs): Choice[] {
return Array.from(logs.keys()).map(step => {
const logLines = logs.get(step);
const stepStatus =
logLines?.filter((line: WorkflowLogLine) => line.marker === 'end-step')[0]?.result ?? '';
return {
title: `${step} - ${stepStatus}`,
value: step,
};
});
export function choicesFromWorkflowLogs(
logs: WorkflowLogs
): (Choice & { name: string; status: string; logLines: WorkflowLogLine[] | undefined })[] {
return Array.from(logs.keys())
.map(step => {
const logLines = logs.get(step);
const stepStatus =
logLines?.filter((line: WorkflowLogLine) => line.marker === 'end-step')[0]?.result ?? '';
return {
title: `${step} - ${stepStatus}`,
name: step,
status: stepStatus,
value: step,
logLines,
};
})
.filter(step => step.status !== 'skipped');
}
export function processWorkflowRuns(runs: WorkflowRunFragment[]): WorkflowRunResult[] {
@@ -100,29 +120,254 @@ export function processWorkflowRuns(runs: WorkflowRunFragment[]): WorkflowRunRes
});
}
export async function processLogsFromJobAsync(
export async function fetchAndProcessLogsFromJobAsync(
state: { graphqlClient: ExpoGraphqlClient },
job: WorkflowJobResult
): Promise<WorkflowLogs | null> {
const rawLogs = await fetchRawLogsForJobAsync(job);
let rawLogs: string | null;
switch (job.type) {
case WorkflowJobType.Build:
case WorkflowJobType.Repack:
rawLogs = await fetchRawLogsForBuildJobAsync(state, job);
break;
default:
rawLogs = await fetchRawLogsForCustomJobAsync(job);
break;
}
if (!rawLogs) {
return null;
}
Log.debug(`rawLogs = ${JSON.stringify(rawLogs, null, 2)}`);
const logs: WorkflowLogs = new Map();
const logKeys = new Set<string>();
rawLogs.split('\n').forEach(line => {
rawLogs.split('\n').forEach((line, index) => {
Log.debug(`line ${index} = ${JSON.stringify(line, null, 2)}`);
try {
const parsedLine = JSON.parse(line);
const { result, marker } = parsedLine;
const { buildStepDisplayName, buildStepInternalId, time, msg } = parsedLine;
const { buildStepDisplayName, buildStepInternalId, time, msg, result, marker, err } =
parsedLine;
const stepId = buildStepDisplayName ?? buildStepInternalId;
if (stepId) {
if (!logKeys.has(stepId)) {
logKeys.add(stepId);
logs.set(stepId, []);
}
logs.get(stepId)?.push({ time, msg, result, marker });
logs.get(stepId)?.push({ time, msg, result, marker, err });
}
} catch {}
});
return logs;
}
function descriptionForJobStatus(status: WorkflowJobStatus): string {
switch (status) {
case WorkflowJobStatus.New:
return 'Waiting for worker';
case WorkflowJobStatus.InProgress:
return 'In progress';
case WorkflowJobStatus.Success:
return 'Completed successfully';
case WorkflowJobStatus.Failure:
return 'Failed';
case WorkflowJobStatus.Canceled:
return 'Canceled';
case WorkflowJobStatus.Skipped:
return 'Skipped';
case WorkflowJobStatus.ActionRequired:
return 'Waiting for action';
case WorkflowJobStatus.PendingCancel:
return 'Pending cancel';
}
}
export async function infoForActiveWorkflowRunAsync(
graphqlClient: ExpoGraphqlClient,
workflowRun: WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'],
maxLogLines: number = 5 // -1 means no limit
): Promise<string> {
const statusLines = [];
const statusValues = [];
for (const job of workflowRun.jobs) {
statusValues.push({ label: '', value: '' });
statusValues.push({ label: ' Job', value: job.name });
statusValues.push({ label: ' Status', value: descriptionForJobStatus(job.status) });
if (job.status !== WorkflowJobStatus.InProgress) {
continue;
}
const logs = await fetchAndProcessLogsFromJobAsync({ graphqlClient }, job);
const steps = logs ? choicesFromWorkflowLogs(logs) : [];
if (steps.length > 0) {
const currentStep = steps[steps.length - 1];
statusValues.push({ label: ' Current step', value: currentStep.name });
if (currentStep?.logLines?.length) {
statusValues.push({ label: ' Current logs', value: '' });
const currentLogs =
currentStep.logLines
?.map(line => line.msg)
.filter((_, index) => {
if (maxLogLines === -1) {
return true;
}
return index > (currentStep.logLines?.length ?? 0) - maxLogLines;
}) ?? [];
for (const log of currentLogs) {
statusValues.push({ label: '', value: log });
}
}
}
}
statusValues.push({ label: '', value: '' });
statusLines.push(formatFields(statusValues));
return statusLines.join('\n');
}
export async function infoForFailedWorkflowRunAsync(
graphqlClient: ExpoGraphqlClient,
workflowRun: WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'],
maxLogLines: number = -1 // -1 means no limit
): Promise<string> {
const statusLines = [];
const statusValues = [];
const logLinesToKeep = maxLogLines === -1 ? Infinity : maxLogLines;
for (const job of workflowRun.jobs) {
if (job.status !== WorkflowJobStatus.Failure) {
continue;
}
const logs = await fetchAndProcessLogsFromJobAsync({ graphqlClient }, job);
const steps = logs ? choicesFromWorkflowLogs(logs) : [];
statusValues.push({ label: '', value: '' });
statusValues.push({ label: ' Failed job', value: job.name });
if (steps.length > 0) {
const failedStep = steps.find(step => step.status === 'fail');
if (failedStep) {
const logs = failedStep.logLines?.map(line => line.msg).slice(-logLinesToKeep) ?? [];
statusValues.push({ label: ' Failed step', value: failedStep.name });
statusValues.push({
label: ' Logs for failed step',
value: '',
});
for (const log of logs) {
statusValues.push({ label: '', value: log });
}
}
}
}
statusValues.push({ label: '', value: '' });
statusLines.push(formatFields(statusValues));
return statusLines.join('\n');
}
export async function fileExistsAsync(filePath: string): Promise<boolean> {
return await fs.promises
.access(filePath, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
}
export async function maybeReadStdinAsync(): Promise<string | null> {
// Check if there's data on stdin
if (process.stdin.isTTY) {
return null;
}
return await new Promise((resolve, reject) => {
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('readable', () => {
let chunk;
while ((chunk = process.stdin.read()) !== null) {
data += chunk;
}
});
process.stdin.on('end', () => {
const trimmedData = data.trim();
resolve(trimmedData || null);
});
process.stdin.on('error', err => {
reject(err);
});
});
}
export async function showWorkflowStatusAsync(
graphqlClient: ExpoGraphqlClient,
{
workflowRunId,
spinnerUsesStdErr,
waitForCompletion = true,
}: { workflowRunId: string; spinnerUsesStdErr: boolean; waitForCompletion?: boolean }
): Promise<WorkflowRunByIdQuery['workflowRuns']['byId']> {
Log.log('Waiting for workflow run to complete. You can press Ctrl+C to exit.');
const spinner = ora({
stream: spinnerUsesStdErr ? process.stderr : process.stdout,
text: '',
}).start();
spinner.prefixText = chalk`{bold.yellow Workflow run is waiting to start:}`;
let failedFetchesCount = 0;
while (true) {
try {
const workflowRun = await WorkflowRunQuery.withJobsByIdAsync(graphqlClient, workflowRunId, {
useCache: false,
});
failedFetchesCount = 0;
switch (workflowRun.status) {
case WorkflowRunStatus.New:
break;
case WorkflowRunStatus.InProgress: {
spinner.prefixText = chalk`{bold.green Workflow run is in progress:}`;
spinner.text = await infoForActiveWorkflowRunAsync(graphqlClient, workflowRun, 5);
break;
}
case WorkflowRunStatus.ActionRequired:
spinner.prefixText = chalk`{bold.yellow Workflow run is waiting for action:}`;
break;
case WorkflowRunStatus.Canceled:
spinner.prefixText = chalk`{bold.yellow Workflow has been canceled.}`;
spinner.stopAndPersist();
return workflowRun;
case WorkflowRunStatus.Failure: {
spinner.prefixText = chalk`{bold.red Workflow has failed.}`;
const failedInfo = await infoForFailedWorkflowRunAsync(graphqlClient, workflowRun, 30);
spinner.fail(failedInfo);
return workflowRun;
}
case WorkflowRunStatus.Success:
spinner.prefixText = chalk`{bold.green Workflow has completed successfully.}`;
spinner.text = '';
spinner.succeed('');
return workflowRun;
}
if (!waitForCompletion) {
if (spinner.isSpinning) {
spinner.stopAndPersist();
}
return workflowRun;
}
} catch {
spinner.text = '⚠ Failed to fetch the workflow run status. Check your network connection.';
failedFetchesCount += 1;
if (failedFetchesCount > 6) {
spinner.fail('Failed to fetch the workflow run status 6 times in a row. Aborting wait.');
process.exit(workflowRunExitCodes.WAIT_ABORTED);
}
}
await sleepAsync(10 /* seconds */ * 1000 /* milliseconds */);
}
}
export const workflowRunExitCodes = {
WORKFLOW_FAILED: 11,
WORKFLOW_CANCELED: 12,
WAIT_ABORTED: 13,
};
+7 -36
View File
@@ -1,23 +1,19 @@
import chalk from 'chalk';
import gql from 'graphql-tag';
import { scheduleBranchDeletionAsync } from '../../branch/delete';
import { selectBranchOnAppAsync } from '../../branch/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import { withErrorHandlingAsync } from '../../graphql/client';
import {
DeleteUpdateBranchMutation,
DeleteUpdateBranchMutationVariables,
DeleteUpdateBranchResult,
GetBranchInfoQuery,
GetBranchInfoQueryVariables,
} from '../../graphql/generated';
import { GetBranchInfoQuery, GetBranchInfoQueryVariables } from '../../graphql/generated';
import Log from '../../log';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { toggleConfirmAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { pollForBackgroundJobReceiptAsync } from '../../utils/pollForBackgroundJobReceiptAsync';
async function getBranchInfoAsync(
graphqlClient: ExpoGraphqlClient,
@@ -50,31 +46,6 @@ async function getBranchInfoAsync(
return data;
}
async function deleteBranchOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{ branchId }: DeleteUpdateBranchMutationVariables
): Promise<DeleteUpdateBranchResult> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<DeleteUpdateBranchMutation, DeleteUpdateBranchMutationVariables>(
gql`
mutation DeleteUpdateBranch($branchId: ID!) {
updateBranch {
deleteUpdateBranch(branchId: $branchId) {
id
}
}
}
`,
{
branchId,
}
)
.toPromise()
);
return data.updateBranch.deleteUpdateBranch;
}
export default class BranchDelete extends EasCommand {
static override description = 'delete a branch';
@@ -146,12 +117,12 @@ export default class BranchDelete extends EasCommand {
}
}
const deletionResult = await deleteBranchOnAppAsync(graphqlClient, {
branchId,
});
const receipt = await scheduleBranchDeletionAsync(graphqlClient, { branchId });
const successfulReceipt = await pollForBackgroundJobReceiptAsync(graphqlClient, receipt);
Log.debug('Deletion result', { successfulReceipt });
if (jsonFlag) {
printJsonOnlyOutput(deletionResult);
printJsonOnlyOutput({ id: branchId });
} else {
Log.withTick(
`️Deleted branch "${branchName}" and all of its updates on project ${chalk.bold(
@@ -44,8 +44,6 @@ export default class WorkerAliasDelete extends EasCommand {
enableJsonOutput();
}
Log.warn('EAS Hosting is still in preview and subject to changes.');
const {
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
@@ -73,8 +73,6 @@ export default class WorkerAlias extends EasCommand {
enableJsonOutput();
}
Log.warn('EAS Hosting is still in preview and subject to changes.');
const {
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
@@ -45,8 +45,6 @@ export default class WorkerDelete extends EasCommand {
enableJsonOutput();
}
Log.warn('EAS Hosting is still in preview and subject to changes.');
const {
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
@@ -120,8 +120,6 @@ export default class WorkerDeploy extends EasCommand {
enableJsonOutput();
}
Log.warn('EAS Hosting is still in preview and subject to changes.');
const {
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
@@ -1,5 +1,6 @@
import { Config } from '@oclif/core';
import chalk from 'chalk';
import * as fs from 'fs-extra';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import { DefaultEnvironment } from '../../../build/utils/environment';
@@ -26,6 +27,7 @@ jest.mock('../../../prompts');
jest.mock('../../../utils/prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
jest.mock('fs-extra');
describe(EnvUpdate, () => {
const projectId = 'test-project-id';
@@ -251,4 +253,251 @@ describe(EnvUpdate, () => {
filterNames: ['TEST_VAR_1'],
});
});
it('processes file type variable in non-interactive mode with --type file and --value', async () => {
const testFilePath = '/path/to/test-file.json';
const testFileBase64 = 'dGVzdCBmaWxlIGNvbnRlbnQ=';
const testFileName = 'test-file.json';
const mockVariable = {
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Project,
environments: [DefaultEnvironment.Development],
type: EnvironmentSecretType.String,
};
const mockUpdatedVariable = {
...mockVariable,
type: EnvironmentSecretType.FileBase64,
};
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue([mockVariable]);
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockUpdatedVariable);
// Mock fs.pathExists to return true
jest.mocked(fs.pathExists).mockImplementation(() => Promise.resolve(true));
// Mock fs.readFile to return base64 encoded content
jest.mocked(fs.readFile).mockImplementation(() => Promise.resolve(testFileBase64));
const command = new EnvUpdate(
[
'--variable-name',
'TEST_VARIABLE',
'--variable-environment',
'development',
'--non-interactive',
'--type',
'file',
'--value',
testFilePath,
'--environment',
'production',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
// Verify file was checked for existence
expect(fs.pathExists).toHaveBeenCalledWith(testFilePath);
// Verify file was read as base64
expect(fs.readFile).toHaveBeenCalledWith(testFilePath, 'base64');
// Verify update was called with FileBase64 type and base64-encoded value
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
id: variableId,
value: testFileBase64,
environments: [DefaultEnvironment.Production],
type: EnvironmentSecretType.FileBase64,
fileName: testFileName,
});
});
it('throws error when updating FILE type variable with value that is not a valid file path', async () => {
const plainStringValue = 'some-new-value';
const mockVariable = {
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Project,
environments: [DefaultEnvironment.Development],
type: EnvironmentSecretType.FileBase64,
};
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue([mockVariable]);
// Mock fs.pathExists to return false (file doesn't exist)
jest.mocked(fs.pathExists).mockImplementation(() => Promise.resolve(false));
const command = new EnvUpdate(
[
'--variable-name',
'TEST_VARIABLE',
'--variable-environment',
'development',
'--non-interactive',
'--value',
plainStringValue,
'--environment',
'production',
// Note: --type is NOT specified
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
// Should throw error when FILE type variable is updated with non-file value
await expect(command.runAsync()).rejects.toThrow(
`Variable "TEST_VARIABLE" is a file type, but "${plainStringValue}" does not exist as a file. If you want to convert it to a string, pass --type string.`
);
});
it('throws error when --type file is explicitly specified but file does not exist', async () => {
const nonExistentFilePath = '/path/to/nonexistent-file.json';
const mockVariable = {
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Project,
environments: [DefaultEnvironment.Development],
type: EnvironmentSecretType.String,
};
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue([mockVariable]);
// Mock fs.pathExists to return false (file doesn't exist)
jest.mocked(fs.pathExists).mockImplementation(() => Promise.resolve(false));
const command = new EnvUpdate(
[
'--variable-name',
'TEST_VARIABLE',
'--variable-environment',
'development',
'--non-interactive',
'--type',
'file',
'--value',
nonExistentFilePath,
'--environment',
'production',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
// Should throw error when --type file is explicitly specified
await expect(command.runAsync()).rejects.toThrow(
`File "${nonExistentFilePath}" does not exist`
);
});
it('preserves existing STRING type when updating value without --type flag', async () => {
const stringValue = 'my-best-file.json';
const mockVariable = {
id: variableId,
name: 'MY_VAR',
scope: EnvironmentVariableScope.Project,
environments: [DefaultEnvironment.Development],
type: EnvironmentSecretType.String,
value: 'old-value',
visibility: EnvironmentVariableVisibility.Public,
};
const mockUpdatedVariable = {
...mockVariable,
value: stringValue,
};
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue([mockVariable]);
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockUpdatedVariable);
const command = new EnvUpdate(
[
'--variable-name',
'MY_VAR',
'--variable-environment',
'development',
'--non-interactive',
'--value',
stringValue,
// Note: --type is NOT specified
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
// Verify update was called with preserved STRING type
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
id: variableId,
value: stringValue,
type: EnvironmentSecretType.String,
});
});
it('preserves existing FILE type when updating value without --type flag', async () => {
const testFilePath = '/path/to/test-file.json';
const testFileBase64 = 'dGVzdCBmaWxlIGNvbnRlbnQ=';
const testFileName = 'test-file.json';
const mockVariable = {
id: variableId,
name: 'MY_FILE_VAR',
scope: EnvironmentVariableScope.Project,
environments: [DefaultEnvironment.Development],
type: EnvironmentSecretType.FileBase64,
value: 'old-base64-value',
visibility: EnvironmentVariableVisibility.Public,
};
const mockUpdatedVariable = {
...mockVariable,
value: testFileBase64,
};
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue([mockVariable]);
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockUpdatedVariable);
// Mock fs.pathExists to return true
jest.mocked(fs.pathExists).mockImplementation(() => Promise.resolve(true));
// Mock fs.readFile to return base64 encoded content
jest.mocked(fs.readFile).mockImplementation(() => Promise.resolve(testFileBase64));
const command = new EnvUpdate(
[
'--variable-name',
'MY_FILE_VAR',
'--variable-environment',
'development',
'--non-interactive',
'--value',
testFilePath,
// Note: --type is NOT specified
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
// Verify file was checked for existence
expect(fs.pathExists).toHaveBeenCalledWith(testFilePath);
// Verify file was read as base64
expect(fs.readFile).toHaveBeenCalledWith(testFilePath, 'base64');
// Verify update was called with preserved FILE type
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
id: variableId,
value: testFileBase64,
type: EnvironmentSecretType.FileBase64,
fileName: testFileName,
});
});
});
+20 -12
View File
@@ -299,18 +299,6 @@ export default class EnvUpdate extends EasCommand {
}
}
let environmentFilePath: string | undefined;
if ((newType ?? selectedVariable.type) === EnvironmentSecretType.FileBase64 && value) {
environmentFilePath = path.resolve(value);
if (!(await fs.pathExists(environmentFilePath))) {
throw new Error(`File "${value}" does not exist`);
}
fileName = path.basename(environmentFilePath);
}
value = environmentFilePath ? await fs.readFile(environmentFilePath, 'base64') : value;
if (!environments || environments.length === 0) {
environments = await promptVariableEnvironmentAsync({
nonInteractive,
@@ -342,6 +330,26 @@ export default class EnvUpdate extends EasCommand {
}
}
// If value is provided but type is not explicitly set, preserve the existing type
if (value && !newType) {
newType = selectedVariable.type;
}
if (newType === EnvironmentSecretType.FileBase64 && value) {
const environmentFilePath = path.resolve(value);
if (!(await fs.pathExists(environmentFilePath))) {
if (type === 'file') {
throw new Error(`File "${value}" does not exist`);
} else {
throw new Error(
`Variable "${selectedVariable.name}" is a file type, but "${value}" does not exist as a file. If you want to convert it to a string, pass --type string.`
);
}
}
fileName = path.basename(environmentFilePath);
value = await fs.readFile(environmentFilePath, 'base64');
}
if (visibility) {
newVisibility = parseVisibility(visibility);
}
@@ -51,7 +51,15 @@ jest.mock('../../../project/expoConfig', () => ({
jest.mock('../../../prompts', () => ({
...jest.requireActual('../../../prompts'),
promptAsync: jest.fn().mockResolvedValue({ account: { name: 'jester' } }),
promptAsync: jest.fn().mockImplementation(async (options: any) => {
if (options.name === 'account') {
return { account: { name: 'jester' } };
}
if (options.name === 'name') {
return { name: options.initial || 'expo-project' };
}
return {};
}),
}));
describe(New.name, () => {
@@ -62,8 +70,8 @@ describe(New.name, () => {
expect(result).toEqual({
projectAccount: 'jester',
projectDirectory: expect.stringContaining('/new-expo-project'),
projectName: 'new-expo-project',
projectDirectory: expect.stringContaining('/expo-project'),
projectName: 'expo-project',
});
});
+22 -13
View File
@@ -21,6 +21,7 @@ import {
updatePackageJsonAsync,
updateReadmeAsync,
} from '../../commandUtils/new/projectFiles';
import { printDirectory } from '../../commandUtils/new/utils';
import { AppFragment } from '../../graphql/generated';
import { AppMutation } from '../../graphql/mutations/AppMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
@@ -40,7 +41,7 @@ export async function generateConfigsAsync(
projectAccount: string;
}> {
const projectAccount = await promptForProjectAccountAsync(actor);
const { projectName, projectDirectory } = await generateProjectConfigAsync(actor, args.path, {
const { projectName, projectDirectory } = await generateProjectConfigAsync(args.path, {
graphqlClient,
projectAccount,
});
@@ -92,7 +93,7 @@ export async function createProjectAsync({
},
{ skipSDKVersionRequirement: true }
);
Log.withTick(`Project successfully linked (ID: ${chalk.bold(projectId)}) (modified app.json)`);
Log.withInfo(`Project successfully linked (ID: ${chalk.bold(projectId)})`);
return projectId;
}
@@ -102,6 +103,7 @@ export async function generateProjectFilesAsync(
app: AppFragment,
packageManager: PackageManager
): Promise<void> {
const spinner = ora(`Generating project files`).start();
await generateAppConfigAsync(projectDir, app);
await generateEasConfigAsync(projectDir);
@@ -111,6 +113,17 @@ export async function generateProjectFilesAsync(
await copyProjectTemplatesAsync(projectDir);
await updateReadmeAsync(projectDir, packageManager);
spinner.succeed(`Generated project files`);
Log.withInfo(
`Generated ${chalk.bold('app.json')}. ${learnMore(
'https://docs.expo.dev/versions/latest/config/app/'
)}`
);
Log.withInfo(
`Generated ${chalk.bold('eas.json')}. ${learnMore(
'https://docs.expo.dev/build-reference/eas-json/'
)}`
);
}
export default class New extends EasCommand {
@@ -136,8 +149,6 @@ export default class New extends EasCommand {
}),
};
static override hidden = true;
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
};
@@ -155,11 +166,7 @@ export default class New extends EasCommand {
);
}
Log.warn(
'This command is not yet implemented. It will create a new project, but it may not be fully configured.'
);
Log.log(`👋 Welcome to Expo, ${actor.username}!`);
Log.newLine();
const {
projectName,
@@ -186,19 +193,21 @@ export default class New extends EasCommand {
await initializeGitRepositoryAsync(projectDirectory);
Log.log('🎉 We finished creating your new project.');
Log.newLine();
Log.log('Next steps:');
Log.withInfo(`Run \`cd ${projectDirectory}\` to navigate to your project.`);
Log.withInfo(
`Run \`${packageManager} run preview\` to create a preview build on EAS. ${learnMore(
`Run ${chalk.bold(`cd ${printDirectory(projectDirectory)}`)} to navigate to your project.`
);
Log.withInfo(
`Run ${chalk.bold(`${packageManager} run draft`)} to create a preview on EAS. ${learnMore(
'https://docs.expo.dev/eas/workflows/examples/publish-preview-update/'
)}`
);
Log.withInfo(
`Run \`${packageManager} run start\` to start developing locally. ${learnMore(
`Run ${chalk.bold(`${packageManager} run start`)} to start developing locally. ${learnMore(
'https://docs.expo.dev/get-started/start-developing/'
)}`
);
Log.withInfo(`See the README.md for more information about your project.`);
Log.newLine();
Log.withInfo(`See the ${chalk.bold('README.md')} for more information about your project.`);
}
}
+10 -2
View File
@@ -31,7 +31,6 @@ import { maybeUploadFingerprintAsync } from '../../project/maybeUploadFingerprin
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import {
RawAsset,
UpdatePublishPlatform,
buildBundlesAsync,
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
@@ -51,7 +50,12 @@ import {
} from '../../project/publish';
import { resolveWorkflowPerPlatformAsync } from '../../project/workflow';
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
import { getUpdateJsonInfosForUpdates } from '../../update/utils';
import {
UpdatePublishPlatform,
getUpdateJsonInfosForUpdates,
isBundleDiffingEnabled,
prewarmDiffingAsync,
} from '../../update/utils';
import {
checkManifestBodyAgainstUpdateInfoGroup,
getCodeSigningInfoAsync,
@@ -571,6 +575,10 @@ export default class UpdatePublish extends EasCommand {
throw e;
}
if (isBundleDiffingEnabled(exp)) {
await prewarmDiffingAsync(graphqlClient, projectId, newUpdates);
}
if (!skipBundler && emitMetadata) {
Log.log('Generating eas-update-metadata.json');
await generateEasMetadataAsync(distRoot, getUpdateJsonInfosForUpdates(newUpdates));
@@ -7,7 +7,7 @@ import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/creat
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import Log, { learnMore } from '../../log';
import { enforceRollBackToEmbeddedUpdateSupportAsync } from '../../project/projectUtils';
import { UpdatePublishPlatform, getUpdateMessageForCommandAsync } from '../../project/publish';
import { getUpdateMessageForCommandAsync } from '../../project/publish';
import { confirmAsync, promptAsync } from '../../prompts';
import { scheduleUpdateGroupDeletionAsync } from '../../update/delete';
import {
@@ -18,6 +18,7 @@ import {
republishAsync,
} from '../../update/republish';
import { publishRollBackToEmbeddedUpdateAsync } from '../../update/roll-back-to-embedded';
import { UpdatePublishPlatform } from '../../update/utils';
import { CodeSigningInfo, getCodeSigningInfoAsync } from '../../utils/code-signing';
import { enableJsonOutput } from '../../utils/json';
import { pollForBackgroundJobReceiptAsync } from '../../utils/pollForBackgroundJobReceiptAsync';
@@ -9,7 +9,6 @@ import { StatuspageServiceName } from '../../graphql/generated';
import { RequestedPlatform } from '../../platform';
import { enforceRollBackToEmbeddedUpdateSupportAsync } from '../../project/projectUtils';
import {
UpdatePublishPlatform,
defaultPublishPlatforms,
getBranchNameForCommandAsync,
getUpdateMessageForCommandAsync,
@@ -17,6 +16,7 @@ import {
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
import { selectRuntimeOnBranchAsync } from '../../update/queries';
import { publishRollBackToEmbeddedUpdateAsync } from '../../update/roll-back-to-embedded';
import { UpdatePublishPlatform } from '../../update/utils';
import { getCodeSigningInfoAsync } from '../../utils/code-signing';
import { enableJsonOutput } from '../../utils/json';
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
@@ -1,4 +1,8 @@
import { parseInputs, parseJsonInputs, parseWorkflowInputsFromYaml } from '../run';
import {
parseInputs,
parseJsonInputs,
parseWorkflowInputsFromYaml,
} from '../../../commandUtils/workflow/inputs';
describe('parseInputs', () => {
it('should parse single key=value pair', () => {
@@ -216,7 +220,6 @@ jobs:
it('should handle invalid YAML gracefully', () => {
const yamlConfig = 'invalid: yaml: content:';
const { parseWorkflowInputsFromYaml } = require('../run');
const inputs = parseWorkflowInputsFromYaml(yamlConfig);
expect(inputs).toEqual({});
+23 -327
View File
@@ -44,41 +44,40 @@ import spawnAsync from '@expo/spawn-async';
import { Flags } from '@oclif/core';
import { CombinedError } from '@urql/core';
import chalk from 'chalk';
import * as fs from 'node:fs';
import { boolish } from 'getenv';
import * as path from 'node:path';
import slash from 'slash';
import * as YAML from 'yaml';
import { z } from 'zod';
import { getWorkflowRunUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EASNonInteractiveFlag, EasJsonOnlyFlag } from '../../commandUtils/flags';
import {
maybePromptForMissingInputsAsync,
parseInputs,
parseJsonInputs,
parseWorkflowInputsFromYaml,
} from '../../commandUtils/workflow/inputs';
import {
fileExistsAsync,
maybeReadStdinAsync,
showWorkflowStatusAsync,
workflowRunExitCodes,
} from '../../commandUtils/workflow/utils';
import {
WorkflowProjectSourceType,
WorkflowRevision,
WorkflowRunByIdQuery,
WorkflowRunStatus,
} from '../../graphql/generated';
import { WorkflowRevisionMutation } from '../../graphql/mutations/WorkflowRevisionMutation';
import { WorkflowRunMutation } from '../../graphql/mutations/WorkflowRunMutation';
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
import Log, { link } from '../../log';
import { ora } from '../../ora';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { uploadAccountScopedFileAsync } from '../../project/uploadAccountScopedFileAsync';
import { uploadAccountScopedProjectSourceAsync } from '../../project/uploadAccountScopedProjectSourceAsync';
import { promptAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { sleepAsync } from '../../utils/promise';
import { WorkflowFile } from '../../utils/workflowFile';
const EXIT_CODES = {
WORKFLOW_FAILED: 11,
WORKFLOW_CANCELED: 12,
WAIT_ABORTED: 13,
};
export default class WorkflowRun extends EasCommand {
static override description =
'run an EAS workflow. The entire local project directory will be packaged and uploaded to EAS servers for the workflow run, unless the --ref flag is used.';
@@ -91,7 +90,7 @@ export default class WorkflowRun extends EasCommand {
default: false,
allowNo: true,
description: 'Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.',
summary: 'Wait for workflow run to complete',
summary: 'Wait for workflow run to complete. Defaults to false.',
}),
input: Flags.string({
char: 'F',
@@ -136,7 +135,9 @@ export default class WorkflowRun extends EasCommand {
const {
projectId,
exp: { slug: projectName },
} = await getDynamicPrivateProjectConfigAsync();
} = await getDynamicPrivateProjectConfigAsync({
skipPlugins: true,
});
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
let yamlConfig: string;
@@ -358,9 +359,11 @@ export default class WorkflowRun extends EasCommand {
process.exit(0);
}
Log.newLine();
const { status } = await waitForWorkflowRunToEndAsync(graphqlClient, {
const spinnerUsesStdErr = boolish('CI', false) || flags.json;
const { status } = await showWorkflowStatusAsync(graphqlClient, {
workflowRunId,
spinnerUsesStdErr,
});
if (flags.json) {
@@ -375,316 +378,9 @@ export default class WorkflowRun extends EasCommand {
}
if (status === WorkflowRunStatus.Failure) {
process.exit(EXIT_CODES.WORKFLOW_FAILED);
process.exit(workflowRunExitCodes.WORKFLOW_FAILED);
} else if (status === WorkflowRunStatus.Canceled) {
process.exit(EXIT_CODES.WORKFLOW_CANCELED);
}
}
}
async function waitForWorkflowRunToEndAsync(
graphqlClient: ExpoGraphqlClient,
{ workflowRunId }: { workflowRunId: string }
): Promise<WorkflowRunByIdQuery['workflowRuns']['byId']> {
Log.log('Waiting for workflow run to complete. You can press Ctrl+C to exit.');
const spinner = ora('Currently waiting for workflow run to start.').start();
let failedFetchesCount = 0;
while (true) {
try {
const workflowRun = await WorkflowRunQuery.byIdAsync(graphqlClient, workflowRunId, {
useCache: false,
});
failedFetchesCount = 0;
switch (workflowRun.status) {
case WorkflowRunStatus.InProgress:
spinner.start('Workflow run is in progress.');
break;
case WorkflowRunStatus.ActionRequired:
spinner.warn('Workflow run is waiting for action.');
break;
case WorkflowRunStatus.Canceled:
spinner.warn('Workflow run has been canceled.');
return workflowRun;
case WorkflowRunStatus.Failure:
spinner.fail('Workflow run has failed.');
return workflowRun;
case WorkflowRunStatus.Success:
spinner.succeed('Workflow run completed successfully.');
return workflowRun;
}
} catch {
spinner.text = '⚠ Failed to fetch the workflow run status. Check your network connection.';
failedFetchesCount += 1;
if (failedFetchesCount > 6) {
spinner.fail('Failed to fetch the workflow run status 6 times in a row. Aborting wait.');
process.exit(EXIT_CODES.WAIT_ABORTED);
}
}
await sleepAsync(10 /* seconds */ * 1000 /* milliseconds */);
}
}
async function fileExistsAsync(filePath: string): Promise<boolean> {
return await fs.promises
.access(filePath, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
}
export function parseInputs(inputFlags: string[]): Record<string, string> {
const inputs: Record<string, string> = {};
for (const inputFlag of inputFlags) {
const equalIndex = inputFlag.indexOf('=');
if (equalIndex === -1) {
throw new Error(`Invalid input format: ${inputFlag}. Expected key=value format.`);
}
const key = inputFlag.substring(0, equalIndex);
const value = inputFlag.substring(equalIndex + 1);
if (!key) {
throw new Error(`Invalid input format: ${inputFlag}. Key cannot be empty.`);
}
inputs[key] = value;
}
return inputs;
}
export async function maybeReadStdinAsync(): Promise<string | null> {
// Check if there's data on stdin
if (process.stdin.isTTY) {
return null;
}
return await new Promise((resolve, reject) => {
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('readable', () => {
let chunk;
while ((chunk = process.stdin.read()) !== null) {
data += chunk;
}
});
process.stdin.on('end', () => {
const trimmedData = data.trim();
resolve(trimmedData || null);
});
process.stdin.on('error', err => {
reject(err);
});
});
}
export function parseJsonInputs(jsonString: string): Record<string, string> {
try {
const parsed = JSON.parse(jsonString);
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw new Error('JSON input must be an object.');
}
return parsed;
} catch (error) {
throw new Error(`Invalid JSON input.`, { cause: error });
}
}
// `z.coerce.boolean()` does `Boolean(val)` under the hood,
// which is not what we want. See:
// https://github.com/colinhacks/zod/issues/2985#issuecomment-2230692578
const booleanLike = z.union([
z.boolean(),
z.codec(z.number(), z.boolean(), {
decode: n => !!n,
encode: b => (b ? 1 : 0),
}),
z.stringbool({ truthy: ['true', 'True'], falsy: ['false', 'False'] }),
]);
const stringLike = z.codec(
z.union([
// We're going to coerce numbers and strings into strings.
z.number(),
z.string(),
// We do not allow other primitives, like:
// - bigints, symbols - because YAML does not support them,
// - booleans - because YAML accepts `True` and `true` as boolean input
// and parses both as JS `true` -- if we stringified that,
// we would lose the capital "T" which may not be what the user expects,
// - nulls - user should do `"null"` or not pass the property at all.
]),
z.string(),
{
decode: value => {
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number') {
return String(value);
}
throw new Error(`Cannot convert ${typeof value} to string: ${value}`);
},
encode: value => value,
}
);
export const WorkflowDispatchInputZ = z
.object({
description: stringLike.optional().describe('Description of the input'),
required: booleanLike.default(false).describe('Whether the input is required.'),
})
.and(
z.union([
z.object({
type: z.literal('string').default('string'),
default: stringLike.optional().describe('Default value for the input'),
}),
z.object({
type: z.literal('boolean'),
default: booleanLike.optional().describe('Default value for the input'),
}),
z.object({
type: z.literal('number'),
default: z.number().optional().describe('Default value for the input'),
}),
z.object({
type: z.literal('choice'),
default: stringLike.optional().describe('Default value for the input'),
options: z.array(stringLike).min(1).describe('Options for choice type inputs'),
}),
z.object({
type: z.literal('environment'),
default: z.string().optional().describe('Default value for the input'),
}),
])
);
export function parseWorkflowInputsFromYaml(
yamlConfig: string
): Record<string, z.infer<typeof WorkflowDispatchInputZ>> {
try {
const parsed = YAML.parse(yamlConfig);
return z
.record(z.string(), WorkflowDispatchInputZ)
.default({})
.parse(parsed?.on?.workflow_dispatch?.inputs);
} catch (error) {
Log.warn('Failed to parse workflow inputs from YAML:', error);
return {};
}
}
async function maybePromptForMissingInputsAsync({
inputSpecs,
inputs,
}: {
inputSpecs: Record<string, z.infer<typeof WorkflowDispatchInputZ>>;
inputs: Record<string, unknown>;
}): Promise<Record<string, unknown>> {
const requiredInputs = Object.entries(inputSpecs).filter(([_, spec]) => spec.required);
const missingRequiredInputs = requiredInputs.filter(([key]) => inputs[key] === undefined);
if (missingRequiredInputs.length === 0) {
return inputs;
}
Log.addNewLineIfNone();
Log.log('Some required inputs are missing. Please provide them:');
const nextInputs = { ...inputs };
for (const [key, spec] of missingRequiredInputs) {
const value = await promptForMissingInputAsync({ key, spec });
nextInputs[key] = value;
}
return nextInputs;
}
async function promptForMissingInputAsync({
key,
spec,
}: {
key: string;
spec: z.infer<typeof WorkflowDispatchInputZ>;
}): Promise<unknown> {
const message = spec.description ? `${key} (${spec.description})` : key;
switch (spec.type) {
case 'boolean': {
const { value } = await promptAsync({
type: 'confirm',
name: 'value',
message,
initial: spec.default,
});
return value;
}
case 'number': {
const { value } = await promptAsync({
type: 'number',
name: 'value',
message,
initial: spec.default,
validate: (val: number) => {
if (isNaN(val)) {
return 'Please enter a valid number';
}
return true;
},
});
return value;
}
case 'choice': {
const { value } = await promptAsync({
type: 'select',
name: 'value',
message,
choices: spec.options.map(option => ({
title: option,
value: option,
})),
initial: spec.default,
});
return value;
}
case 'string':
case 'environment': {
const { value } = await promptAsync({
type: 'text',
name: 'value',
message,
initial: spec.default,
validate: (val: string) => {
if (spec.required && (!val || val.trim() === '')) {
return 'This field is required';
}
return true;
},
});
return value;
process.exit(workflowRunExitCodes.WORKFLOW_CANCELED);
}
}
}
@@ -0,0 +1,138 @@
/**
* EAS Workflow Status Command
*
* This command shows the status of an existing workflow run.
*
* If no run ID is provided, you will be prompted to select from recent workflow runs for the current project.
*
* If the selected run is still in progress, the command will show the progress of the run, with an option
* to show periodic status updates while waiting for completion (similar to `eas workflow:run --wait`).
*
*/
import { Flags } from '@oclif/core';
import { boolish } from 'getenv';
import { getWorkflowRunUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag, EasJsonOnlyFlag } from '../../commandUtils/flags';
import {
choiceFromWorkflowRun,
processWorkflowRuns,
showWorkflowStatusAsync,
workflowRunExitCodes,
} from '../../commandUtils/workflow/utils';
import { WorkflowRunStatus } from '../../graphql/generated';
import { AppQuery } from '../../graphql/queries/AppQuery';
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
import Log, { link } from '../../log';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class WorkflowStatus extends EasCommand {
static override description =
'show the status of an existing workflow run. If no run ID is provided, you will be prompted to select from recent workflow runs for the current project.';
static override args = [
{
name: 'WORKFLOW_RUN_ID',
description: 'A workflow run ID.',
},
];
static override flags = {
...EASNonInteractiveFlag,
wait: Flags.boolean({
default: false,
allowNo: true,
description: 'Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.',
summary: 'Wait for workflow run to complete. Defaults to false.',
}),
...EasJsonOnlyFlag,
};
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Vcs,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const { flags, args } = await this.parse(WorkflowStatus);
if (flags.json) {
enableJsonOutput();
}
const {
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
} = await this.getContextAsync(WorkflowStatus, {
nonInteractive: flags['non-interactive'],
withServerSideEnvironment: null,
});
const {
projectId,
exp: { slug: projectName },
} = await getDynamicPrivateProjectConfigAsync();
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
let workflowRunId = args.WORKFLOW_RUN_ID;
if (!workflowRunId && flags['non-interactive']) {
throw new Error('Workflow run ID is required in non-interactive mode');
}
if (!workflowRunId) {
const queryResult = await AppQuery.byIdWorkflowRunsFilteredByStatusAsync(
graphqlClient,
projectId,
undefined,
50
);
const runs = processWorkflowRuns(queryResult);
if (runs.length === 0) {
Log.warn('No workflow runs to show');
return;
}
const answers = await promptAsync({
type: 'select',
name: 'selectedRun',
message: 'Select a workflow run:',
choices: runs.map(run => choiceFromWorkflowRun(run)),
});
workflowRunId = answers.selectedRun;
}
Log.addNewLineIfNone();
Log.log(`See logs: ${link(getWorkflowRunUrl(account.name, projectName, workflowRunId))}`);
Log.addNewLineIfNone();
const spinnerUsesStdErr = boolish('CI', false) || flags.json;
await showWorkflowStatusAsync(graphqlClient, {
workflowRunId,
spinnerUsesStdErr,
waitForCompletion: flags.wait,
});
const workflowRun = await WorkflowRunQuery.withJobsByIdAsync(graphqlClient, workflowRunId, {
useCache: false,
});
const status = workflowRun.status;
if (flags.json) {
printJsonOnlyOutput({
...workflowRun,
url: getWorkflowRunUrl(account.name, projectName, workflowRunId),
});
}
if (status === WorkflowRunStatus.Failure) {
process.exit(workflowRunExitCodes.WORKFLOW_FAILED);
} else if (status === WorkflowRunStatus.Canceled) {
process.exit(workflowRunExitCodes.WORKFLOW_CANCELED);
}
}
}
@@ -1,5 +1,6 @@
import { JSONObject } from '@expo/json-file';
import type { XCBuildConfiguration } from 'xcode';
import { z } from 'zod';
import {
AccountFragment,
@@ -33,3 +34,42 @@ export type IosCredentials = Record<string, TargetCredentials>;
export type IosAppBuildCredentialsMap = Record<string, IosAppBuildCredentialsFragment>;
export type IosAppCredentialsMap = Record<string, CommonIosAppCredentialsFragment | null>;
// `z.coerce.boolean()` does `Boolean(val)` under the hood,
// which is not what we want. See:
// https://github.com/colinhacks/zod/issues/2985#issuecomment-2230692578
export const booleanLike = z.union([
z.boolean(),
z.codec(z.number(), z.boolean(), {
decode: n => !!n,
encode: b => (b ? 1 : 0),
}),
z.stringbool({ truthy: ['true', 'True'], falsy: ['false', 'False'] }),
]);
export const stringLike = z.codec(
z.union([
// We're going to coerce numbers and strings into strings.
z.number(),
z.string(),
// We do not allow other primitives, like:
// - bigints, symbols - because YAML does not support them,
// - booleans - because YAML accepts `True` and `true` as boolean input
// and parses both as JS `true` -- if we stringified that,
// we would lose the capital "T" which may not be what the user expects,
// - nulls - user should do `"null"` or not pass the property at all.
]),
z.string(),
{
decode: value => {
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number') {
return String(value);
}
throw new Error(`Cannot convert ${typeof value} to string: ${value}`);
},
encode: value => value,
}
);
+477 -26
View File
@@ -159,6 +159,7 @@ export type Account = {
pendingSentryInstallation?: Maybe<PendingSentryInstallation>;
profileImageUrl: Scalars['String']['output'];
pushSecurityEnabled: Scalars['Boolean']['output'];
requireTwoFactor: Scalars['Boolean']['output'];
/** @deprecated Legacy access tokens are deprecated */
requiresAccessTokenForPushSecurity: Scalars['Boolean']['output'];
sentryInstallation?: Maybe<SentryInstallation>;
@@ -186,8 +187,13 @@ export type Account = {
userActorOwner?: Maybe<UserActor>;
/** Pending user invitations for this account */
userInvitations: Array<UserInvitation>;
userSpecifiedAccountUsage?: Maybe<UserSpecifiedAccountUsage>;
/** Actors associated with this account and permissions they hold */
users: Array<UserPermission>;
/** Vexo account connection for this account */
vexoAccountConnection?: Maybe<VexoAccountConnection>;
/** Notification preferences of the viewer for this account */
viewerNotificationPreferences: Array<NotificationPreferenceItem>;
/** Permission info for the viewer on this account */
viewerUserPermission: UserPermission;
/** @deprecated Build packs are no longer supported */
@@ -539,6 +545,7 @@ export enum AccountAppsSortByField {
export type AccountDataInput = {
displayName?: InputMaybe<Scalars['String']['input']>;
name: Scalars['String']['input'];
userSpecifiedAccountUsage?: InputMaybe<UserSpecifiedAccountUsage>;
};
export type AccountGoogleServiceAccountKeysConnection = {
@@ -577,6 +584,15 @@ export type AccountMutation = {
setDisplayName: Account;
/** Require authorization to send push notifications for experiences owned by this account */
setPushSecurityEnabled: Account;
/**
* Set whether two-factor authentication is required for this account. When enabled:
* - Existing members with 2FA cannot disable it
* - New members must enable 2FA before joining
* - Existing members without 2FA are not affected
*/
setRequireTwoFactor: Account;
/** Set the user specified account usage for the account. */
setUserSpecifiedAccountUsage: Account;
};
@@ -647,6 +663,34 @@ export type AccountMutationSetPushSecurityEnabledArgs = {
pushSecurityEnabled: Scalars['Boolean']['input'];
};
export type AccountMutationSetRequireTwoFactorArgs = {
accountID: Scalars['ID']['input'];
requireTwoFactor: Scalars['Boolean']['input'];
};
export type AccountMutationSetUserSpecifiedAccountUsageArgs = {
accountID: Scalars['ID']['input'];
userSpecifiedAccountUsage: UserSpecifiedAccountUsage;
};
/** Account-level notification preference */
export type AccountNotificationPreference = NotificationPreference & {
__typename?: 'AccountNotificationPreference';
account: Account;
enabled: Scalars['Boolean']['output'];
event: NotificationEvent;
type: NotificationType;
};
export type AccountNotificationPreferenceInput = {
accountId: Scalars['ID']['input'];
enabled: Scalars['Boolean']['input'];
event: NotificationEvent;
type: NotificationType;
};
export type AccountNotificationSubscriptionInput = {
accountId: Scalars['ID']['input'];
event: NotificationEvent;
@@ -1269,6 +1313,7 @@ export type App = Project & {
appStoreUrl?: Maybe<Scalars['String']['output']>;
assetLimitPerUpdateGroup: Scalars['Int']['output'];
branchesPaginated: AppBranchesConnection;
buildProfiles: Array<Scalars['String']['output']>;
/** (EAS Build) Builds associated with this app */
builds: Array<Build>;
buildsPaginated: AppBuildsConnection;
@@ -1416,6 +1461,9 @@ export type App = Project & {
username: Scalars['String']['output'];
/** @deprecated No longer supported */
users?: Maybe<Array<Maybe<User>>>;
vexoApp?: Maybe<VexoApp>;
/** Notification preferences of the viewer for this app */
viewerNotificationPreferences: Array<NotificationPreferenceItem>;
/** Webhooks for an app */
webhooks: Array<Webhook>;
workerCustomDomain?: Maybe<WorkerCustomDomain>;
@@ -1430,6 +1478,7 @@ export type App = Project & {
workflowRunGitBranchesPaginated: AppWorkflowRunGitBranchesConnection;
workflowRunsPaginated: AppWorkflowRunsConnection;
workflows: Array<Workflow>;
workflowsInsights: AppWorkflowsInsights;
};
@@ -1918,6 +1967,22 @@ export type AppMutationSetResourceClassExperimentArgs = {
resourceClassExperiment?: InputMaybe<ResourceClassExperiment>;
};
/** App-level notification preference */
export type AppNotificationPreference = NotificationPreference & {
__typename?: 'AppNotificationPreference';
app: App;
enabled: Scalars['Boolean']['output'];
event: NotificationEvent;
type: NotificationType;
};
export type AppNotificationPreferenceInput = {
appId: Scalars['ID']['input'];
enabled: Scalars['Boolean']['input'];
event: NotificationEvent;
type: NotificationType;
};
export type AppNotificationSubscriptionInput = {
appId: Scalars['ID']['input'];
event: NotificationEvent;
@@ -2235,6 +2300,7 @@ export type AppWorkflowRunEdge = {
export type AppWorkflowRunFilterInput = {
requestedGitRef?: InputMaybe<Scalars['String']['input']>;
status?: InputMaybe<WorkflowRunStatus>;
timeRange?: InputMaybe<WorkflowRunTimeRangeInput>;
};
export type AppWorkflowRunGitBranchEdge = {
@@ -2255,12 +2321,45 @@ export type AppWorkflowRunGitBranchesConnection = {
pageInfo: PageInfo;
};
export type AppWorkflowRunTimeRangeInput = {
earliest?: InputMaybe<Scalars['DateTime']['input']>;
latest?: InputMaybe<Scalars['DateTime']['input']>;
};
export type AppWorkflowRunsConnection = {
__typename?: 'AppWorkflowRunsConnection';
edges: Array<AppWorkflowRunEdge>;
pageInfo: PageInfo;
};
export type AppWorkflowsInsights = {
__typename?: 'AppWorkflowsInsights';
overviewMetrics: WorkflowsInsightsOverviewMetrics;
runsOverTime: WorkflowsInsightsRunsOverTimeData;
workflows: WorkflowsInsightsWorkflowConnection;
};
export type AppWorkflowsInsightsOverviewMetricsArgs = {
filters?: InputMaybe<WorkflowsInsightsFiltersInput>;
timespan: WorkflowsInsightsTimespanInput;
};
export type AppWorkflowsInsightsRunsOverTimeArgs = {
filters?: InputMaybe<WorkflowsInsightsFiltersInput>;
granularity: WorkflowsInsightsRunsOverTimeGranularity;
timespan: WorkflowsInsightsTimespanInput;
};
export type AppWorkflowsInsightsWorkflowsArgs = {
after?: InputMaybe<Scalars['String']['input']>;
filters?: InputMaybe<WorkflowsInsightsFiltersInput>;
first: Scalars['Int']['input'];
timespan: WorkflowsInsightsTimespanInput;
};
export type AppleAppIdentifier = {
__typename?: 'AppleAppIdentifier';
account: Account;
@@ -3112,6 +3211,8 @@ export type BuildFilter = {
};
export type BuildFilterInput = {
buildModes?: InputMaybe<Array<BuildMode>>;
buildProfile?: InputMaybe<Scalars['String']['input']>;
channel?: InputMaybe<Scalars['String']['input']>;
developmentClient?: InputMaybe<Scalars['Boolean']['input']>;
distributions?: InputMaybe<Array<DistributionType>>;
@@ -3672,6 +3773,21 @@ export type CreateSubmissionResult = {
submission: Submission;
};
export type CreateVexoAccountConnectionInput = {
accountId: Scalars['ID']['input'];
code: Scalars['String']['input'];
};
export type CreateVexoAppInput = {
appId: Scalars['ID']['input'];
domain?: InputMaybe<Scalars['String']['input']>;
iconUrl?: InputMaybe<Scalars['String']['input']>;
name: Scalars['String']['input'];
owner: Scalars['String']['input'];
slug: Scalars['String']['input'];
vexoIdentifier: Scalars['String']['input'];
};
export type CumulativeAverageMetrics = {
__typename?: 'CumulativeAverageMetrics';
averageUpdatePayloadBytes: Scalars['Int']['output'];
@@ -5613,7 +5729,10 @@ export type MeMutation = {
scheduleCurrentUserDeletion: BackgroundJobReceipt;
/** Schedule deletion of a SSO user. Actor must be an owner on the SSO user's SSO account. */
scheduleSSOUserDeletionAsSSOAccountOwner: BackgroundJobReceipt;
/** Send SMS OTP to a second factor device for use during device setup or during change confirmation */
/**
* Send SMS OTP to a second factor device for use during device setup or during change confirmation
* @deprecated This method is no longer supported.
*/
sendSMSOTPToSecondFactorDevice: SecondFactorBooleanResult;
/**
* Sets user preferences. This is a key-value store for user-specific settings. Provided values are
@@ -5757,6 +5876,32 @@ export enum NotificationEvent {
export type NotificationMetadata = BuildLimitThresholdExceededMetadata | BuildPlanCreditThresholdExceededMetadata | TestNotificationMetadata;
/** Base interface for notification preferences */
export type NotificationPreference = {
enabled: Scalars['Boolean']['output'];
event: NotificationEvent;
type: NotificationType;
};
/** Union type representing either account-level or app-level notification preferences */
export type NotificationPreferenceItem = AccountNotificationPreference | AppNotificationPreference;
export type NotificationPreferenceMutation = {
__typename?: 'NotificationPreferenceMutation';
setAccountNotificationPreference: NotificationPreferenceItem;
setAppNotificationPreference: NotificationPreferenceItem;
};
export type NotificationPreferenceMutationSetAccountNotificationPreferenceArgs = {
input: AccountNotificationPreferenceInput;
};
export type NotificationPreferenceMutationSetAppNotificationPreferenceArgs = {
input: AppNotificationPreferenceInput;
};
export type NotificationSubscription = {
__typename?: 'NotificationSubscription';
account?: Maybe<Account>;
@@ -6232,7 +6377,12 @@ export type RootMutation = {
logRocketProject: LogRocketProjectMutation;
/** Mutations that modify the currently authenticated User */
me: MeMutation;
/** Mutations that modify a NotificationSubscription */
/** Notification preference management */
notificationPreference: NotificationPreferenceMutation;
/**
* Mutations that modify a NotificationSubscription
* @deprecated Use notificationPreference mutation instead
*/
notificationSubscription: NotificationSubscriptionMutation;
/** Mutations that create, update, and delete Robots */
robot: RobotMutation;
@@ -6253,6 +6403,12 @@ export type RootMutation = {
userDashboardViewPins: UserDashboardViewPinMutation;
/** Mutations that create, delete, and accept UserInvitations */
userInvitation: UserInvitationMutation;
/** Mutation interface for user preferences */
userPreference: UserPreferenceMutation;
/** Mutations for Vexo account connections */
vexoAccountConnection: VexoAccountConnectionMutation;
/** Mutations for Vexo apps */
vexoApp: VexoAppMutation;
/** Mutations that create, delete, update Webhooks */
webhook: WebhookMutation;
/** Mutations that modify a websiteNotification */
@@ -6385,6 +6541,10 @@ export type RootQuery = {
userByUsername?: Maybe<User>;
/** Top-level query object for querying UserInvitationPublicData publicly. */
userInvitationPublicData: UserInvitationPublicDataQuery;
/** Query interface for user preferences */
userPreference: UserPreferenceQuery;
/** Top-level query object for querying Vexo Integration information. */
vexoIntegration: VexoIntegrationQuery;
/**
* If authenticated as a typical end user, this is the appropriate top-level
* query object
@@ -6573,6 +6733,7 @@ export type SsoUser = Actor & UserActor & {
lastName?: Maybe<Scalars['String']['output']>;
/** @deprecated No longer supported */
location?: Maybe<Scalars['String']['output']>;
/** @deprecated Use Account/App.viewerNotificationPreferences instead */
notificationSubscriptions: Array<NotificationSubscription>;
pinnedApps: Array<App>;
pinnedDashboardViews: Array<PinnedDashboardView>;
@@ -6928,6 +7089,7 @@ export type Submission = ActivityTimelineProjectActivity & {
id: Scalars['ID']['output'];
initiatingActor?: Maybe<Actor>;
iosConfig?: Maybe<IosSubmissionConfig>;
jobRun?: Maybe<JobRun>;
logFiles: Array<Scalars['String']['output']>;
/** @deprecated Use logFiles instead */
logsUrl?: Maybe<Scalars['String']['output']>;
@@ -7167,6 +7329,7 @@ export type UpdateBranch = {
name: Scalars['String']['output'];
runtimes: RuntimesConnection;
updateGroups: Array<Array<Update>>;
updateGroupsPaginated: UpdateGroupsConnection;
updatedAt: Scalars['DateTime']['output'];
updates: Array<Update>;
};
@@ -7188,6 +7351,15 @@ export type UpdateBranchUpdateGroupsArgs = {
};
export type UpdateBranchUpdateGroupsPaginatedArgs = {
after?: InputMaybe<Scalars['String']['input']>;
before?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<UpdatesFilterV2>;
first?: InputMaybe<Scalars['Int']['input']>;
last?: InputMaybe<Scalars['Int']['input']>;
};
export type UpdateBranchUpdatesArgs = {
filter?: InputMaybe<UpdatesFilter>;
limit: Scalars['Int']['input'];
@@ -7198,7 +7370,10 @@ export type UpdateBranchMutation = {
__typename?: 'UpdateBranchMutation';
/** Create an EAS branch for an app */
createUpdateBranchForApp: UpdateBranch;
/** Delete an EAS branch and all of its updates as long as the branch is not being used by any channels */
/**
* Delete an EAS branch and all of its updates as long as the branch is not being used by any channels
* @deprecated Use scheduleUpdateBranchDeletion instead
*/
deleteUpdateBranch: DeleteUpdateBranchResult;
/**
* Edit an EAS branch. The branch can be specified either by its ID or
@@ -7207,6 +7382,8 @@ export type UpdateBranchMutation = {
editUpdateBranch: UpdateBranch;
/** Publish an update group to a branch */
publishUpdateGroups: Array<Update>;
/** Delete an EAS branch and all of its updates in the background */
scheduleUpdateBranchDeletion: BackgroundJobReceipt;
};
@@ -7230,6 +7407,11 @@ export type UpdateBranchMutationPublishUpdateGroupsArgs = {
publishUpdateGroupsInput: Array<PublishUpdateGroupInput>;
};
export type UpdateBranchMutationScheduleUpdateBranchDeletionArgs = {
branchId: Scalars['ID']['input'];
};
export type UpdateChannel = {
__typename?: 'UpdateChannel';
app: App;
@@ -7395,6 +7577,18 @@ export type UpdateGitHubRepositorySettingsInput = {
baseDirectory: Scalars['String']['input'];
};
export type UpdateGroupEdge = {
__typename?: 'UpdateGroupEdge';
cursor: Scalars['String']['output'];
node: Array<Update>;
};
export type UpdateGroupsConnection = {
__typename?: 'UpdateGroupsConnection';
edges: Array<UpdateGroupEdge>;
pageInfo: PageInfo;
};
export type UpdateInfoGroup = {
android?: InputMaybe<PartialManifest>;
ios?: InputMaybe<PartialManifest>;
@@ -7485,10 +7679,22 @@ export type UpdateRolloutInfoGroup = {
web?: InputMaybe<UpdateRolloutInfo>;
};
export type UpdateVexoAppInput = {
domain?: InputMaybe<Scalars['String']['input']>;
iconUrl?: InputMaybe<Scalars['String']['input']>;
name?: InputMaybe<Scalars['String']['input']>;
owner?: InputMaybe<Scalars['String']['input']>;
slug?: InputMaybe<Scalars['String']['input']>;
};
export type UpdatesFilter = {
platform?: InputMaybe<AppPlatform>;
runtimeVersions?: InputMaybe<Array<Scalars['String']['input']>>;
sdkVersions?: InputMaybe<Array<Scalars['String']['input']>>;
};
export type UpdatesFilterV2 = {
platform?: InputMaybe<AppPlatform>;
runtimeVersions?: InputMaybe<Array<Scalars['String']['input']>>;
};
export type UpdatesMetricsData = {
@@ -7619,6 +7825,7 @@ export type User = Actor & UserActor & {
lastName?: Maybe<Scalars['String']['output']>;
/** @deprecated No longer supported */
location?: Maybe<Scalars['String']['output']>;
/** @deprecated Use Account/App.viewerNotificationPreferences instead */
notificationSubscriptions: Array<NotificationSubscription>;
/** Pending UserInvitations for this user. Only resolves for the viewer. */
pendingUserInvitations: Array<UserInvitation>;
@@ -7731,6 +7938,7 @@ export type UserActor = {
lastName?: Maybe<Scalars['String']['output']>;
/** @deprecated No longer supported */
location?: Maybe<Scalars['String']['output']>;
/** @deprecated Use Account/App.viewerNotificationPreferences instead */
notificationSubscriptions: Array<NotificationSubscription>;
pinnedApps: Array<App>;
preferences: UserPreferences;
@@ -8022,6 +8230,8 @@ export type UserInvitation = {
* @deprecated Use accountProfileImageUrl
*/
accountProfilePhoto?: Maybe<Scalars['String']['output']>;
/** Whether the account requires two-factor authentication */
accountRequiresTwoFactor: Scalars['Boolean']['output'];
created: Scalars['DateTime']['output'];
/** Email to which this invitation was sent */
email: Scalars['String']['output'];
@@ -8107,6 +8317,7 @@ export type UserInvitationPublicData = {
accountName: Scalars['String']['output'];
accountProfileImageUrl: Scalars['String']['output'];
accountProfilePhoto?: Maybe<Scalars['String']['output']>;
accountRequiresTwoFactor: Scalars['Boolean']['output'];
created: Scalars['DateTime']['output'];
email: Scalars['String']['output'];
expires: Scalars['DateTime']['output'];
@@ -8142,6 +8353,51 @@ export type UserPermission = {
userActor?: Maybe<UserActor>;
};
/** A single user preference key-value pair */
export type UserPreference = {
__typename?: 'UserPreference';
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
key: Scalars['String']['output'];
updatedAt: Scalars['DateTime']['output'];
value: Scalars['JSON']['output'];
};
/** Mutation interface for user preferences */
export type UserPreferenceMutation = {
__typename?: 'UserPreferenceMutation';
/** Delete a user preference by key */
delete?: Maybe<UserPreference>;
/** Set a user preference value by key */
set: UserPreference;
};
/** Mutation interface for user preferences */
export type UserPreferenceMutationDeleteArgs = {
key: Scalars['String']['input'];
};
/** Mutation interface for user preferences */
export type UserPreferenceMutationSetArgs = {
key: Scalars['String']['input'];
value: Scalars['JSON']['input'];
};
/** Query interface for user preferences */
export type UserPreferenceQuery = {
__typename?: 'UserPreferenceQuery';
/** Get a user preference by key */
get?: Maybe<UserPreference>;
};
/** Query interface for user preferences */
export type UserPreferenceQueryGetArgs = {
key: Scalars['String']['input'];
};
export type UserPreferences = {
__typename?: 'UserPreferences';
onboarding?: Maybe<UserPreferencesOnboarding>;
@@ -8222,6 +8478,86 @@ export type UserSecondFactorDevice = {
user: User;
};
export enum UserSpecifiedAccountUsage {
Company = 'COMPANY',
Personal = 'PERSONAL'
}
export type VexoAccountAccessToken = {
__typename?: 'VexoAccountAccessToken';
access_token: Scalars['String']['output'];
expires_at?: Maybe<Scalars['String']['output']>;
token_type: Scalars['String']['output'];
};
export type VexoAccountConnection = {
__typename?: 'VexoAccountConnection';
accessToken: VexoAccountAccessToken;
account: Account;
id: Scalars['ID']['output'];
};
export type VexoAccountConnectionMutation = {
__typename?: 'VexoAccountConnectionMutation';
/** Create a Vexo account connection for an Account */
createVexoAccountConnection: VexoAccountConnection;
/** Delete a Vexo account connection by ID and revoke the access token */
revokeAndDeleteVexoAccountConnection: VexoAccountConnection;
};
export type VexoAccountConnectionMutationCreateVexoAccountConnectionArgs = {
vexoAccountConnectionData: CreateVexoAccountConnectionInput;
};
export type VexoAccountConnectionMutationRevokeAndDeleteVexoAccountConnectionArgs = {
vexoAccountConnectionId: Scalars['ID']['input'];
};
export type VexoApp = {
__typename?: 'VexoApp';
app: App;
domain?: Maybe<Scalars['String']['output']>;
iconUrl?: Maybe<Scalars['String']['output']>;
id: Scalars['ID']['output'];
name: Scalars['String']['output'];
owner: Scalars['String']['output'];
slug: Scalars['String']['output'];
vexoIdentifier: Scalars['String']['output'];
};
export type VexoAppMutation = {
__typename?: 'VexoAppMutation';
/** Create a Vexo app for an App */
createVexoApp: VexoApp;
/** Delete a Vexo app by ID */
deleteVexoApp: VexoApp;
/** Update a Vexo app by ID */
updateVexoApp: VexoApp;
};
export type VexoAppMutationCreateVexoAppArgs = {
vexoAppData: CreateVexoAppInput;
};
export type VexoAppMutationDeleteVexoAppArgs = {
vexoAppId: Scalars['ID']['input'];
};
export type VexoAppMutationUpdateVexoAppArgs = {
vexoAppData: UpdateVexoAppInput;
vexoAppId: Scalars['ID']['input'];
};
export type VexoIntegrationQuery = {
__typename?: 'VexoIntegrationQuery';
clientIdentifier: Scalars['String']['output'];
};
export type WebNotificationUpdateReadStateInput = {
id: Scalars['ID']['input'];
isRead: Scalars['Boolean']['input'];
@@ -8761,6 +9097,7 @@ export type Workflow = {
createdAt: Scalars['DateTime']['output'];
fileName: Scalars['String']['output'];
id: Scalars['ID']['output'];
insights: WorkflowsInsightsWorkflowMetrics;
name?: Maybe<Scalars['String']['output']>;
revisionsPaginated: WorkflowRevisionsConnection;
runsPaginated: WorkflowRunsConnection;
@@ -8768,6 +9105,12 @@ export type Workflow = {
};
export type WorkflowInsightsArgs = {
filters?: InputMaybe<WorkflowsInsightsFiltersInput>;
timespan: WorkflowsInsightsTimespanInput;
};
export type WorkflowRevisionsPaginatedArgs = {
after?: InputMaybe<Scalars['String']['input']>;
before?: InputMaybe<Scalars['String']['input']>;
@@ -8808,6 +9151,7 @@ export type WorkflowJob = {
approvals: Array<WorkflowJobApproval>;
createdAt: Scalars['DateTime']['output'];
credentialsAppleDeviceRegistrationRequest?: Maybe<AppleDeviceRegistrationRequest>;
environment?: Maybe<Scalars['String']['output']>;
errors: Array<WorkflowJobError>;
id: Scalars['ID']['output'];
key: Scalars['String']['output'];
@@ -8950,6 +9294,7 @@ export type WorkflowRevisionInput = {
export type WorkflowRevisionMutation = {
__typename?: 'WorkflowRevisionMutation';
getOrCreateWorkflowRevisionFromGitRef: WorkflowRevision;
getWorkflowRevisionsFromGitRef: Array<WorkflowRevision>;
validateWorkflowYamlConfig: Scalars['Boolean']['output'];
};
@@ -8961,6 +9306,12 @@ export type WorkflowRevisionMutationGetOrCreateWorkflowRevisionFromGitRefArgs =
};
export type WorkflowRevisionMutationGetWorkflowRevisionsFromGitRefArgs = {
appId: Scalars['ID']['input'];
gitRef: Scalars['String']['input'];
};
export type WorkflowRevisionMutationValidateWorkflowYamlConfigArgs = {
appId: Scalars['ID']['input'];
yamlConfig: Scalars['String']['input'];
@@ -8989,6 +9340,7 @@ export type WorkflowRun = ActivityTimelineProjectActivity & {
createdAt: Scalars['DateTime']['output'];
durationSeconds?: Maybe<Scalars['Int']['output']>;
errors: Array<WorkflowRunError>;
finalizedAt?: Maybe<Scalars['DateTime']['output']>;
gitCommitHash?: Maybe<Scalars['String']['output']>;
gitCommitMessage?: Maybe<Scalars['String']['output']>;
githubRepository?: Maybe<GitHubRepository>;
@@ -9024,6 +9376,7 @@ export type WorkflowRunError = {
export type WorkflowRunFilterInput = {
requestedGitRef?: InputMaybe<Scalars['String']['input']>;
status?: InputMaybe<WorkflowRunStatus>;
timeRange?: InputMaybe<WorkflowRunTimeRangeInput>;
};
export type WorkflowRunGitBranchFilterInput = {
@@ -9087,6 +9440,11 @@ export enum WorkflowRunStatus {
Success = 'SUCCESS'
}
export type WorkflowRunTimeRangeInput = {
earliest?: InputMaybe<Scalars['DateTime']['input']>;
latest?: InputMaybe<Scalars['DateTime']['input']>;
};
export enum WorkflowRunTriggerEventType {
GithubPullRequestLabeled = 'GITHUB_PULL_REQUEST_LABELED',
GithubPullRequestOpened = 'GITHUB_PULL_REQUEST_OPENED',
@@ -9103,6 +9461,73 @@ export type WorkflowRunsConnection = {
pageInfo: PageInfo;
};
export type WorkflowsInsightsFiltersInput = {
gitRefRequested?: InputMaybe<Array<Scalars['String']['input']>>;
statuses?: InputMaybe<Array<WorkflowRunStatus>>;
triggerEventTypes?: InputMaybe<Array<WorkflowRunTriggerEventType>>;
workflowIds?: InputMaybe<Array<Scalars['ID']['input']>>;
};
export type WorkflowsInsightsMetric = {
__typename?: 'WorkflowsInsightsMetric';
currentValue: Scalars['Float']['output'];
previousValue: Scalars['Float']['output'];
};
export type WorkflowsInsightsOverviewMetrics = {
__typename?: 'WorkflowsInsightsOverviewMetrics';
activeWorkflows: WorkflowsInsightsMetric;
failedRuns: WorkflowsInsightsMetric;
successfulRuns: WorkflowsInsightsMetric;
totalRuns: WorkflowsInsightsMetric;
};
export type WorkflowsInsightsRunsOverTimeData = {
__typename?: 'WorkflowsInsightsRunsOverTimeData';
lineChart: LineChartData;
};
export enum WorkflowsInsightsRunsOverTimeGranularity {
Day = 'DAY',
Hour = 'HOUR',
Minute = 'MINUTE'
}
export type WorkflowsInsightsTimespanInput = {
end: Scalars['DateTime']['input'];
start: Scalars['DateTime']['input'];
};
export type WorkflowsInsightsWorkflowConnection = {
__typename?: 'WorkflowsInsightsWorkflowConnection';
edges: Array<WorkflowsInsightsWorkflowEdge>;
pageInfo: PageInfo;
};
export type WorkflowsInsightsWorkflowEdge = {
__typename?: 'WorkflowsInsightsWorkflowEdge';
cursor: Scalars['String']['output'];
node: WorkflowsInsightsWorkflowNode;
};
export type WorkflowsInsightsWorkflowMetrics = {
__typename?: 'WorkflowsInsightsWorkflowMetrics';
failedRuns: WorkflowsInsightsMetric;
lastRunAt?: Maybe<Scalars['DateTime']['output']>;
successfulRuns: WorkflowsInsightsMetric;
totalRuns: WorkflowsInsightsMetric;
};
export type WorkflowsInsightsWorkflowNode = {
__typename?: 'WorkflowsInsightsWorkflowNode';
failedRuns: Scalars['Int']['output'];
lastRunAt: Scalars['DateTime']['output'];
name: Scalars['String']['output'];
successfulRuns: Scalars['Int']['output'];
totalRuns: Scalars['Int']['output'];
workflowId: Scalars['ID']['output'];
};
export type DeleteAndroidAppBuildCredentialsResult = {
__typename?: 'deleteAndroidAppBuildCredentialsResult';
id: Scalars['ID']['output'];
@@ -9123,6 +9548,13 @@ export type DeleteApplePushKeyResult = {
id: Scalars['ID']['output'];
};
export type ScheduleBranchDeletionMutationVariables = Exact<{
branchId: Scalars['ID']['input'];
}>;
export type ScheduleBranchDeletionMutation = { __typename?: 'RootMutation', updateBranch: { __typename?: 'UpdateBranchMutation', scheduleUpdateBranchDeletion: { __typename?: 'BackgroundJobReceipt', id: string, state: BackgroundJobState, tries: number, willRetry: boolean, resultId?: string | null, resultType: BackgroundJobResultType, resultData?: any | null, errorCode?: string | null, errorMessage?: string | null, createdAt: any, updatedAt: any } } };
export type CreateUpdateBranchForAppMutationVariables = Exact<{
appId: Scalars['ID']['input'];
name: Scalars['String']['input'];
@@ -9148,13 +9580,6 @@ export type GetBranchInfoQueryVariables = Exact<{
export type GetBranchInfoQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranchByName?: { __typename?: 'UpdateBranch', id: string, name: string } | null } } };
export type DeleteUpdateBranchMutationVariables = Exact<{
branchId: Scalars['ID']['input'];
}>;
export type DeleteUpdateBranchMutation = { __typename?: 'RootMutation', updateBranch: { __typename?: 'UpdateBranchMutation', deleteUpdateBranch: { __typename?: 'DeleteUpdateBranchResult', id: string } } };
export type EditUpdateBranchMutationVariables = Exact<{
input: EditUpdateBranchInput;
}>;
@@ -9653,7 +10078,7 @@ export type CreateAndroidBuildMutationVariables = Exact<{
}>;
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateIosBuildMutationVariables = Exact<{
appId: Scalars['ID']['input'];
@@ -9663,7 +10088,7 @@ export type CreateIosBuildMutationVariables = Exact<{
}>;
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type UpdateBuildMetadataMutationVariables = Exact<{
buildId: Scalars['ID']['input'];
@@ -9671,7 +10096,7 @@ export type UpdateBuildMetadataMutationVariables = Exact<{
}>;
export type UpdateBuildMetadataMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', updateBuildMetadata: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type UpdateBuildMetadataMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', updateBuildMetadata: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type RetryIosBuildMutationVariables = Exact<{
buildId: Scalars['ID']['input'];
@@ -9679,7 +10104,7 @@ export type RetryIosBuildMutationVariables = Exact<{
}>;
export type RetryIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', retryIosBuild: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type RetryIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', retryIosBuild: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type CreateEnvironmentSecretForAccountMutationVariables = Exact<{
input: CreateEnvironmentSecretInput;
@@ -9763,7 +10188,7 @@ export type CreateLocalBuildMutationVariables = Exact<{
}>;
export type CreateLocalBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createLocalBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } } };
export type CreateLocalBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createLocalBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } } };
export type GetSignedUploadMutationVariables = Exact<{
contentTypes: Array<Scalars['String']['input']> | Scalars['String']['input'];
@@ -9861,6 +10286,14 @@ export type DeleteWebhookMutationVariables = Exact<{
export type DeleteWebhookMutation = { __typename?: 'RootMutation', webhook: { __typename?: 'WebhookMutation', deleteWebhook: { __typename?: 'DeleteWebhookResult', id: string } } };
export type GetWorkflowRevisionsFromGitRefMutationVariables = Exact<{
appId: Scalars['ID']['input'];
gitRef: Scalars['String']['input'];
}>;
export type GetWorkflowRevisionsFromGitRefMutation = { __typename?: 'RootMutation', workflowRevision: { __typename?: 'WorkflowRevisionMutation', getWorkflowRevisionsFromGitRef: Array<{ __typename?: 'WorkflowRevision', id: string, yamlConfig: string, blobSha: string, commitSha?: string | null, createdAt: any, workflow: { __typename?: 'Workflow', id: string } }> } };
export type GetOrCreateWorkflowRevisionFromGitRefMutationVariables = Exact<{
appId: Scalars['ID']['input'];
fileName: Scalars['String']['input'];
@@ -9903,6 +10336,14 @@ export type CancelWorkflowRunMutationVariables = Exact<{
export type CancelWorkflowRunMutation = { __typename?: 'RootMutation', workflowRun: { __typename?: 'WorkflowRunMutation', cancelWorkflowRun: { __typename?: 'WorkflowRun', id: string } } };
export type AccountUsageForOverageWarningQueryVariables = Exact<{
accountId: Scalars['String']['input'];
currentDate: Scalars['DateTime']['input'];
}>;
export type AccountUsageForOverageWarningQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byId: { __typename?: 'Account', id: string, name: string, subscription?: { __typename?: 'SubscriptionDetails', id: string, name?: string | null } | null, usageMetrics: { __typename?: 'AccountUsageMetrics', EAS_BUILD: { __typename?: 'UsageMetricTotal', id: string, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, serviceMetric: EasServiceMetric, value: number, limit: number }> } } } } };
export type AppByIdQueryVariables = Exact<{
appId: Scalars['String']['input'];
}>;
@@ -9949,6 +10390,14 @@ export type LatestAppVersionQueryVariables = Exact<{
export type LatestAppVersionQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, latestAppVersionByPlatformAndApplicationIdentifier?: { __typename?: 'AppVersion', id: string, storeVersion: string, buildVersion: string } | null } } };
export type GetAssetSignedUrlsQueryVariables = Exact<{
updateId: Scalars['ID']['input'];
storageKeys: Array<Scalars['String']['input']> | Scalars['String']['input'];
}>;
export type GetAssetSignedUrlsQuery = { __typename?: 'RootQuery', asset: { __typename?: 'AssetQuery', signedUrls: Array<{ __typename?: 'AssetSignedUrlResult', storageKey: string, url: string, headers?: any | null }> } };
export type BackgroundJobReceiptByIdQueryVariables = Exact<{
id: Scalars['ID']['input'];
}>;
@@ -9969,6 +10418,8 @@ export type ViewLatestUpdateOnBranchQueryVariables = Exact<{
branchName: Scalars['String']['input'];
platform: AppPlatform;
runtimeVersion: Scalars['String']['input'];
offset: Scalars['Int']['input'];
limit: Scalars['Int']['input'];
}>;
@@ -10009,21 +10460,21 @@ export type BuildsByIdQueryVariables = Exact<{
}>;
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type BuildsWithSubmissionsByIdQueryVariables = Exact<{
buildId: Scalars['ID']['input'];
}>;
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type BuildsWithFingerprintByIdQueryVariables = Exact<{
buildId: Scalars['ID']['input'];
}>;
export type BuildsWithFingerprintByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type BuildsWithFingerprintByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type ViewBuildsOnAppQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -10033,7 +10484,7 @@ export type ViewBuildsOnAppQueryVariables = Exact<{
}>;
export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }> } } };
export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }> } } };
export type ViewUpdateChannelBasicInfoOnAppQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -10257,7 +10708,7 @@ export type WorkflowJobByIdQueryVariables = Exact<{
}>;
export type WorkflowJobByIdQuery = { __typename?: 'RootQuery', workflowJobs: { __typename?: 'WorkflowJobQuery', byId: { __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> } } };
export type WorkflowJobByIdQuery = { __typename?: 'RootQuery', workflowJobs: { __typename?: 'WorkflowJobQuery', byId: { __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> } } };
export type WorkflowRunByIdQueryVariables = Exact<{
workflowRunId: Scalars['ID']['input'];
@@ -10271,7 +10722,7 @@ export type WorkflowRunByIdWithJobsQueryVariables = Exact<{
}>;
export type WorkflowRunByIdWithJobsQuery = { __typename?: 'RootQuery', workflowRuns: { __typename?: 'WorkflowRunQuery', byId: { __typename?: 'WorkflowRun', id: string, status: WorkflowRunStatus, gitCommitMessage?: string | null, gitCommitHash?: string | null, requestedGitRef?: string | null, triggeringLabelName?: string | null, triggerEventType: WorkflowRunTriggerEventType, triggeringSchedule?: string | null, createdAt: any, updatedAt: any, workflow: { __typename?: 'Workflow', id: string, name?: string | null, fileName: string, app: { __typename?: 'App', id: string, name: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } }, jobs: Array<{ __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> }>, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, errors: Array<{ __typename?: 'WorkflowRunError', title?: string | null, message: string }> } } };
export type WorkflowRunByIdWithJobsQuery = { __typename?: 'RootQuery', workflowRuns: { __typename?: 'WorkflowRunQuery', byId: { __typename?: 'WorkflowRun', id: string, status: WorkflowRunStatus, gitCommitMessage?: string | null, gitCommitHash?: string | null, requestedGitRef?: string | null, triggeringLabelName?: string | null, triggerEventType: WorkflowRunTriggerEventType, triggeringSchedule?: string | null, createdAt: any, updatedAt: any, workflow: { __typename?: 'Workflow', id: string, name?: string | null, fileName: string, app: { __typename?: 'App', id: string, name: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } }, jobs: Array<{ __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> }>, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, errors: Array<{ __typename?: 'WorkflowRunError', title?: string | null, message: string }> } } };
export type WorkflowRunsForAppIdFileNameAndStatusQueryVariables = Exact<{
appId: Scalars['ID']['input'];
@@ -10289,11 +10740,11 @@ export type AppFragment = { __typename?: 'App', id: string, name: string, fullNa
export type BackgroundJobReceiptDataFragment = { __typename?: 'BackgroundJobReceipt', id: string, state: BackgroundJobState, tries: number, willRetry: boolean, resultId?: string | null, resultType: BackgroundJobResultType, resultData?: any | null, errorCode?: string | null, errorMessage?: string | null, createdAt: any, updatedAt: any };
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
export type BuildWithFingerprintFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
export type BuildWithFingerprintFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
export type EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any };
@@ -10321,7 +10772,7 @@ export type WebhookFragment = { __typename?: 'Webhook', id: string, event: Webho
export type WorkflowFragment = { __typename?: 'Workflow', id: string, name?: string | null, fileName: string, createdAt: any, updatedAt: any, revisionsPaginated: { __typename?: 'WorkflowRevisionsConnection', edges: Array<{ __typename?: 'WorkflowRevisionEdge', node: { __typename?: 'WorkflowRevision', id: string, blobSha: string, commitSha?: string | null, createdAt: any, yamlConfig: string } }> } };
export type WorkflowJobFragment = { __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> };
export type WorkflowJobFragment = { __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> };
export type WorkflowRunFragment = { __typename?: 'WorkflowRun', id: string, status: WorkflowRunStatus, gitCommitMessage?: string | null, gitCommitHash?: string | null, requestedGitRef?: string | null, triggeringLabelName?: string | null, triggerEventType: WorkflowRunTriggerEventType, triggeringSchedule?: string | null, createdAt: any, updatedAt: any, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, errors: Array<{ __typename?: 'WorkflowRunError', title?: string | null, message: string }>, workflow: { __typename?: 'Workflow', id: string, name?: string | null, fileName: string } };
@@ -6,6 +6,8 @@ import { withErrorHandlingAsync } from '../client';
import {
GetOrCreateWorkflowRevisionFromGitRefMutation,
GetOrCreateWorkflowRevisionFromGitRefMutationVariables,
GetWorkflowRevisionsFromGitRefMutation,
GetWorkflowRevisionsFromGitRefMutationVariables,
ValidateWorkflowYamlConfigMutation,
ValidateWorkflowYamlConfigMutationVariables,
WorkflowRevision,
@@ -19,7 +21,53 @@ export namespace WorkflowRevisionMutation {
fieldErrors: z.record(z.string(), z.array(z.string())),
}),
});
export async function getWorkflowRevisionsFromGitRefAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
gitRef,
}: {
appId: string;
gitRef: string;
}
): Promise<WorkflowRevision[] | undefined> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<
GetWorkflowRevisionsFromGitRefMutation,
GetWorkflowRevisionsFromGitRefMutationVariables
>(
gql`
mutation GetWorkflowRevisionsFromGitRef($appId: ID!, $gitRef: String!) {
workflowRevision {
getWorkflowRevisionsFromGitRef(appId: $appId, gitRef: $gitRef) {
id
yamlConfig
blobSha
commitSha
createdAt
workflow {
id
app {
id
name
slug
}
fileName
}
}
}
}
`,
{
appId,
gitRef,
}
)
.toPromise()
);
return (data.workflowRevision?.getWorkflowRevisionsFromGitRef as WorkflowRevision[]) ?? [];
}
export async function getOrCreateWorkflowRevisionFromGitRefAsync(
graphqlClient: ExpoGraphqlClient,
{
@@ -0,0 +1,54 @@
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
AccountUsageForOverageWarningQuery,
AccountUsageForOverageWarningQueryVariables,
} from '../generated';
export const AccountUsageQuery = {
async getUsageForOverageWarningAsync(
graphqlClient: ExpoGraphqlClient,
accountId: string,
currentDate: Date
): Promise<AccountUsageForOverageWarningQuery['account']['byId']> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AccountUsageForOverageWarningQuery, AccountUsageForOverageWarningQueryVariables>(
gql`
query AccountUsageForOverageWarning($accountId: String!, $currentDate: DateTime!) {
account {
byId(accountId: $accountId) {
id
name
subscription {
id
name
}
usageMetrics {
EAS_BUILD: byBillingPeriod(date: $currentDate, service: BUILDS) {
id
planMetrics {
id
serviceMetric
value
limit
}
}
}
}
}
}
`,
{ accountId, currentDate: currentDate.toISOString() },
{
additionalTypenames: ['Account', 'AccountUsageMetrics', 'UsageMetricTotal'],
}
)
.toPromise()
);
return data.account.byId;
},
};
@@ -0,0 +1,41 @@
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
AssetSignedUrlResult,
GetAssetSignedUrlsQuery,
GetAssetSignedUrlsQueryVariables,
} from '../generated';
export const AssetQuery = {
async getSignedUrlsAsync(
graphqlClient: ExpoGraphqlClient,
updateId: string,
storageKeys: string[]
): Promise<AssetSignedUrlResult[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<GetAssetSignedUrlsQuery, GetAssetSignedUrlsQueryVariables>(
gql`
query GetAssetSignedUrls($updateId: ID!, $storageKeys: [String!]!) {
asset {
signedUrls(updateId: $updateId, storageKeys: $storageKeys) {
storageKey
url
headers
}
}
}
`,
{
updateId,
storageKeys,
},
{ additionalTypenames: [] }
)
.toPromise()
);
return data.asset.signedUrls;
},
};
@@ -63,15 +63,24 @@ export const BranchQuery = {
}
return updateBranchByName;
},
async getLatestUpdateIdOnBranchAsync(
async getUpdateIdsOnBranchAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
branchName,
platform,
runtimeVersion,
}: { appId: string; branchName: string; platform: AppPlatform; runtimeVersion: string }
): Promise<string | null> {
offset = 0,
limit = 1,
}: {
appId: string;
branchName: string;
platform: AppPlatform;
runtimeVersion: string;
offset?: number;
limit?: number;
}
): Promise<string[]> {
const response = await withErrorHandlingAsync(
graphqlClient
.query<ViewLatestUpdateOnBranchQuery, ViewLatestUpdateOnBranchQueryVariables>(
@@ -81,6 +90,8 @@ export const BranchQuery = {
$branchName: String!
$platform: AppPlatform!
$runtimeVersion: String!
$offset: Int!
$limit: Int!
) {
app {
byId(appId: $appId) {
@@ -88,8 +99,8 @@ export const BranchQuery = {
updateBranchByName(name: $branchName) {
id
updates(
offset: 0
limit: 1
offset: $offset
limit: $limit
filter: { platform: $platform, runtimeVersions: [$runtimeVersion] }
) {
id
@@ -104,6 +115,8 @@ export const BranchQuery = {
branchName,
platform,
runtimeVersion,
offset,
limit,
},
{ additionalTypenames: ['UpdateBranch'] }
)
@@ -113,11 +126,26 @@ export const BranchQuery = {
if (!updateBranchByName) {
throw new BranchNotFoundError(`Could not find a branch named "${branchName}".`);
}
const latestUpdate = updateBranchByName.updates[0];
if (!latestUpdate) {
return null;
}
return latestUpdate.id;
return updateBranchByName.updates.map(update => update.id);
},
async getLatestUpdateIdOnBranchAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
branchName,
platform,
runtimeVersion,
}: { appId: string; branchName: string; platform: AppPlatform; runtimeVersion: string }
): Promise<string | null> {
const updateIds = await this.getUpdateIdsOnBranchAsync(graphqlClient, {
appId,
branchName,
platform,
runtimeVersion,
offset: 0,
limit: 1,
});
return updateIds[0] ?? null;
},
async listBranchesOnAppAsync(
graphqlClient: ExpoGraphqlClient,
@@ -29,6 +29,7 @@ export const BuildFragmentNode = gql`
id
displayName
}
logFiles
project {
__typename
id
+3
View File
@@ -9,11 +9,13 @@ export async function runGitCloneAsync({
githubRepositoryName,
githubUsername,
cloneMethod,
showOutput = true,
}: {
githubUsername: string;
githubRepositoryName: string;
targetProjectDir: string;
cloneMethod: 'ssh' | 'https';
showOutput?: boolean;
}): Promise<{
targetProjectDir: string;
}> {
@@ -29,6 +31,7 @@ export async function runGitCloneAsync({
return line.includes('Cloning into');
},
showSpinner: false,
showOutput,
});
return { targetProjectDir };
} catch (error: any) {
@@ -35,5 +35,6 @@ export async function installDependenciesAsync({
);
},
showOutput: outputLevel !== 'none',
showSpinner: outputLevel !== 'none',
});
}
@@ -21,7 +21,9 @@ export async function runCommandAsync({
showSpinner?: boolean;
showOutput?: boolean;
}): Promise<void> {
Log.log(`🏗️ Running ${chalk.bold(`${command} ${args.join(' ')}`)}...`);
if (showOutput) {
Log.log(`🏗️ Running ${chalk.bold(`${command} ${args.join(' ')}`)}...`);
}
let spinner: Ora | undefined;
if (showSpinner) {
spinner = ora(`${chalk.bold(`${command} ${args.join(' ')}`)}`).start();
@@ -71,6 +73,10 @@ export async function runCommandAsync({
throw error;
}
if (!showOutput) {
return;
}
if (showSpinner) {
spinner?.succeed(`${chalk.bold(`${command} ${args.join(' ')}`)} succeeded`);
} else {
+2 -9
View File
@@ -22,7 +22,6 @@ import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGr
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { FingerprintOptions, createFingerprintsByKeyAsync } from '../fingerprint/cli';
import {
AppPlatform,
AssetMetadataStatus,
BuildFragment,
FingerprintSourceInput,
@@ -39,8 +38,10 @@ import { promptAsync } from '../prompts';
import { getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync } from '../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync';
import {
UpdateJsonInfo,
UpdatePublishPlatform,
formatUpdateMessage,
truncateString as truncateUpdateMessage,
updatePublishPlatformToAppPlatform,
} from '../update/utils';
import { PresignedPost, uploadWithPresignedPostWithRetryAsync } from '../uploads';
import {
@@ -56,9 +57,6 @@ import mapMapAsync from '../utils/expodash/mapMapAsync';
import uniqBy from '../utils/expodash/uniqBy';
import { Client } from '../vcs/vcs';
// update publish does not currently support web
export type UpdatePublishPlatform = 'ios' | 'android';
type Metadata = {
version: number;
bundler: 'metro';
@@ -1049,11 +1047,6 @@ export const platformDisplayNames: Record<UpdatePublishPlatform, string> = {
ios: 'iOS',
};
export const updatePublishPlatformToAppPlatform: Record<UpdatePublishPlatform, AppPlatform> = {
android: AppPlatform.Android,
ios: AppPlatform.Ios,
};
export async function getRuntimeToUpdateRolloutInfoGroupMappingAsync(
graphqlClient: ExpoGraphqlClient,
{
@@ -5,7 +5,6 @@ import {
App,
DeploymentResult,
Runtime,
UpdateBranch,
UpdateChannel,
UpdateChannelRuntimeInsights,
UpdateInsights,
@@ -126,7 +125,7 @@ function mockUpdateChannel({
};
}
function mockUpdateBranches(branchNames: string[]): UpdateBranch[] {
function mockUpdateBranches(branchNames: string[]): any[] {
return branchNames.map(branchName => ({
id: 'f9dc4dbb-663f-4a19-8cf2-a783b484d2db',
latestActivity: '2022-12-07T02:24:29.786Z',
+1 -1
View File
@@ -2,6 +2,7 @@ import assert from 'assert';
import chalk from 'chalk';
import {
UpdatePublishPlatform,
formatBranch,
formatUpdateGroup,
formatUpdateTitle,
@@ -19,7 +20,6 @@ import {
import { RuntimeQuery } from '../graphql/queries/RuntimeQuery';
import { UpdateQuery } from '../graphql/queries/UpdateQuery';
import Log from '../log';
import { UpdatePublishPlatform } from '../project/publish';
import formatFields from '../utils/formatFields';
import { printJsonOnlyOutput } from '../utils/json';
import {
+2 -2
View File
@@ -7,7 +7,7 @@ import {
selectRuntimeAndGetLatestUpdateGroupForEachPublishPlatformOnBranchAsync,
selectUpdateGroupOnBranchAsync,
} from './queries';
import { truncateString as truncateUpdateMessage } from './utils';
import { UpdatePublishPlatform, truncateString as truncateUpdateMessage } from './utils';
import { selectBranchOnAppAsync } from '../branch/queries';
import { getUpdateGroupUrl } from '../build/utils/url';
import { selectChannelOnAppAsync } from '../channel/queries';
@@ -20,7 +20,7 @@ import { UpdateQuery } from '../graphql/queries/UpdateQuery';
import Log, { link } from '../log';
import { ora } from '../ora';
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
import { UpdatePublishPlatform, getUpdateRolloutInfoGroupAsync } from '../project/publish';
import { getUpdateRolloutInfoGroupAsync } from '../project/publish';
import { promptAsync } from '../prompts';
import {
CodeSigningInfo,
@@ -1,7 +1,7 @@
import { ExpoConfig } from '@expo/config';
import nullthrows from 'nullthrows';
import { getUpdateJsonInfosForUpdates } from './utils';
import { UpdatePublishPlatform, getUpdateJsonInfosForUpdates } from './utils';
import { getUpdateGroupUrl } from '../build/utils/url';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import fetch from '../fetch';
@@ -12,7 +12,6 @@ import { ora } from '../ora';
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
import {
RuntimeVersionInfo,
UpdatePublishPlatform,
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects,
} from '../project/publish';
import {
+88
View File
@@ -3,14 +3,19 @@ import { format } from '@expo/timeago.js';
import chalk from 'chalk';
import dateFormat from 'dateformat';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import {
AppPlatform,
Robot,
SsoUser,
Update,
UpdateBranchFragment,
UpdateFragment,
UpdatePublishMutation,
User,
} from '../graphql/generated';
import { AssetQuery } from '../graphql/queries/AssetQuery';
import { BranchQuery } from '../graphql/queries/BranchQuery';
import { learnMore } from '../log';
import { RequestedPlatform } from '../platform';
import { getActorDisplayName } from '../user/User';
@@ -269,3 +274,86 @@ export function getBranchDescription(branch: UpdateBranchFragment): FormattedBra
},
};
}
export function isBundleDiffingEnabled(exp: ExpoConfig): boolean {
return (exp.updates as any)?.enableBsdiffPatchSupport === true;
}
// Make authenticated requests to the launch asset URL with diffing headers
export async function prewarmDiffingAsync(
graphqlClient: ExpoGraphqlClient,
appId: string,
newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups']
): Promise<void> {
const DUMMY_EMBEDDED_UPDATE_ID = '00000000-0000-0000-0000-000000000000';
const toPrewarm = [] as {
update: UpdatePublishMutation['updateBranch']['publishUpdateGroups'][0];
launchAssetKey: string;
}[];
for (const update of newUpdates) {
const manifest = JSON.parse(update.manifestFragment);
const launchAssetKey: string | undefined = manifest.launchAsset?.storageKey;
const requestedUpdateId: string = update.id;
if (!launchAssetKey || !requestedUpdateId) {
continue;
}
toPrewarm.push({
update,
launchAssetKey,
});
}
await Promise.allSettled(
toPrewarm.map(async ({ update, launchAssetKey }) => {
try {
// Check to see if there's a second most recent update so we can pre-emptively generate a patch for it
const updatePublishPlatform = update.platform as UpdatePublishPlatform;
const updateIds = await BranchQuery.getUpdateIdsOnBranchAsync(graphqlClient, {
appId,
branchName: update.branch.name,
platform: updatePublishPlatformToAppPlatform[updatePublishPlatform],
runtimeVersion: update.runtimeVersion,
limit: 2,
});
if (updateIds.length !== 2) {
return;
}
const nextMostRecentUpdateId = updateIds[1];
const signed = await AssetQuery.getSignedUrlsAsync(graphqlClient, update.id, [
launchAssetKey,
]);
const first = signed?.[0];
if (!first) {
return;
}
const headers: Record<string, string> = {
...(first.headers as Record<string, string> | undefined),
'expo-current-update-id': nextMostRecentUpdateId,
'expo-requested-update-id': update.id,
'expo-embedded-update-id': DUMMY_EMBEDDED_UPDATE_ID,
'a-im': 'bsdiff',
};
await fetch(first.url, {
method: 'HEAD',
headers,
signal: AbortSignal.timeout(2500),
});
} catch {
// ignore errors, best-effort optimization
}
})
);
}
// update publish does not currently support web
export type UpdatePublishPlatform = 'ios' | 'android';
export const updatePublishPlatformToAppPlatform: Record<UpdatePublishPlatform, AppPlatform> = {
android: AppPlatform.Android,
ios: AppPlatform.Ios,
};
@@ -0,0 +1,258 @@
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import {
AccountUsageForOverageWarningQuery,
EasService,
EasServiceMetric,
EstimatedUsage,
UsageMetricType,
} from '../../../graphql/generated';
import { AccountUsageQuery } from '../../../graphql/queries/AccountUsageQuery';
import Log, { link } from '../../../log';
import {
createProgressBar,
displayOverageWarning,
maybeWarnAboutUsageOveragesAsync,
} from '../checkForOverages';
jest.mock('../../../graphql/queries/AccountUsageQuery');
jest.mock('../../../log', () => ({
...jest.requireActual('../../../log'),
warn: jest.fn(),
newLine: jest.fn(),
debug: jest.fn(),
link: jest.fn((url: string, opts?: { text?: string }) => opts?.text ?? url),
}));
function createMockPlanMetric({
value = 50,
limit = 100,
}: Partial<Pick<EstimatedUsage, 'value' | 'limit'>> = {}): EstimatedUsage {
return {
__typename: 'EstimatedUsage',
id: 'plan-metric-id',
service: EasService.Builds,
serviceMetric: EasServiceMetric.Builds,
metricType: UsageMetricType.Build,
value,
limit,
platformBreakdown: null,
};
}
function createMockAccountUsage({
id = 'account-id',
name = 'test-account',
subscriptionName = 'Free',
buildPlanMetrics = [],
}: {
id?: string;
name?: string;
subscriptionName?: string;
buildPlanMetrics?: EstimatedUsage[];
} = {}): AccountUsageForOverageWarningQuery['account']['byId'] {
return {
__typename: 'Account',
id,
name,
subscription: {
__typename: 'SubscriptionDetails',
id: 'sub-id',
name: subscriptionName,
},
usageMetrics: {
EAS_BUILD: {
__typename: 'UsageMetricTotal',
id: 'metric-id',
planMetrics: buildPlanMetrics,
},
},
};
}
describe('maybeWarnAboutUsageOveragesAsync', () => {
const mockGraphqlClient = {} as ExpoGraphqlClient;
const mockGetUsageForOverageWarningAsync = jest.mocked(
AccountUsageQuery.getUsageForOverageWarningAsync
);
const mockWarn = jest.mocked(Log.warn);
const mockDebug = jest.mocked(Log.debug);
beforeEach(() => {
mockGetUsageForOverageWarningAsync.mockClear();
mockWarn.mockClear();
mockDebug.mockClear();
});
it('displays a warning for a Free plan with high build usage', async () => {
mockGetUsageForOverageWarningAsync.mockResolvedValue(
createMockAccountUsage({
buildPlanMetrics: [createMockPlanMetric({ value: 85 })],
})
);
await maybeWarnAboutUsageOveragesAsync({
graphqlClient: mockGraphqlClient,
accountId: 'account-id',
});
expect(mockGetUsageForOverageWarningAsync).toHaveBeenCalledWith(
mockGraphqlClient,
'account-id',
expect.any(Date)
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You've used 85% of your included build credits for this month.")
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You won't be able to start new builds once you reach the limit.")
);
});
it('displays a warning for a Starter plan with high build usage', async () => {
mockGetUsageForOverageWarningAsync.mockResolvedValue(
createMockAccountUsage({
subscriptionName: 'Starter',
buildPlanMetrics: [createMockPlanMetric({ value: 90 })],
})
);
await maybeWarnAboutUsageOveragesAsync({
graphqlClient: mockGraphqlClient,
accountId: 'account-id',
});
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You've used 90% of your included build credits for this month.")
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining(
'Additional usage beyond your limit will be charged at pay-as-you-go rates.'
)
);
});
it('does not display a warning when usage is below threshold', async () => {
mockGetUsageForOverageWarningAsync.mockResolvedValue(
createMockAccountUsage({
buildPlanMetrics: [createMockPlanMetric({ value: 50 })],
})
);
await maybeWarnAboutUsageOveragesAsync({
graphqlClient: mockGraphqlClient,
accountId: 'account-id',
});
expect(mockWarn).not.toHaveBeenCalled();
});
it('handles errors gracefully', async () => {
mockGetUsageForOverageWarningAsync.mockRejectedValue(new Error('Network error'));
await maybeWarnAboutUsageOveragesAsync({
graphqlClient: mockGraphqlClient,
accountId: 'account-id',
});
expect(mockWarn).not.toHaveBeenCalled();
expect(mockDebug).toHaveBeenCalledWith(expect.stringContaining('Failed to fetch usage data'));
});
it('does not display a warning when there are no plan metrics', async () => {
mockGetUsageForOverageWarningAsync.mockResolvedValue(createMockAccountUsage());
await maybeWarnAboutUsageOveragesAsync({
graphqlClient: mockGraphqlClient,
accountId: 'account-id',
});
expect(mockWarn).not.toHaveBeenCalled();
});
});
describe('createProgressBar', () => {
it('creates a progress bar with correct fill for a given percentage', () => {
const bar = createProgressBar(85, 20);
expect(bar).toBe('█████████████████░░░');
});
it('creates a progress bar with correct fill for 100%', () => {
const bar = createProgressBar(100, 20);
expect(bar).toBe('████████████████████');
});
it('creates a progress bar with correct fill for 0%', () => {
const bar = createProgressBar(0, 20);
expect(bar).toBe('░░░░░░░░░░░░░░░░░░░░');
});
it('handles custom width', () => {
const bar = createProgressBar(50, 10);
expect(bar).toBe('█████░░░░░');
});
});
describe('displayOverageWarning', () => {
const mockWarn = Log.warn as jest.MockedFunction<typeof Log.warn>;
const mockLink = link as jest.MockedFunction<typeof link>;
beforeEach(() => {
mockWarn.mockClear();
mockLink.mockClear();
});
it('displays a warning for a Free plan', () => {
displayOverageWarning({
percentUsed: 85,
hasFreePlan: true,
name: 'test-account',
});
expect(mockWarn).toHaveBeenCalledTimes(2);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You've used 85% of your included build credits for this month.")
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You won't be able to start new builds once you reach the limit.")
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining('Upgrade your plan to continue service.')
);
});
it('displays a warning for a paid plan', () => {
displayOverageWarning({
percentUsed: 85,
hasFreePlan: false,
name: 'test-account',
});
expect(mockWarn).toHaveBeenCalledTimes(2);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You've used 85% of your included build credits for this month.")
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining(
'Additional usage beyond your limit will be charged at pay-as-you-go rates.'
)
);
expect(mockWarn).toHaveBeenCalledWith(expect.stringContaining('See usage in billing.'));
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining('██████████████████████████░░░░')
);
});
it('includes correct account name in billing URL', () => {
displayOverageWarning({
percentUsed: 85,
hasFreePlan: true,
name: 'my-custom-account',
});
expect(mockLink).toHaveBeenCalledWith(
'https://expo.dev/accounts/my-custom-account/settings/billing',
expect.objectContaining({ text: 'Upgrade your plan to continue service.' })
);
});
});
@@ -0,0 +1,84 @@
import chalk from 'chalk';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { AccountUsageQuery } from '../../graphql/queries/AccountUsageQuery';
import Log, { link } from '../../log';
const THRESHOLD_PERCENT = 85;
export async function maybeWarnAboutUsageOveragesAsync({
graphqlClient,
accountId,
}: {
graphqlClient: ExpoGraphqlClient;
accountId: string;
}): Promise<void> {
try {
const currentDate = new Date();
const {
name,
subscription,
usageMetrics: { EAS_BUILD },
} = await AccountUsageQuery.getUsageForOverageWarningAsync(
graphqlClient,
accountId,
currentDate
);
const planMetric = EAS_BUILD?.planMetrics?.[0];
if (!planMetric || !subscription) {
return;
}
const percentUsed = calculatePercentUsed(planMetric.value, planMetric.limit);
if (percentUsed >= THRESHOLD_PERCENT) {
const hasFreePlan = subscription.name === 'Free';
displayOverageWarning({ percentUsed, hasFreePlan, name });
}
} catch (error) {
// Silently fail if we can't fetch usage data - we don't want to block the user's workflow
Log.debug(`Failed to fetch usage data: ${error}`);
}
}
export function calculatePercentUsed(value: number, limit: number): number {
if (limit === 0) {
return 0;
}
return Math.min(Math.floor((value / limit) * 100), 100);
}
export function createProgressBar(percentUsed: number, width: number = 30): string {
const filledWidth = Math.round((percentUsed / 100) * width);
const emptyWidth = width - filledWidth;
const filled = '█'.repeat(filledWidth);
const empty = '░'.repeat(emptyWidth);
return `${filled}${empty}`;
}
export function displayOverageWarning({
percentUsed,
hasFreePlan,
name,
}: {
percentUsed: number;
hasFreePlan: boolean;
name: string;
}): void {
Log.warn(
chalk.bold(`You've used ${percentUsed}% of your included build credits for this month. `) +
createProgressBar(percentUsed)
);
const billingUrl = `https://expo.dev/accounts/${name}/settings/billing`;
const warning = hasFreePlan
? "You won't be able to start new builds once you reach the limit. " +
link(billingUrl, { text: 'Upgrade your plan to continue service.', dim: false })
: 'Additional usage beyond your limit will be charged at pay-as-you-go rates. ' +
link(billingUrl, {
text: 'See usage in billing.',
dim: false,
});
Log.warn(warning);
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with eas.json",
"version": "16.23.0",
"version": "16.25.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
+4 -2
View File
@@ -522,7 +522,8 @@
"**Closed testing** releases are available to a limited number of testers that you choose, who can test a pre-release version of your app and submit feedback.",
"**Internal testing** releases are available to up to 100 testers that you choose.",
"**Production releases** are available to all Google Play users in your chosen countries."
]
],
"default": "internal"
},
"releaseStatus": {
"enum": ["draft", "inProgress", "halted", "completed"],
@@ -533,7 +534,8 @@
"The release's APKs are being served to a fraction of users, if set.",
"The release's APKs will no longer be served to users. Users who already have these APKs are unaffected.",
"The release will have no further changes. Its APKs are being served to all users, unless they are eligible to APKs of a more recent release."
]
],
"default": "completed"
},
"rollout": {
"type": "number",
+3 -1
View File
@@ -8,7 +8,9 @@ GITHUB_EMAIL=`git config get --global user.email`
SCRIPTS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
ROOT_DIR="$( cd "$SCRIPTS_DIR"/.. && pwd )"
$SCRIPTS_DIR/bin/run update-local-plugin
# Since https://github.com/expo/eas-cli/pull/3237 , the local plugin version is identical
# to @expo/eas-build-job dependency, so the update-local-plugin step is no longer needed.
# $SCRIPTS_DIR/bin/run update-local-plugin
next_version_bump=$($SCRIPTS_DIR/bin/run next-version)
next_version=${1:-$next_version_bump}
Generated
+56 -55
View File
@@ -1412,7 +1412,7 @@
node-forge "^1.2.1"
nullthrows "^1.1.1"
"@expo/[email protected]", "@expo/config-plugins@~9.0.10":
"@expo/[email protected]":
version "9.0.12"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-9.0.12.tgz#f122b2dca22e135eadf6e73442da3ced0ce8aa0a"
integrity sha512-/Ko/NM+GzvJyRkq8PITm8ms0KY5v0wmN1OQFYRMkcJqOi3PjlhndW+G6bHpJI9mkQXBaUnHwAiGLqIC3+MQ5Wg==
@@ -1452,14 +1452,14 @@
xcode "^3.0.1"
xml2js "0.6.0"
"@expo/config-plugins@~9.0.0":
version "9.0.9"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-9.0.9.tgz#3765c310c112e200c6715365d6023f6a05b001ed"
integrity sha512-pbgbY3SwCMwkijhfe163J05BrTx4MqzeaV+nVgUMs7vRcjHY1tfM57Pdv6SPtgeDvZ8fvdXFXXzkJva+a7C9Bw==
"@expo/config-plugins@~9.0.0", "@expo/config-plugins@~9.0.10", "@expo/config-plugins@~9.0.17":
version "9.0.17"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-9.0.17.tgz#c997072209129b9f9616efa3533314b889cfd788"
integrity sha512-m24F1COquwOm7PBl5wRbkT9P9DviCXe0D7S7nQsolfbhdCWuvMkfXeoWmgjtdhy7sDlOyIgBrAdnB6MfsWKqIg==
dependencies:
"@expo/config-types" "^52.0.0"
"@expo/json-file" "~9.0.0"
"@expo/plist" "^0.2.0"
"@expo/config-types" "^52.0.5"
"@expo/json-file" "~9.0.2"
"@expo/plist" "^0.2.2"
"@expo/sdk-runtime-versions" "^1.0.0"
chalk "^4.1.2"
debug "^4.3.5"
@@ -1472,10 +1472,10 @@
xcode "^3.0.1"
xml2js "0.6.0"
"@expo/config-types@^52.0.0":
version "52.0.1"
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-52.0.1.tgz#327af1b72a3a9d4556f41e083e0e284dd8198b96"
integrity sha512-vD8ZetyKV7U29lR6+NJohYeoLYTH+eNYXJeNiSOrWCz0witJYY11meMmEnpEaVbN89EfC6uauSUOa6wihtbyPQ==
"@expo/config-types@^52.0.0", "@expo/config-types@^52.0.5":
version "52.0.5"
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-52.0.5.tgz#e10a226990dd903a4e3db5992ffb3015adf13f38"
integrity sha512-AMDeuDLHXXqd8W+0zSjIt7f37vUd/BP8p43k68NHpyAvQO+z8mbQZm3cNQVAMySeayK2XoPigAFB1JF2NFajaA==
"@expo/config-types@^53.0.3", "@expo/config-types@^53.0.4":
version "53.0.4"
@@ -1521,14 +1521,14 @@
sucrase "3.35.0"
"@expo/config@~10.0.4":
version "10.0.4"
resolved "https://registry.yarnpkg.com/@expo/config/-/config-10.0.4.tgz#a58ec3baef123e77584fbae769484bae2ee74f37"
integrity sha512-pkvdPqKTaP6+Qvc8aTmDLQ9Dfwp98P1GO37MFKwsF5XormfN/9/eN8HfIRoM6d3uSIVKCcWW3X2yAEbNmOyfXw==
version "10.0.11"
resolved "https://registry.yarnpkg.com/@expo/config/-/config-10.0.11.tgz#5371ccb3b08ece4c174d5d7009d61e928e6925b0"
integrity sha512-nociJ4zr/NmbVfMNe9j/+zRlt7wz/siISu7PjdWE4WE+elEGxWWxsGzltdJG0llzrM+khx8qUiFK5aiVcdMBww==
dependencies:
"@babel/code-frame" "~7.10.4"
"@expo/config-plugins" "~9.0.0"
"@expo/config-types" "^52.0.0"
"@expo/json-file" "^9.0.0"
"@expo/config-plugins" "~9.0.17"
"@expo/config-types" "^52.0.5"
"@expo/json-file" "^9.0.2"
deepmerge "^4.3.1"
getenv "^1.0.0"
glob "^10.4.2"
@@ -1549,10 +1549,10 @@
semver "^7.6.2"
zod "^4.1.3"
"@expo/[email protected]7":
version "1.0.237"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-1.0.237.tgz#2a3230540ac85599ee51d01b62c9d7b74ccfad2a"
integrity sha512-bvE7/+QO6hcyeD7Jn5job4jRZJNLep7MDaOsYTZ66xRNYiqY8g2TMKTcg8oOaBvhv8i5WJMJOexNmzkxWFjUtw==
"@expo/[email protected]43":
version "1.0.243"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-1.0.243.tgz#2b390ca1a84b4a4ea06630cf875b96c0954094ab"
integrity sha512-wLU9q0pzIKibr4jLMV/U6BYszk5T85p/WmRYUIqH7nhmy2dDUJHz0WlP/i/ZWAsnrHplwidbCZvZGoxwgJsISA==
dependencies:
"@expo/logger" "1.0.221"
joi "^17.13.1"
@@ -1571,9 +1571,9 @@
getenv "^1.0.0"
"@expo/image-utils@^0.6.0":
version "0.6.3"
resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.6.3.tgz#89c744460beefc686989b969121357bbd5520c8a"
integrity sha512-v/JbCKBrHeudxn1gN1TgfPE/pWJSlLPrl29uXJBgrJFQVkViQvUHQNDhaS+UEa9wYI5HHh7XYmtzAehyG4L+GA==
version "0.6.5"
resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.6.5.tgz#af25e9dd79d1168bebea91dc7f1e6f3efd28643c"
integrity sha512-RsS/1CwJYzccvlprYktD42KjyfWZECH6PPIEowvoSmXfGLfdViwcUEI4RvBfKX5Jli6P67H+6YmHvPTbGOboew==
dependencies:
"@expo/spawn-async" "^1.7.2"
chalk "^4.0.0"
@@ -1595,7 +1595,7 @@
json5 "^2.2.2"
write-file-atomic "^2.3.0"
"@expo/json-file@^9.0.0", "@expo/json-file@~9.0.0":
"@expo/json-file@^9.0.0":
version "9.0.0"
resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-9.0.0.tgz#e3688c9b108cfd7e819f1354a9458ba6e93fc943"
integrity sha512-M+55xFVrFzDcgMDf+52lPDLjKB5xwRfStWlv/b/Vu2OLgxGZLWpxoPYjlRoHqxjPbCQIi2ZCbobK+0KuNhsELg==
@@ -1604,6 +1604,14 @@
json5 "^2.2.3"
write-file-atomic "^2.3.0"
"@expo/json-file@^9.0.2":
version "9.1.5"
resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-9.1.5.tgz#7d7b2dc4990dc2c2de69a571191aba984b7fb7ed"
integrity sha512-prWBhLUlmcQtvN6Y7BpW2k9zXGd3ySa3R6rAguMJkp1z22nunLN64KYTUWfijFlprFoxm9r2VNnGkcbndAlgKA==
dependencies:
"@babel/code-frame" "~7.10.4"
json5 "^2.2.3"
"@expo/json-file@^9.1.4", "@expo/json-file@~9.1.4":
version "9.1.4"
resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-9.1.4.tgz#e719d092c08afb3234643f9285e57c6a24989327"
@@ -1612,6 +1620,15 @@
"@babel/code-frame" "~7.10.4"
json5 "^2.2.3"
"@expo/json-file@~9.0.0", "@expo/json-file@~9.0.2":
version "9.0.2"
resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-9.0.2.tgz#ec508c2ad17490e0c664c9d7e2ae0ce65915d3ed"
integrity sha512-yAznIUrybOIWp3Uax7yRflB0xsEpvIwIEqIjao9SGi2Gaa+N0OamWfe0fnXBSWF+2zzF4VvqwT4W5zwelchfgw==
dependencies:
"@babel/code-frame" "~7.10.4"
json5 "^2.2.3"
write-file-atomic "^2.3.0"
"@expo/[email protected]":
version "1.0.221"
resolved "https://registry.yarnpkg.com/@expo/logger/-/logger-1.0.221.tgz#bd6ef6e03b3c4bf0872843d8b2a83556892178c0"
@@ -1660,7 +1677,7 @@
dependencies:
node-forge "^1.2.1"
"@expo/[email protected]", "@expo/plist@^0.2.0":
"@expo/[email protected]":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.2.0.tgz#beb014c0bfd56a993086c0972ec1ca3ef3f9d36c"
integrity sha512-F/IZJQaf8OIVnVA6XWUeMPC3OH6MV00Wxf0WC0JhTQht2QgjyHUa3U5Gs3vRtDq8tXNsZneOQRDVwpaOnd4zTQ==
@@ -1669,6 +1686,15 @@
base64-js "^1.2.3"
xmlbuilder "^14.0.0"
"@expo/plist@^0.2.0", "@expo/plist@^0.2.2":
version "0.2.2"
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.2.2.tgz#2563b71b4aa78dc9dbc34cc3d2e1011e994bc9cd"
integrity sha512-ZZGvTO6vEWq02UAPs3LIdja+HRO18+LRI5QuDl6Hs3Ps7KX7xU6Y6kjahWKY37Rx2YjNpX07dGpBFzzC+vKa2g==
dependencies:
"@xmldom/xmldom" "~0.7.7"
base64-js "^1.2.3"
xmlbuilder "^14.0.0"
"@expo/plist@^0.3.4":
version "0.3.4"
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.3.4.tgz#0c48eeff2158cf26c5c9ed4f681d24997ccfbeca"
@@ -13831,16 +13857,7 @@ string-length@^5.0.1:
char-regex "^2.0.0"
strip-ansi "^7.0.1"
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -13996,7 +14013,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -14017,13 +14034,6 @@ strip-ansi@^6.0.0:
dependencies:
ansi-regex "^5.0.0"
strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
@@ -15243,16 +15253,7 @@ wordwrap@^1.0.0:
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
[email protected], wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", [email protected], wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==