Compare commits

...
Author SHA1 Message Date
Stanisław Chmiela 8a38cc24e1 v14.7.0 2025-01-30 23:42:13 +01:00
Evan Bacon a85b9e2d80 fix: Skip non-exempt check in non-interactive mode. (#2849)
* Skip non-exempt check in non-interactive mode.

* Update CHANGELOG.md
2025-01-30 14:07:21 -08:00
Szymon Dziedzic e0f69327da [eas-cli] add --submit and -s aliases to --auto-submit flag (#2846)
* [eas-cli] add `--submit` and `-s` aliases to `--auto-submit` flag

* changelog
2025-01-30 19:24:53 +01:00
Expo CI 76d4e65d92 update CHANGELOG.md 2025-01-30 17:19:28 +00:00
Szymon Dziedzic 58917ecbb4 v14.6.0 2025-01-30 18:13:53 +01:00
Szymon Dziedzic e75a102361 upgrade eas-cli-local-build-plugin to 1.0.168 2025-01-30 18:13:46 +01:00
Szymon Dziedzic 061f626b2c [ENG-14625][eas-cli] add eas build:dev command (#2820)
* [eas-cli] add eas dev command

* update logs

* rename command do `build:dev`

* filter by dev client

* apply suggested changes

* use real vs virtaul build profiles

* make it hidden

* prompt to commit after adding new build profile
2025-01-30 18:11:37 +01:00
Piotr Szeremeta 3159550468 Move environment from metadata to job object (#2777)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-14307: move environment to job object from metadata](https://linear.app/expo/issue/ENG-14307/move-environment-to-job-object-from-metadata)

`Environment` should be considering  a first class citizen in job object instead of being stored in the metadata.

# How

* Pass `environment` in `job` object
* remove `environment` from `metadata`.

# Test Plan

1. Start a build specifying `environment` in `eas.json`.
2. Build should use the specified environment (check on website in `Build details`)
2025-01-30 10:31:33 +01:00
Quinlan Jung a20bd0051e [eas-cli] release fingerprint:compare (#2821)
* [eas-cli] release fingerprint:compare

* Temporary Commit at 1/15/2025, 11:33:18 PM

* Temporary Commit at 1/28/2025, 4:41:48 PM

* Temporary Commit at 1/28/2025, 5:03:51 PM

* Temporary Commit at 1/28/2025, 5:33:24 PM

* pr feedbak

* Temporary Commit at 1/29/2025, 4:07:55 PM

* fix lint
2025-01-29 20:54:49 -08:00
Evan Bacon 0b64d1c8df feat(eas-cli): Ensure non-exempt encryption status (#2843)
* Ensure non-exempt encryption status

* Update exemptEncryption.ts

* Update CHANGELOG.md

* add tests

* Update exemptEncryption.ts

* Update exemptEncryption.ts

* Update exemptEncryption.ts
2025-01-29 14:04:11 -08:00
Evan Bacon fe37600323 feat: add internal TestFlight group to submit (#2839)
* feat: add internal TestFlight group to submit

* Update CHANGELOG.md

* fixed transient issue

* Update AppProduce.ts

* refactor

* remove ability to add all users

* Update ensureTestFlightGroup.ts

* Update provisioningProfile.ts

* reduce

* Update target-test.ts
2025-01-29 13:44:38 -08:00
35 changed files with 2131 additions and 252 deletions

No files matched your search

+14 -1
View File
@@ -8,10 +8,23 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Sanitize and generate names for EAS Submit to prevent failures due to invalid characters or taken names. ([#2842](https://github.com/expo/eas-cli/pull/2842) by [@evanbacon](https://github.com/evanbacon))
- Add `--submit` and `-s` as aliases for `--auto-submit` flag. ([#2846](https://github.com/expo/eas-cli/pull/2846) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Skip non-exempt check in non-interactive mode. ([#2849](https://github.com/expo/eas-cli/pull/2849) by [@EvanBacon](https://github.com/EvanBacon))
### 🧹 Chores
## [14.6.0](https://github.com/expo/eas-cli/releases/tag/v14.6.0) - 2025-01-30
### 🎉 New features
- Prompt to set non-exempt encryption status for the iOS app to support faster store submissions. ([#2843](https://github.com/expo/eas-cli/pull/2843) by [@EvanBacon](https://github.com/EvanBacon))
- Automatically create internal TestFlight group in EAS Submit command. ([#2839](https://github.com/expo/eas-cli/pull/2839) by [@evanbacon](https://github.com/evanbacon))
- Sanitize and generate names for EAS Submit to prevent failures due to invalid characters or taken names. ([#2842](https://github.com/expo/eas-cli/pull/2842) by [@evanbacon](https://github.com/evanbacon))
- Release `eas fingerprint:compare`. ([#2821](https://github.com/expo/eas-cli/pull/2821) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Make new autoIncremented builds start at nr 1 by default ([#2828](https://github.com/expo/eas-cli/pull/2828) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "14.5.0",
"version": "14.7.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+130 -76
View File
@@ -69,6 +69,7 @@ eas --help COMMAND
* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
* [`eas build:configure`](#eas-buildconfigure)
* [`eas build:delete [BUILD_ID]`](#eas-builddelete-build_id)
* [`eas build:dev`](#eas-builddev)
* [`eas build:inspect`](#eas-buildinspect)
* [`eas build:list`](#eas-buildlist)
* [`eas build:resign`](#eas-buildresign)
@@ -105,6 +106,7 @@ eas --help COMMAND
* [`eas env:pull [ENVIRONMENT]`](#eas-envpull-environment)
* [`eas env:push [ENVIRONMENT]`](#eas-envpush-environment)
* [`eas env:update [ENVIRONMENT]`](#eas-envupdate-environment)
* [`eas fingerprint:compare [HASH1] [HASH2]`](#eas-fingerprintcompare-hash1-hash2)
* [`eas help [COMMAND]`](#eas-help-command)
* [`eas init`](#eas-init)
* [`eas init:onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-initonboarding-target_project_directory)
@@ -158,7 +160,7 @@ ALIASES
$ eas login
```
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -175,7 +177,7 @@ ALIASES
$ eas logout
```
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -192,7 +194,7 @@ ALIASES
$ eas whoami
```
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -206,7 +208,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/analytics.ts)_
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v14.7.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -258,7 +260,7 @@ DESCRIPTION
create a branch
```
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -279,7 +281,7 @@ DESCRIPTION
delete a branch
```
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -299,7 +301,7 @@ DESCRIPTION
list all branches
```
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -319,7 +321,7 @@ DESCRIPTION
rename a branch
```
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -342,7 +344,7 @@ DESCRIPTION
view a branch
```
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -350,16 +352,16 @@ start a build
```
USAGE
$ eas build [-p android|ios|all] [-e <value>] [--local] [--output <value>] [--wait] [--clear-cache]
[--auto-submit | --auto-submit-with-profile <value>] [-m <value>] [--build-logger-level
trace|debug|info|warn|error|fatal] [--freeze-credentials] [--json --non-interactive]
$ eas build [-p android|ios|all] [-e <value>] [--local] [--output <value>] [--wait] [--clear-cache] [-s |
--auto-submit-with-profile <value>] [-m <value>] [--build-logger-level trace|debug|info|warn|error|fatal]
[--freeze-credentials] [--json --non-interactive]
FLAGS
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to
"production" if defined in eas.json.
-m, --message=<value> A short message describing the build
-p, --platform=(android|ios|all)
--auto-submit Submit on build complete using the submit profile with the
-s, --auto-submit Submit on build complete using the submit profile with the
same name as the build profile
--auto-submit-with-profile=PROFILE_NAME Submit on build complete using the submit profile with
provided name
@@ -379,7 +381,7 @@ DESCRIPTION
start a build
```
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -398,7 +400,7 @@ DESCRIPTION
cancel a build
```
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -415,7 +417,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:delete [BUILD_ID]`
@@ -434,7 +436,27 @@ DESCRIPTION
delete a build
```
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/build/delete.ts)_
## `eas build:dev`
run dev client simulator/emulator build with matching fingerprint or create a new one
```
USAGE
$ eas build:dev [-p ios|android] [-e <value>]
FLAGS
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. It must be a profile allowing to create
emulator/simulator internal distribution dev client builds. The "development-simulator"
build profile will be selected by default.
-p, --platform=(ios|android)
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/v14.7.0/packages/eas-cli/src/commands/build/dev.ts)_
## `eas build:inspect`
@@ -469,7 +491,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -521,7 +543,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -550,7 +572,7 @@ DESCRIPTION
re-sign a build archive
```
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -576,7 +598,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -626,7 +648,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/build/version/get.ts)_
## `eas build:version:set`
@@ -645,7 +667,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -664,7 +686,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -681,7 +703,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -702,7 +724,7 @@ DESCRIPTION
create a channel
```
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -724,7 +746,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -744,7 +766,7 @@ DESCRIPTION
list all channels
```
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:pause [NAME]`
@@ -766,7 +788,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/channel/pause.ts)_
## `eas channel:resume [NAME]`
@@ -788,7 +810,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/channel/resume.ts)_
## `eas channel:rollout [CHANNEL]`
@@ -821,7 +843,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -844,7 +866,7 @@ DESCRIPTION
view a channel
```
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -865,7 +887,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/v14.5.0/packages/eas-cli/src/commands/config.ts)_
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v14.7.0/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -882,7 +904,7 @@ DESCRIPTION
manage credentials
```
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/credentials/index.ts)_
## `eas credentials:configure-build`
@@ -900,7 +922,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
## `eas deploy [options]`
@@ -928,7 +950,7 @@ ALIASES
$ eas worker:deploy
```
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/deploy/index.ts)_
## `eas deploy:alias`
@@ -953,7 +975,7 @@ ALIASES
$ eas deploy:promote
```
_See code: [packages/eas-cli/src/commands/deploy/alias.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/deploy/alias.ts)_
_See code: [packages/eas-cli/src/commands/deploy/alias.ts](https://github.com/expo/eas-cli/blob/v14.7.0/packages/eas-cli/src/commands/deploy/alias.ts)_
## `eas deploy:promote`
@@ -990,7 +1012,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -1010,7 +1032,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -1031,7 +1053,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:rename`
@@ -1052,7 +1074,7 @@ DESCRIPTION
rename a registered device
```
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/device/rename.ts)_
## `eas device:view [UDID]`
@@ -1066,7 +1088,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -1080,7 +1102,7 @@ DESCRIPTION
display environment info
```
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v14.7.0/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas env:create [ENVIRONMENT]`
@@ -1109,7 +1131,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/env/create.ts)_
## `eas env:delete [ENVIRONMENT]`
@@ -1133,7 +1155,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/env/delete.ts)_
## `eas env:exec ENVIRONMENT BASH_COMMAND`
@@ -1154,7 +1176,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/env/exec.ts)_
## `eas env:get [ENVIRONMENT]`
@@ -1179,7 +1201,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/env/get.ts)_
## `eas env:list [ENVIRONMENT]`
@@ -1204,7 +1226,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/env/list.ts)_
## `eas env:pull [ENVIRONMENT]`
@@ -1227,7 +1249,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/env/pull.ts)_
## `eas env:push [ENVIRONMENT]`
@@ -1248,7 +1270,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/env/push.ts)_
## `eas env:update [ENVIRONMENT]`
@@ -1279,7 +1301,39 @@ 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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/env/update.ts)_
## `eas fingerprint:compare [HASH1] [HASH2]`
compare fingerprints of the current project, builds and updates
```
USAGE
$ eas fingerprint:compare [HASH1] [HASH2] [--build-id <value>] [--json --non-interactive]
ARGUMENTS
HASH1 If provided alone, HASH1 is compared against the current project's fingerprint.
HASH2 If two hashes are provided, HASH1 is compared against HASH2.
FLAGS
--build-id=<value> Compare the fingerprint with the build with the specified ID
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
compare fingerprints of the current project, builds and updates
EXAMPLES
$ eas fingerprint:compare # Compare fingerprints in interactive mode
$ eas fingerprint:compare c71a7d475aa6f75291bc93cd74aef395c3c94eee # Compare fingerprint against local directory
$ eas fingerprint:compare c71a7d475aa6f75291bc93cd74aef395c3c94eee f0d6a916e73f401d428e6e006e07b12453317ba2 # Compare provided fingerprints
$ eas fingerprint:compare --build-id 82bc6456-611a-48cb-8db4-5f9eb2ca1003 # Compare fingerprint from build against local directory
```
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v14.7.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
## `eas help [COMMAND]`
@@ -1387,7 +1441,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -1404,7 +1458,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -1421,7 +1475,7 @@ DESCRIPTION
sync the local store configuration to the app stores
```
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1451,7 +1505,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/v14.5.0/packages/eas-cli/src/commands/open.ts)_
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v14.7.0/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -1465,7 +1519,7 @@ DESCRIPTION
information about the current project
```
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -1488,7 +1542,7 @@ ALIASES
$ eas init
```
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/project/init.ts)_
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1506,7 +1560,7 @@ ALIASES
$ eas onboarding
```
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/project/onboarding.ts)_
## `eas submit`
@@ -1537,7 +1591,7 @@ ALIASES
$ eas build:submit
```
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/submit.ts)_
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v14.7.0/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1579,7 +1633,7 @@ DESCRIPTION
publish an update group
```
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1599,7 +1653,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1620,7 +1674,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:edit [GROUPID]`
@@ -1644,7 +1698,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/update/edit.ts)_
## `eas update:list`
@@ -1666,7 +1720,7 @@ DESCRIPTION
view the recent updates
```
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
@@ -1698,7 +1752,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:roll-back-to-embedded`
@@ -1726,7 +1780,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
## `eas update:rollback`
@@ -1746,7 +1800,7 @@ DESCRIPTION
roll back to an embedded update or an existing update
```
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/update/rollback.ts)_
## `eas update:view GROUPID`
@@ -1766,7 +1820,7 @@ DESCRIPTION
update group details
```
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1787,7 +1841,7 @@ DESCRIPTION
create a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1807,7 +1861,7 @@ DESCRIPTION
delete a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1825,7 +1879,7 @@ DESCRIPTION
list webhooks
```
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1847,7 +1901,7 @@ DESCRIPTION
update a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1864,7 +1918,7 @@ DESCRIPTION
view a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
@@ -1948,7 +2002,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/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/workflow/create.ts)_
## `eas workflow:run [FILE]`
@@ -1968,7 +2022,7 @@ DESCRIPTION
Run an EAS workflow
```
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/workflow/run.ts)_
## `eas workflow:validate PATH`
@@ -1988,5 +2042,5 @@ DESCRIPTION
validate a workflow configuration yaml file
```
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v14.5.0/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/v14.7.0/packages/eas-cli/src/commands/workflow/validate.ts)_
<!-- commandsstop -->
+3
View File
@@ -12,3 +12,6 @@ if (process.env.TMPDIR) {
(fs.realpath as any).native = jest.fn();
module.exports = fs;
// Ensure requiring node:fs returns the mock too. This is needed for expo/config and expo/json-file.
jest.mock('node:fs', () => fs);
+446 -18
View File
@@ -6923,6 +6923,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environment",
"description": null,
"type": {
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "experimental",
"description": null,
@@ -10373,6 +10385,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "filter",
"description": null,
"type": {
"kind": "INPUT_OBJECT",
"name": "UpdateFilterInput",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "first",
"description": null,
@@ -19744,7 +19768,7 @@
"ofType": null
},
"isDeprecated": true,
"deprecationReason": "Use 'runtime.fingerprintDebugInfoUrl' instead."
"deprecationReason": "Use 'runtime.fingerprint.debugInfoUrl' instead."
},
{
"name": "xcodeBuildLogsUrl",
@@ -19987,6 +20011,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "developmentClient",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "distribution",
"description": null,
@@ -28068,6 +28104,55 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deleteBulkEnvironmentVariables",
"description": "Bulk delete environment variables",
"args": [
{
"name": "ids",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DeleteEnvironmentVariableResult",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deleteEnvironmentVariable",
"description": "Delete an environment variable",
@@ -28272,6 +28357,55 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateBulkEnvironmentVariables",
"description": "Bulk update environment variables",
"args": [
{
"name": "environmentVariablesData",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "UpdateEnvironmentVariableInput",
"ofType": null
}
}
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "EnvironmentVariable",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateEnvironmentVariable",
"description": "Update an environment variable",
@@ -30085,7 +30219,7 @@
"fields": [
{
"name": "account",
"description": null,
"description": "The Expo account that owns the installation entity.",
"args": [],
"type": {
"kind": "NON_NULL",
@@ -30296,6 +30430,29 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "GitHubAppInstallationAccountType",
"description": null,
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "ORGANIZATION",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "USER",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GitHubAppInstallationMetadata",
@@ -30315,7 +30472,7 @@
},
{
"name": "githubAccountName",
"description": null,
"description": "The login of the GitHub account that owns the installation. Not the display name.",
"args": [],
"type": {
"kind": "SCALAR",
@@ -30325,6 +30482,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "githubAccountType",
"description": null,
"args": [],
"type": {
"kind": "ENUM",
"name": "GitHubAppInstallationAccountType",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "installationStatus",
"description": null,
@@ -34867,6 +35036,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environment",
"description": null,
"type": {
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "experimental",
"description": null,
@@ -35709,6 +35890,30 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "artifacts",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "WorkflowArtifact",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "childJobRun",
"description": null,
@@ -39891,18 +40096,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "runtimeFingerprintSource",
"description": null,
"type": {
"kind": "INPUT_OBJECT",
"name": "FingerprintSourceInput",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "runtimeVersion",
"description": null,
@@ -43086,12 +43279,12 @@
"deprecationReason": null
},
{
"name": "fingerprintDebugInfoUrl",
"name": "fingerprint",
"description": null,
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"kind": "OBJECT",
"name": "Fingerprint",
"ofType": null
},
"isDeprecated": false,
@@ -48693,6 +48886,53 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateFilterInput",
"description": null,
"fields": null,
"inputFields": [
{
"name": "fingerprintHash",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "hasFingerprint",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "runtimeVersion",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateGitHubBuildTriggerInput",
@@ -58844,6 +59084,149 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "WorkflowArtifact",
"description": null,
"fields": [
{
"name": "contentType",
"description": null,
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "downloadUrl",
"description": null,
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "fileSizeBytes",
"description": null,
"args": [],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "filename",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "jobRun",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "JobRun",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updatedAt",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "WorkflowJob",
@@ -60367,6 +60750,18 @@
"name": "retryWorkflowRun",
"description": null,
"args": [
{
"name": "fromFailedJobs",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "workflowRunId",
"description": null,
@@ -60513,6 +60908,30 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "GITHUB_PULL_REQUEST_OPENED",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "GITHUB_PULL_REQUEST_REOPENED",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "GITHUB_PULL_REQUEST_SYNCHRONIZE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "GITHUB_PUSH",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "MANUAL",
"description": null,
@@ -61643,6 +62062,15 @@
}
]
},
{
"name": "oneOf",
"description": "Indicates exactly one field must be supplied and this field must not be `null`.",
"isRepeatable": false,
"locations": [
"INPUT_OBJECT"
],
"args": []
},
{
"name": "skip",
"description": "Directs the executor to skip this field or fragment when the `if` argument is true.",
+2 -2
View File
@@ -1,14 +1,14 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "14.5.0",
"version": "14.7.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "2.1.5",
"@expo/apple-utils": "2.1.6",
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "10.0.6",
"@expo/config-plugins": "9.0.12",
@@ -8,6 +8,7 @@ import {
transformProjectArchive,
transformWorkflow,
} from '../graphql';
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
export function transformJob(job: Android.Job): AndroidJobInput {
return {
@@ -29,6 +30,7 @@ export function transformJob(job: Android.Job): AndroidJobInput {
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
environment: buildProfileEnvironmentToEnvironment(job.environment),
loggerLevel: job.loggerLevel
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
: undefined,
@@ -92,6 +92,7 @@ export async function prepareJobAsync(
gradleCommand: buildProfile.gradleCommand,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths,
environment: ctx.buildProfile.environment,
buildType,
username,
...(ctx.android.versionCodeOverride && {
+73
View File
@@ -4,6 +4,7 @@ import fs from 'fs-extra';
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
import Log, { learnMore } from '../log';
import { ora } from '../ora';
import { easCliVersion } from '../utils/easCli';
import { Client } from '../vcs/vcs';
@@ -52,6 +53,78 @@ async function configureAsync({
}
}
export async function doesBuildProfileExistAsync({
projectDir,
profileName,
}: {
projectDir: string;
profileName: string;
}): Promise<boolean> {
try {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJson = await easJsonAccessor.readRawJsonAsync();
if (!easJson.build?.[profileName]) {
return false;
}
return true;
} catch (error) {
Log.error(`We were unable to read ${chalk.bold('eas.json')} contents. Error: ${error}.`);
throw error;
}
}
export async function createBuildProfileAsync({
projectDir,
profileName,
profileContents,
vcsClient,
nonInteractive,
}: {
projectDir: string;
profileName: string;
profileContents: Record<string, any>;
vcsClient: Client;
nonInteractive: boolean;
}): Promise<void> {
const spinner = ora(`Adding "${profileName}" build profile to ${chalk.bold('eas.json')}`).start();
try {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
return {
...easJsonRawObject,
build: {
...easJsonRawObject.build,
[profileName]: profileContents,
},
};
});
await easJsonAccessor.writeAsync();
spinner.succeed(
`Successfully added "${profileName}" build profile to ${chalk.bold('eas.json')}.`
);
if (await vcsClient.isCommitRequiredAsync()) {
Log.newLine();
await reviewAndCommitChangesAsync(
vcsClient,
`Add "${profileName}" build profile to eas.json`,
{
nonInteractive,
}
);
}
} catch (error) {
spinner.fail(
`We were not able to configure "${profileName}" build profile inside of ${chalk.bold(
'eas.json'
)}. Error: ${error}.`
);
throw error;
}
}
const EAS_JSON_DEFAULT: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
@@ -1,17 +1,12 @@
import { Env } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { isEnvironment } from './utils/environment';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
import Log, { learnMore } from '../log';
function isEnvironment(env: string): env is EnvironmentVariableEnvironment {
return Object.values(EnvironmentVariableEnvironment).includes(
env as EnvironmentVariableEnvironment
);
}
export async function evaluateConfigWithEnvVarsAsync<Config extends { projectId: string }, Opts>({
buildProfile,
buildProfileName,
+2
View File
@@ -10,6 +10,7 @@ import { IosCredentials } from '../../credentials/ios/types';
import { BuildParamsInput } from '../../graphql/generated';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
import { ensureNonExemptEncryptionIsDefinedForManagedProjectAsync } from '../../project/ios/exemptEncryption';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
@@ -28,6 +29,7 @@ export async function createIosContextAsync(
if (ctx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx);
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync(ctx);
}
checkNodeEnvVariable(ctx);
@@ -9,6 +9,7 @@ import {
transformProjectArchive,
transformWorkflow,
} from '../graphql';
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
export function transformJob(job: Ios.Job): IosJobInput {
return {
@@ -31,6 +32,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
environment: buildProfileEnvironmentToEnvironment(job.environment),
loggerLevel: job.loggerLevel
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
: undefined,
@@ -88,6 +88,7 @@ export async function prepareJobAsync(
buildConfiguration: buildProfile.buildConfiguration,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths,
environment: ctx.buildProfile.environment,
username,
...(ctx.ios.buildNumberOverride && {
version: {
+1 -1
View File
@@ -7,7 +7,7 @@ import Log from '../log';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '1.0.163';
const PLUGIN_PACKAGE_VERSION = '1.0.168';
export enum LocalBuildMode {
/**
-1
View File
@@ -62,7 +62,6 @@ export async function collectMetadataAsync<T extends Platform>(
requiredPackageManager: ctx.requiredPackageManager ?? undefined,
selectedImage: ctx.buildProfile.image,
customNodeVersion: ctx.buildProfile.node,
environment: ctx.buildProfile.environment,
simulator: 'simulator' in ctx.buildProfile && ctx.buildProfile.simulator,
};
return sanitizeMetadata(metadata);
+42 -23
View File
@@ -99,15 +99,27 @@ export interface BuildFlags {
repack: boolean;
}
export async function runBuildAndSubmitAsync(
graphqlClient: ExpoGraphqlClient,
analytics: Analytics,
vcsClient: Client,
projectDir: string,
flags: BuildFlags,
actor: Actor,
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn
): Promise<{
export async function runBuildAndSubmitAsync({
graphqlClient,
analytics,
vcsClient,
projectDir,
flags,
actor,
getDynamicPrivateProjectConfigAsync,
downloadSimBuildAutoConfirm,
envOverride,
}: {
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
vcsClient: Client;
projectDir: string;
flags: BuildFlags;
actor: Actor;
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
downloadSimBuildAutoConfirm?: boolean;
envOverride?: Env;
}): Promise<{
buildIds: string[];
}> {
await vcsClient.ensureRepoExistsAsync();
@@ -192,13 +204,15 @@ export async function runBuildAndSubmitAsync(
for (const buildProfile of buildProfiles) {
const platform = toAppPlatform(buildProfile.platform);
const { env } = await evaluateConfigWithEnvVarsAsync({
buildProfile: buildProfile.profile,
buildProfileName: buildProfile.profileName,
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: buildProfile.profile.env },
});
const { env } = !envOverride
? await evaluateConfigWithEnvVarsAsync({
buildProfile: buildProfile.profile,
buildProfileName: buildProfile.profileName,
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: buildProfile.profile.env },
})
: { env: envOverride };
const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
projectDir,
@@ -308,7 +322,7 @@ export async function runBuildAndSubmitAsync(
[BuildStatus.Errored, BuildStatus.Canceled, BuildStatus.PendingCancel].includes(build?.status)
);
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags);
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags, downloadSimBuildAutoConfirm);
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
if (flags.json) {
@@ -542,7 +556,8 @@ async function downloadAndRunAsync(build: BuildFragment): Promise<void> {
async function maybeDownloadAndRunSimulatorBuildsAsync(
builds: MaybeBuildFragment[],
flags: BuildFlags
flags: BuildFlags,
autoConfirm?: boolean
): Promise<void> {
const simBuilds = builds.filter(truthy).filter(isRunnableOnSimulatorOrEmulator);
@@ -550,11 +565,15 @@ async function maybeDownloadAndRunSimulatorBuildsAsync(
for (const simBuild of simBuilds) {
if (simBuild.platform === AppPlatform.Android || process.platform === 'darwin') {
Log.newLine();
const confirm = await confirmAsync({
message: `Install and run the ${
simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS'
} build on ${simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator'}?`,
});
const confirm =
autoConfirm ??
(await confirmAsync({
message: `Install and run the ${
simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS'
} build on ${
simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator'
}?`,
}));
if (confirm) {
await downloadAndRunAsync(simBuild);
}
@@ -0,0 +1,26 @@
import { BuildProfile } from '@expo/eas-json';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
type Environment = NonNullable<BuildProfile['environment']>;
const BuildProfileEnvironmentToEnvironment: Record<Environment, EnvironmentVariableEnvironment> = {
production: EnvironmentVariableEnvironment.Production,
preview: EnvironmentVariableEnvironment.Preview,
development: EnvironmentVariableEnvironment.Development,
};
export function isEnvironment(env: string): env is EnvironmentVariableEnvironment {
return Object.values(EnvironmentVariableEnvironment).includes(
env as EnvironmentVariableEnvironment
);
}
export function buildProfileEnvironmentToEnvironment(
environment: BuildProfile['environment']
): EnvironmentVariableEnvironment | null {
if (!environment) {
return null;
}
return BuildProfileEnvironmentToEnvironment[environment];
}
+297
View File
@@ -0,0 +1,297 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJsonAccessor } from '@expo/eas-json';
import { Errors, Flags } from '@oclif/core';
import {
createBuildProfileAsync,
doesBuildProfileExistAsync,
ensureProjectConfiguredAsync,
} from '../../build/configure';
import { evaluateConfigWithEnvVarsAsync } from '../../build/evaluateConfigWithEnvVarsAsync';
import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { BuildStatus, DistributionType } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { toAppPlatform } from '../../graphql/types/AppPlatform';
import Log from '../../log';
import { RequestedPlatform } from '../../platform';
import { resolveWorkflowAsync } from '../../project/workflow';
import { confirmAsync, promptAsync } from '../../prompts';
import { runAsync } from '../../run/run';
import { downloadAndMaybeExtractAppAsync } from '../../utils/download';
import { createFingerprintAsync } from '../../utils/fingerprintCli';
import { ProfileData, getProfilesAsync } from '../../utils/profiles';
import { Client } from '../../vcs/vcs';
const DEFAULT_EAS_BUILD_RUN_PROFILE_NAME = 'development-simulator';
export default class BuildDev extends EasCommand {
static override hidden: true;
static override description =
'run dev client simulator/emulator build with matching fingerprint or create a new one';
static override flags = {
platform: Flags.enum<Platform.IOS | Platform.ANDROID>({
char: 'p',
options: [Platform.IOS, Platform.ANDROID],
}),
profile: Flags.string({
char: 'e',
description: `Name of the build profile from eas.json. It must be a profile allowing to create emulator/simulator internal distribution dev client builds. The "${DEFAULT_EAS_BUILD_RUN_PROFILE_NAME}" build profile will be selected by default.`,
helpValue: 'PROFILE_NAME',
}),
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Vcs,
...this.ContextOptions.Analytics,
...this.ContextOptions.ProjectId,
};
protected override async runAsync(): Promise<any> {
const { flags } = await this.parse(BuildDev);
const {
loggedIn: { actor, graphqlClient },
getDynamicPrivateProjectConfigAsync,
projectDir,
analytics,
vcsClient,
projectId,
} = await this.getContextAsync(BuildDev, {
nonInteractive: false,
withServerSideEnvironment: null,
});
const platform = await this.selectPlatformAsync(flags.platform);
if (process.platform !== 'darwin' && platform === Platform.IOS) {
Errors.error('Running iOS builds in simulator is only supported on macOS.', { exit: 1 });
}
await vcsClient.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(vcsClient, flags.nonInteractive);
await ensureProjectConfiguredAsync({
projectDir,
nonInteractive: false,
vcsClient,
});
const buildProfile = await this.ensureValidBuildRunProfileExistsAsync({
projectDir,
platform,
selectedBuildProfileName: flags.profile,
vcsClient,
});
const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient);
const { env } = await evaluateConfigWithEnvVarsAsync({
buildProfile: buildProfile.profile,
buildProfileName: buildProfile.profileName,
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: buildProfile.profile.env },
});
const fingerprint = await createFingerprintAsync(projectDir, {
env,
workflow,
platforms: [platform],
});
if (!fingerprint) {
Errors.error('Failed to calculate fingerprint', { exit: 1 });
}
Log.log(`✨ Calculated fingerprint hash: ${fingerprint.hash}`);
Log.newLine();
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
filter: {
platform: toAppPlatform(platform),
fingerprintHash: fingerprint.hash,
status: BuildStatus.Finished,
simulator: platform === Platform.IOS ? true : undefined,
distribution: platform === Platform.ANDROID ? DistributionType.Internal : undefined,
developmentClient: true,
},
offset: 0,
limit: 1,
});
if (builds.length !== 0) {
const build = builds[0];
Log.succeed(
`🎯 Found successful build with matching fingerprint on EAS servers. Running it...`
);
if (build.artifacts?.applicationArchiveUrl) {
const buildPath = await downloadAndMaybeExtractAppAsync(
build.artifacts.applicationArchiveUrl,
build.platform
);
await runAsync(buildPath, build.platform);
return;
} else {
Log.warn('Artifacts for this build expired. New build will be started.');
}
}
Log.log('🚀 No successful build with matching fingerprint found. Starting a new build...');
await runBuildAndSubmitAsync({
graphqlClient,
analytics,
vcsClient,
projectDir,
flags: {
requestedPlatform:
platform === Platform.ANDROID ? RequestedPlatform.Android : RequestedPlatform.Ios,
nonInteractive: false,
freezeCredentials: false,
wait: true,
clearCache: false,
json: false,
autoSubmit: false,
localBuildOptions: {},
repack: false,
profile: flags.profile ?? DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
},
actor,
getDynamicPrivateProjectConfigAsync,
downloadSimBuildAutoConfirm: true,
envOverride: env,
});
}
private async selectPlatformAsync(platform?: Platform): Promise<Platform> {
if (platform) {
return platform;
}
const { resolvedPlatform } = await promptAsync({
type: 'select',
message: 'Select platform',
name: 'resolvedPlatform',
choices: [
{ title: 'Android', value: Platform.ANDROID },
{ title: 'iOS', value: Platform.IOS },
],
});
return resolvedPlatform;
}
private async validateBuildRunProfileAsync({
platform,
buildProfile,
buildProfileName,
}: {
platform: Platform;
buildProfile: BuildProfile;
buildProfileName: string;
}): Promise<void> {
if (buildProfile.developmentClient !== true) {
Errors.error(
`Profile "${buildProfileName}" must specify "developmentClient: true" to create a dev client build. Select a different profile or update the profile in eas.json.`,
{ exit: 1 }
);
}
if (buildProfile.distribution !== 'internal') {
Errors.error(
`Profile "${buildProfileName}" must specify "distribution: internal" in order to work with eas build:dev command. Select a different profile or update the profile in eas.json.`,
{ exit: 1 }
);
}
if (platform === Platform.IOS) {
const iosProfile = buildProfile as BuildProfile<Platform.IOS>;
if (iosProfile.simulator !== true && iosProfile.withoutCredentials !== true) {
Errors.error(
`Profile "${buildProfileName}" must specify "ios.simulator: true" or "withoutCredentials: true" to create an iOS simulator build. Select a different profile or update the profile in eas.json.`,
{ exit: 1 }
);
}
} else {
const androidProfile = buildProfile as BuildProfile<Platform.ANDROID>;
if (
androidProfile.distribution !== 'internal' &&
androidProfile.withoutCredentials !== true
) {
Errors.error(
`Profile "${buildProfileName}" must specify "distribution: internal" or "withoutCredentials: true" to create an Android emulator build. Select a different profile or update the profile in eas.json.`,
{ exit: 1 }
);
}
}
}
private async ensureValidBuildRunProfileExistsAsync({
projectDir,
platform,
selectedBuildProfileName,
vcsClient,
}: {
projectDir: string;
platform: Platform;
selectedBuildProfileName?: string;
vcsClient: Client;
}): Promise<ProfileData<'build'>> {
if (
!!selectedBuildProfileName ||
(await doesBuildProfileExistAsync({
projectDir,
profileName: DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
}))
) {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const [buildProfile] = await getProfilesAsync({
type: 'build',
easJsonAccessor,
platforms: [platform],
profileName: selectedBuildProfileName ?? DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
projectDir,
});
await this.validateBuildRunProfileAsync({
buildProfileName: selectedBuildProfileName ?? DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
platform,
buildProfile: buildProfile.profile,
});
} else {
const createBuildProfile = await confirmAsync({
message: `We want to go ahead and generate "${DEFAULT_EAS_BUILD_RUN_PROFILE_NAME}" build profile for you, that matches eas build:dev criteria. Do you want to proceed?`,
});
if (!createBuildProfile) {
Errors.error(
'Come back later or specify different build compliant with eas build:dev requirements by using "--profile" flag.',
{ exit: 1 }
);
}
await createBuildProfileAsync({
projectDir,
profileName: DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
profileContents: {
developmentClient: true,
distribution: 'internal',
ios: {
simulator: true,
},
environment: 'development',
},
nonInteractive: false,
vcsClient,
});
}
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const [buildProfile] = await getProfilesAsync({
type: 'build',
easJsonAccessor,
platforms: [platform],
profileName: selectedBuildProfileName ?? DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
projectDir,
});
return buildProfile;
}
}
+6 -4
View File
@@ -78,10 +78,12 @@ export default class Build extends EasCommand {
description: 'Clear cache before the build',
}),
'auto-submit': Flags.boolean({
char: 's',
default: false,
description:
'Submit on build complete using the submit profile with the same name as the build profile',
exclusive: ['auto-submit-with-profile'],
aliases: ['submit'],
}),
'auto-submit-with-profile': Flags.string({
description: 'Submit on build complete using the submit profile with provided name',
@@ -158,15 +160,15 @@ export default class Build extends EasCommand {
const flagsWithPlatform = await this.ensurePlatformSelectedAsync(flags);
await runBuildAndSubmitAsync(
await runBuildAndSubmitAsync({
graphqlClient,
analytics,
vcsClient,
projectDir,
flagsWithPlatform,
flags: flagsWithPlatform,
actor,
getDynamicPrivateProjectConfigAsync
);
getDynamicPrivateProjectConfigAsync,
});
}
private sanitizeFlags(
@@ -99,12 +99,12 @@ export default class BuildInspect extends EasCommand {
await this.copyToOutputDirAsync(tmpWorkingdir, outputDirectory);
} else {
try {
await runBuildAndSubmitAsync(
await runBuildAndSubmitAsync({
graphqlClient,
analytics,
vcsClient,
projectDir,
{
flags: {
nonInteractive: false,
freezeCredentials: false,
wait: true,
@@ -124,8 +124,8 @@ export default class BuildInspect extends EasCommand {
repack: false,
},
actor,
getDynamicPrivateProjectConfigAsync
);
getDynamicPrivateProjectConfigAsync,
});
if (!flags.verbose) {
Log.log(chalk.green('Build successful'));
}
@@ -70,12 +70,12 @@ export default class BuildInternal extends EasCommand {
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await runBuildAndSubmitAsync(
await runBuildAndSubmitAsync({
graphqlClient,
analytics,
vcsClient,
projectDir,
{
flags: {
requestedPlatform: flags.platform as RequestedPlatform,
profile: flags.profile,
nonInteractive: true,
@@ -91,7 +91,7 @@ export default class BuildInternal extends EasCommand {
repack: false,
},
actor,
getDynamicPrivateProjectConfigAsync
);
getDynamicPrivateProjectConfigAsync,
});
}
}
@@ -1,4 +1,4 @@
import { Platform } from '@expo/eas-build-job';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -6,9 +6,15 @@ import EasCommand from '../../commandUtils/EasCommand';
import { fetchBuildsAsync, formatBuild } from '../../commandUtils/builds';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { AppPlatform, BuildStatus } from '../../graphql/generated';
import {
AppPlatform,
BuildFragment,
BuildStatus,
FingerprintFragment,
} from '../../graphql/generated';
import { FingerprintMutation } from '../../graphql/mutations/FingerprintMutation';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { FingerprintQuery } from '../../graphql/queries/FingerprintQuery';
import Log from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
@@ -20,11 +26,52 @@ import { Fingerprint, FingerprintDiffItem } from '../../utils/fingerprint';
import { createFingerprintAsync, diffFingerprint } from '../../utils/fingerprintCli';
import { abridgedDiff } from '../../utils/fingerprintDiff';
import formatFields, { FormatFieldsItem } from '../../utils/formatFields';
import { enableJsonOutput } from '../../utils/json';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { Client } from '../../vcs/vcs';
export interface FingerprintCompareFlags {
buildId?: string;
hash1?: string;
hash2?: string;
nonInteractive: boolean;
json: boolean;
}
enum FingerprintOriginType {
Build = 'build',
Hash = 'hash',
Project = 'project',
}
type FingerprintOrigin = {
type: FingerprintOriginType;
build?: BuildFragment;
};
export default class FingerprintCompare extends EasCommand {
static override description = 'compare fingerprints of the current project, builds and updates';
static override hidden = true;
static override strict = false;
static override examples = [
'$ eas fingerprint:compare \t # Compare fingerprints in interactive mode',
'$ eas fingerprint:compare c71a7d475aa6f75291bc93cd74aef395c3c94eee \t # Compare fingerprint against local directory',
'$ eas fingerprint:compare c71a7d475aa6f75291bc93cd74aef395c3c94eee f0d6a916e73f401d428e6e006e07b12453317ba2 \t # Compare provided fingerprints',
'$ eas fingerprint:compare --build-id 82bc6456-611a-48cb-8db4-5f9eb2ca1003 \t # Compare fingerprint from build against local directory',
];
static override args = [
{
name: 'hash1',
description:
"If provided alone, HASH1 is compared against the current project's fingerprint.",
required: false,
},
{
name: 'hash2',
description: 'If two hashes are provided, HASH1 is compared against HASH2.',
required: false,
},
];
static override flags = {
'build-id': Flags.string({
@@ -42,8 +89,10 @@ export default class FingerprintCompare extends EasCommand {
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(FingerprintCompare);
const { json: jsonFlag, 'non-interactive': nonInteractive, buildId: buildIdFromArg } = flags;
const { args, flags } = await this.parse(FingerprintCompare);
const { hash1, hash2 } = args;
const { json, 'non-interactive': nonInteractive, 'build-id': buildId } = flags;
const sanitizedFlagsAndArgs = { json, nonInteractive, buildId, hash1, hash2 };
const {
projectId,
@@ -54,70 +103,52 @@ export default class FingerprintCompare extends EasCommand {
nonInteractive,
withServerSideEnvironment: null,
});
if (jsonFlag) {
if (json) {
enableJsonOutput();
}
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
let buildId: string | null = buildIdFromArg;
if (!buildId) {
if (nonInteractive) {
throw new Error('Build ID must be provided in non-interactive mode');
}
buildId = await selectBuildToCompareAsync(graphqlClient, projectId, displayName, {
filters: { hasFingerprint: true },
});
if (!buildId) {
return;
}
}
Log.log(`Comparing fingerprints of the current project and build ${buildId}…`);
const buildWithFingerprint = await BuildQuery.withFingerprintByIdAsync(graphqlClient, buildId);
const fingerprintDebugUrl = buildWithFingerprint.fingerprint?.debugInfoUrl;
if (!fingerprintDebugUrl) {
Log.error('A fingerprint for the build could not be found.');
return;
}
const fingerprintResponse = await fetch(fingerprintDebugUrl);
const fingerprint = (await fingerprintResponse.json()) as Fingerprint;
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
const buildPlatform = buildWithFingerprint.platform;
const workflow = workflows[appPlatformToPlatform(buildPlatform)];
const projectFingerprint = await createFingerprintAsync(projectDir, {
workflow,
platforms: [appPlatformToString(buildPlatform)],
debug: true,
env: undefined,
});
if (!projectFingerprint) {
Log.error('Project fingerprints can only be computed for projects with SDK 52 or higher');
return;
}
const uploadedFingerprint = await maybeUploadFingerprintAsync({
hash: fingerprint.hash,
fingerprint: {
fingerprintSources: fingerprint.sources,
isDebugFingerprintSource: Log.isDebug,
},
const firstFingerprintInfo = await getFirstFingerprintInfoAsync(
graphqlClient,
});
await FingerprintMutation.createFingerprintAsync(graphqlClient, projectId, {
hash: uploadedFingerprint.hash,
source: uploadedFingerprint.fingerprintSource,
});
projectId,
sanitizedFlagsAndArgs
);
const { fingerprint: firstFingerprint, origin: firstFingerprintOrigin } = firstFingerprintInfo;
if (fingerprint.hash === projectFingerprint.hash) {
Log.log(`✅ Project fingerprint matches build`);
const secondFingerprintInfo = await getSecondFingerprintInfoAsync(
graphqlClient,
projectDir,
projectId,
vcsClient,
firstFingerprintInfo,
sanitizedFlagsAndArgs
);
const { fingerprint: secondFingerprint, origin: secondFingerprintOrigin } =
secondFingerprintInfo;
if (json) {
printJsonOnlyOutput({ fingerprint1: firstFingerprint, fingerprint2: secondFingerprint });
return;
}
if (firstFingerprint.hash === secondFingerprint.hash) {
Log.log(
`✅ ${capitalizeFirstLetter(
prettyPrintFingerprint(firstFingerprint, firstFingerprintOrigin)
)} matches fingerprint from ${prettyPrintFingerprint(
secondFingerprint,
secondFingerprintOrigin
)}`
);
return;
} else {
Log.log(`🔄 Project fingerprint differs from build`);
Log.log(
`🔄 ${capitalizeFirstLetter(
prettyPrintFingerprint(firstFingerprint, firstFingerprintOrigin)
)} differs from ${prettyPrintFingerprint(secondFingerprint, secondFingerprintOrigin)}`
);
}
const fingerprintDiffs = diffFingerprint(projectDir, fingerprint, projectFingerprint);
const fingerprintDiffs = diffFingerprint(projectDir, firstFingerprint, secondFingerprint);
if (!fingerprintDiffs) {
Log.error('Fingerprint diffs can only be computed for projects with SDK 52 or higher');
return;
@@ -170,6 +201,204 @@ export default class FingerprintCompare extends EasCommand {
}
}
function prettyPrintFingerprint(fingerprint: Fingerprint, origin: FingerprintOrigin): string {
if (origin.type === FingerprintOriginType.Hash) {
return `fingerprint ${fingerprint.hash} from hash`;
}
return `fingerprint ${fingerprint.hash} from ${origin.type}`;
}
function capitalizeFirstLetter(string: string): string {
return string.charAt(0).toUpperCase() + string.slice(1);
}
async function getFirstFingerprintInfoAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
{ buildId: buildIdFromArg, hash1, nonInteractive }: FingerprintCompareFlags
): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
if (hash1) {
const fingerprintFragment = await getFingerprintFragmentFromHashAsync(
graphqlClient,
projectId,
hash1
);
const fingerprint = await getFingerprintFromFingerprintFragmentAsync(fingerprintFragment);
let platforms;
const fingerprintBuilds = fingerprintFragment.builds?.edges.map(edge => edge.node) ?? [];
const fingerprintUpdates = fingerprintFragment.updates?.edges.map(edge => edge.node) ?? [];
if (fingerprintBuilds.length > 0) {
platforms = [fingerprintBuilds[0].platform];
} else if (fingerprintUpdates.length > 0) {
platforms = [stringToAppPlatform(fingerprintUpdates[0].platform)];
}
return {
fingerprint,
platforms,
origin: {
type: FingerprintOriginType.Hash,
},
};
}
let buildId: string | null = buildIdFromArg ?? null;
if (!buildId) {
if (nonInteractive) {
throw new Error('Build ID must be provided in non-interactive mode');
}
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
buildId = await selectBuildToCompareAsync(graphqlClient, projectId, displayName, {
filters: { hasFingerprint: true },
});
if (!buildId) {
throw new Error('Must select build with fingerprint for comparison.');
}
}
Log.log(`Comparing fingerprints of the current project and build ${buildId}…`);
const buildWithFingerprint = await BuildQuery.withFingerprintByIdAsync(graphqlClient, buildId);
if (!buildWithFingerprint.fingerprint) {
throw new Error(`Fingerprint for build ${buildId} was not computed.`);
} else if (!buildWithFingerprint.fingerprint.debugInfoUrl) {
throw new Error(`Fingerprint source for build ${buildId} was not computed.`);
}
return {
fingerprint: await getFingerprintFromFingerprintFragmentAsync(buildWithFingerprint.fingerprint),
platforms: [buildWithFingerprint.platform],
origin: {
type: FingerprintOriginType.Build,
build: buildWithFingerprint,
},
};
}
async function getSecondFingerprintInfoAsync(
graphqlClient: ExpoGraphqlClient,
projectDir: string,
projectId: string,
vcsClient: Client,
firstFingerprintInfo: {
fingerprint: Fingerprint;
platforms?: AppPlatform[];
origin: FingerprintOrigin;
},
{ hash2 }: FingerprintCompareFlags
): Promise<{ fingerprint: Fingerprint; origin: FingerprintOrigin }> {
if (hash2) {
const fingerprintFragment = await getFingerprintFragmentFromHashAsync(
graphqlClient,
projectId,
hash2
);
if (!fingerprintFragment) {
throw new Error(`Fingerprint with hash ${hash2} was not uploaded.`);
}
return {
fingerprint: await getFingerprintFromFingerprintFragmentAsync(fingerprintFragment),
origin: { type: FingerprintOriginType.Hash },
};
}
const firstFingerprintPlatforms = firstFingerprintInfo.platforms;
if (!firstFingerprintPlatforms) {
throw new Error(
`Cannot compare the local directory against the provided fingerprint hash "${firstFingerprintInfo.fingerprint.hash}" because the associated platform could not be determined. Ensure the fingerprint is linked to a build or update to identify the platform.`
);
}
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
const optionsFromWorkflow = getFingerprintOptionsFromWorkflow(
firstFingerprintPlatforms,
workflows
);
const projectFingerprint = await createFingerprintAsync(projectDir, {
...optionsFromWorkflow,
platforms: firstFingerprintPlatforms.map(appPlatformToString),
debug: true,
env: undefined,
});
if (!projectFingerprint) {
throw new Error('Project fingerprints can only be computed for projects with SDK 52 or higher');
}
const uploadedFingerprint = await maybeUploadFingerprintAsync({
hash: projectFingerprint.hash,
fingerprint: {
fingerprintSources: projectFingerprint.sources,
isDebugFingerprintSource: Log.isDebug,
},
graphqlClient,
});
await FingerprintMutation.createFingerprintAsync(graphqlClient, projectId, {
hash: uploadedFingerprint.hash,
source: uploadedFingerprint.fingerprintSource,
});
return { fingerprint: projectFingerprint, origin: { type: FingerprintOriginType.Project } };
}
async function getFingerprintFragmentFromHashAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
hash: string
): Promise<FingerprintFragment> {
const fingerprint = await FingerprintQuery.byHashAsync(graphqlClient, {
appId: projectId,
hash,
});
if (!fingerprint) {
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
throw new Error(`Fingerprint with hash ${hash} was not uploaded for ${displayName}.`);
}
return fingerprint;
}
async function getFingerprintFromFingerprintFragmentAsync(
fingerprintFragment: FingerprintFragment
): Promise<Fingerprint> {
const fingerprintDebugUrl = fingerprintFragment.debugInfoUrl;
if (!fingerprintDebugUrl) {
throw new Error(
`The source for fingerprint hash ${fingerprintFragment.hash} was not computed.`
);
}
const fingerprintResponse = await fetch(fingerprintDebugUrl);
return (await fingerprintResponse.json()) as Fingerprint;
}
function getFingerprintOptionsFromWorkflow(
platforms: AppPlatform[],
workflowsByPlatform: Record<Platform, Workflow>
): { workflow?: Workflow; ignorePaths?: string[] } {
if (platforms.length === 0) {
throw new Error('Could not determine platform from fingerprint sources');
}
// Single platform case
if (platforms.length === 1) {
const platform = platforms[0];
return { workflow: workflowsByPlatform[appPlatformToPlatform(platform)] };
}
// Multiple platforms case
const workflows = platforms.map(platform => workflowsByPlatform[appPlatformToPlatform(platform)]);
// If all workflows are the same, return the common workflow
const [firstWorkflow, ...restWorkflows] = workflows;
if (restWorkflows.every(workflow => workflow === firstWorkflow)) {
return { workflow: firstWorkflow };
}
// Generate ignorePaths for mixed workflows
const ignorePaths = platforms
.filter(platform => workflowsByPlatform[appPlatformToPlatform(platform)] === Workflow.MANAGED)
.map(platform => `${appPlatformToString(platform)}/**/*`);
return { ignorePaths };
}
function printContentDiff(diff: FingerprintDiffItem): void {
if (diff.op === 'added') {
const sourceType = diff.addedSource.type;
@@ -350,6 +579,17 @@ function appPlatformToString(platform: AppPlatform): string {
}
}
function stringToAppPlatform(platform: string): AppPlatform {
switch (platform) {
case 'android':
return AppPlatform.Android;
case 'ios':
return AppPlatform.Ios;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
export async function selectBuildToCompareAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
@@ -285,12 +285,12 @@ export default class Onboarding extends EasCommand {
} else {
Log.log('🚀 Now we are going to trigger your first build');
Log.log();
const { buildIds } = await runBuildAndSubmitAsync(
const { buildIds } = await runBuildAndSubmitAsync({
graphqlClient,
analytics,
vcsClient,
finalTargetProjectDirectory,
{
projectDir: finalTargetProjectDirectory,
flags: {
nonInteractive: true,
requestedPlatform:
platform === Platform.ANDROID ? RequestedPlatform.Android : RequestedPlatform.Ios,
@@ -307,8 +307,9 @@ export default class Onboarding extends EasCommand {
repack: true,
},
actor,
getDynamicProjectConfigFn
);
// eslint-disable-next-line async-protect/async-suffix
getDynamicPrivateProjectConfigAsync: getDynamicProjectConfigFn,
});
const buildId = buildIds[0];
Log.log();
Log.log('🚀 You can now go back to the website to continue:');
@@ -317,7 +317,7 @@ export async function createAppAsync(
}
}
function isAppleError(error: any): error is {
export function isAppleError(error: any): error is {
data: {
errors: {
id: string;
@@ -0,0 +1,196 @@
import { App, BetaGroup, Session, User, UserRole } from '@expo/apple-utils';
import { isAppleError } from './ensureAppExists';
import Log from '../../../log';
import { ora } from '../../../ora';
import { confirmAsync } from '../../../prompts';
// The name of the internal TestFlight group, this should probably never change.
const AUTO_GROUP_NAME = 'Team (Expo)';
/**
* Ensure a TestFlight internal group with access to all builds exists for the app and has all admin users invited to it.
* This allows users to instantly access their builds from TestFlight after it finishes processing.
*/
export async function ensureTestFlightGroupExistsAsync(app: App): Promise<void> {
const group = await ensureInternalGroupAsync(app);
const users = await User.getAsync(app.context);
const admins = users.filter(user => user.attributes.roles?.includes(UserRole.ADMIN));
await addAllUsersToInternalGroupAsync(group, admins);
}
async function ensureInternalGroupAsync(app: App): Promise<BetaGroup> {
const groups = await app.getBetaGroupsAsync({
query: {
includes: ['betaTesters'],
},
});
let betaGroup = groups.find(group => group.attributes.name === AUTO_GROUP_NAME);
if (!betaGroup) {
const spinner = ora().start('Creating TestFlight group...');
try {
// Apple throw an error if you create the group too quickly after creating the app. We'll retry a few times.
await pollRetryAsync(
async () => {
betaGroup = await app.createBetaGroupAsync({
name: AUTO_GROUP_NAME,
publicLinkEnabled: false,
publicLinkLimitEnabled: false,
isInternalGroup: true,
// Automatically add latest builds to the group without needing to run the command.
hasAccessToAllBuilds: true,
});
},
{
shouldRetry(error) {
if (isAppleError(error)) {
spinner.text = `TestFlight not ready, retrying in 25 seconds...`;
return error.data.errors.some(
error => error.code === 'ENTITY_ERROR.RELATIONSHIP.INVALID'
);
}
return false;
},
}
);
spinner.succeed(`TestFlight group created: ${AUTO_GROUP_NAME}`);
} catch (error: any) {
spinner.fail('Failed to create TestFlight group...');
throw error;
}
}
if (!betaGroup) {
throw new Error('Failed to create internal TestFlight group');
}
// `hasAccessToAllBuilds` is a newer feature that allows the group to automatically have access to all builds. This cannot be patched so we need to recreate the group.
if (!betaGroup.attributes.hasAccessToAllBuilds) {
if (
await confirmAsync({
message: 'Regenerate internal TestFlight group to allow automatic access to all builds?',
})
) {
await BetaGroup.deleteAsync(app.context, { id: betaGroup.id });
return await ensureInternalGroupAsync(app);
}
}
return betaGroup;
}
async function addAllUsersToInternalGroupAsync(group: BetaGroup, users: User[]): Promise<void> {
let emails = users
.filter(user => user.attributes.email)
.map(user => ({
email: user.attributes.email!,
firstName: user.attributes.firstName ?? '',
lastName: user.attributes.lastName ?? '',
}));
const { betaTesters } = group.attributes;
const existingEmails = betaTesters?.map(tester => tester.attributes.email).filter(Boolean) ?? [];
// Filter out existing beta testers.
if (betaTesters) {
emails = emails.filter(
user => !existingEmails.find(existingEmail => existingEmail === user.email)
);
}
// No new users to add to the internal group.
if (!emails.length) {
// No need to log which users are here on subsequent runs as devs already know the drill at this point.
Log.debug(`All current admins are already added to the group: ${group.attributes.name}`);
return;
}
Log.debug(`Adding ${emails.length} users to internal group: ${group.attributes.name}`);
Log.debug(`Users: ${emails.map(user => user.email).join(', ')}`);
const data = await group.createBulkBetaTesterAssignmentsAsync(emails);
const success = data.attributes.betaTesters.every(tester => {
if (tester.assignmentResult === 'FAILED') {
if (tester.errors && Array.isArray(tester.errors) && tester.errors.length) {
if (
tester.errors.length === 1 &&
tester.errors[0].key === 'Halliday.tester.already.exists'
) {
return true;
}
for (const error of tester.errors) {
Log.error(
`Error adding user ${tester.email} to TestFlight group "${group.attributes.name}": ${error.key}`
);
}
}
return false;
}
if (tester.assignmentResult === 'NOT_QUALIFIED_FOR_INTERNAL_GROUP') {
return false;
}
return true;
});
if (!success) {
const groupUrl = await getTestFlightGroupUrlAsync(group);
Log.error(
`Unable to add all admins to TestFlight group "${
group.attributes.name
}". You can add them manually in App Store Connect. ${groupUrl ?? ''}`
);
} else {
Log.log(
`TestFlight access enabled for: ` +
data.attributes.betaTesters
.map(tester => tester.email)
.filter(Boolean)
.join(', ')
);
// TODO: When we have more TestFlight functionality, we can link to it from here.
}
}
async function getTestFlightGroupUrlAsync(group: BetaGroup): Promise<string | null> {
if (group.context.providerId) {
try {
const session = await Session.getSessionForProviderIdAsync(group.context.providerId);
return `https://appstoreconnect.apple.com/teams/${session.provider.publicProviderId}/apps/6741088859/testflight/groups/${group.id}`;
} catch (error) {
// Avoid crashing if we can't get the session.
Log.debug('Failed to get session for provider ID', error);
}
}
return null;
}
async function pollRetryAsync<T>(
fn: () => Promise<T>,
{
shouldRetry,
retries = 10,
// 25 seconds was the minium interval I calculated when measuring against 5 second intervals.
interval = 25000,
}: { shouldRetry?: (error: Error) => boolean; retries?: number; interval?: number } = {}
): Promise<T> {
let lastError: Error | null = null;
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error: any) {
if (shouldRetry && !shouldRetry(error)) {
throw error;
}
lastError = error;
await new Promise(resolve => setTimeout(resolve, interval));
}
}
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw lastError;
}
@@ -29,6 +29,7 @@ function resolveProfileType(
return resolveProfileTypeIos(profileClass, isEnterprise);
case ApplePlatform.TV_OS:
return resolveProfileTypeAppleTv(profileClass, isEnterprise);
case ApplePlatform.VISION_OS:
case ApplePlatform.MAC_OS:
throw new Error(`${applePlatform} profiles are not supported`);
}
+69 -7
View File
@@ -1090,6 +1090,7 @@ export type AndroidJobInput = {
cache?: InputMaybe<BuildCacheInput>;
customBuildConfig?: InputMaybe<CustomBuildConfigInput>;
developmentClient?: InputMaybe<Scalars['Boolean']['input']>;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
experimental?: InputMaybe<Scalars['JSONObject']['input']>;
gradleCommand?: InputMaybe<Scalars['String']['input']>;
loggerLevel?: InputMaybe<WorkerLoggerLevel>;
@@ -1601,6 +1602,7 @@ export type AppUpdatesArgs = {
export type AppUpdatesPaginatedArgs = {
after?: InputMaybe<Scalars['String']['input']>;
before?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<UpdateFilterInput>;
first?: InputMaybe<Scalars['Int']['input']>;
last?: InputMaybe<Scalars['Int']['input']>;
};
@@ -2902,7 +2904,7 @@ export type BuildArtifacts = {
applicationArchiveUrl?: Maybe<Scalars['String']['output']>;
buildArtifactsUrl?: Maybe<Scalars['String']['output']>;
buildUrl?: Maybe<Scalars['String']['output']>;
/** @deprecated Use 'runtime.fingerprintDebugInfoUrl' instead. */
/** @deprecated Use 'runtime.fingerprint.debugInfoUrl' instead. */
fingerprintUrl?: Maybe<Scalars['String']['output']>;
xcodeBuildLogsUrl?: Maybe<Scalars['String']['output']>;
};
@@ -2933,6 +2935,7 @@ export type BuildFilter = {
appVersion?: InputMaybe<Scalars['String']['input']>;
buildProfile?: InputMaybe<Scalars['String']['input']>;
channel?: InputMaybe<Scalars['String']['input']>;
developmentClient?: InputMaybe<Scalars['Boolean']['input']>;
distribution?: InputMaybe<DistributionType>;
fingerprintHash?: InputMaybe<Scalars['String']['input']>;
gitCommitHash?: InputMaybe<Scalars['String']['input']>;
@@ -4092,6 +4095,8 @@ export type EnvironmentVariableMutation = {
createEnvironmentVariableForAccount: EnvironmentVariable;
/** Create an environment variable for an App */
createEnvironmentVariableForApp: EnvironmentVariable;
/** Bulk delete environment variables */
deleteBulkEnvironmentVariables: Array<DeleteEnvironmentVariableResult>;
/** Delete an environment variable */
deleteEnvironmentVariable: DeleteEnvironmentVariableResult;
/** Bulk link shared environment variables */
@@ -4100,6 +4105,8 @@ export type EnvironmentVariableMutation = {
linkSharedEnvironmentVariable: EnvironmentVariable;
/** Unlink shared environment variable */
unlinkSharedEnvironmentVariable: EnvironmentVariable;
/** Bulk update environment variables */
updateBulkEnvironmentVariables: Array<EnvironmentVariable>;
/** Update an environment variable */
updateEnvironmentVariable: EnvironmentVariable;
};
@@ -4129,6 +4136,11 @@ export type EnvironmentVariableMutationCreateEnvironmentVariableForAppArgs = {
};
export type EnvironmentVariableMutationDeleteBulkEnvironmentVariablesArgs = {
ids: Array<Scalars['ID']['input']>;
};
export type EnvironmentVariableMutationDeleteEnvironmentVariableArgs = {
id: Scalars['ID']['input'];
};
@@ -4153,6 +4165,11 @@ export type EnvironmentVariableMutationUnlinkSharedEnvironmentVariableArgs = {
};
export type EnvironmentVariableMutationUpdateBulkEnvironmentVariablesArgs = {
environmentVariablesData: Array<UpdateEnvironmentVariableInput>;
};
export type EnvironmentVariableMutationUpdateEnvironmentVariableArgs = {
environmentVariableData: UpdateEnvironmentVariableInput;
};
@@ -4384,6 +4401,7 @@ export enum GitHubAppEnvironment {
export type GitHubAppInstallation = {
__typename?: 'GitHubAppInstallation';
/** The Expo account that owns the installation entity. */
account: Account;
actor?: Maybe<Actor>;
id: Scalars['ID']['output'];
@@ -4403,10 +4421,17 @@ export type GitHubAppInstallationAccessibleRepository = {
url: Scalars['String']['output'];
};
export enum GitHubAppInstallationAccountType {
Organization = 'ORGANIZATION',
User = 'USER'
}
export type GitHubAppInstallationMetadata = {
__typename?: 'GitHubAppInstallationMetadata';
githubAccountAvatarUrl?: Maybe<Scalars['String']['output']>;
/** The login of the GitHub account that owns the installation. Not the display name. */
githubAccountName?: Maybe<Scalars['String']['output']>;
githubAccountType?: Maybe<GitHubAppInstallationAccountType>;
installationStatus: GitHubAppInstallationStatus;
};
@@ -5050,6 +5075,7 @@ export type IosJobInput = {
developmentClient?: InputMaybe<Scalars['Boolean']['input']>;
/** @deprecated */
distribution?: InputMaybe<DistributionType>;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
experimental?: InputMaybe<Scalars['JSONObject']['input']>;
loggerLevel?: InputMaybe<WorkerLoggerLevel>;
mode?: InputMaybe<BuildMode>;
@@ -5142,6 +5168,7 @@ export type IosSubmissionConfigInput = {
export type JobRun = {
__typename?: 'JobRun';
app: App;
artifacts: Array<WorkflowArtifact>;
/** @deprecated No longer supported */
childJobRun?: Maybe<JobRun>;
createdAt: Scalars['DateTime']['output'];
@@ -5744,7 +5771,6 @@ export type PublishUpdateGroupInput = {
message?: InputMaybe<Scalars['String']['input']>;
rollBackToEmbeddedInfoGroup?: InputMaybe<UpdateRollBackToEmbeddedGroup>;
rolloutInfoGroup?: InputMaybe<UpdateRolloutInfoGroup>;
runtimeFingerprintSource?: InputMaybe<FingerprintSourceInput>;
runtimeVersion: Scalars['String']['input'];
turtleJobRunId?: InputMaybe<Scalars['String']['input']>;
updateInfoGroup?: InputMaybe<UpdateInfoGroup>;
@@ -6182,7 +6208,7 @@ export type Runtime = {
builds: AppBuildsConnection;
createdAt: Scalars['DateTime']['output'];
deployments: DeploymentsConnection;
fingerprintDebugInfoUrl?: Maybe<Scalars['String']['output']>;
fingerprint?: Maybe<Fingerprint>;
firstBuildCreatedAt?: Maybe<Scalars['DateTime']['output']>;
id: Scalars['ID']['output'];
updatedAt: Scalars['DateTime']['output'];
@@ -6984,6 +7010,12 @@ export type UpdateEnvironmentVariableInput = {
visibility?: InputMaybe<EnvironmentVariableVisibility>;
};
export type UpdateFilterInput = {
fingerprintHash?: InputMaybe<Scalars['String']['input']>;
hasFingerprint?: InputMaybe<Scalars['Boolean']['input']>;
runtimeVersion?: InputMaybe<Scalars['String']['input']>;
};
export type UpdateGitHubBuildTriggerInput = {
autoSubmit: Scalars['Boolean']['input'];
buildProfile: Scalars['String']['input'];
@@ -8299,6 +8331,19 @@ export type WorkflowRunsPaginatedArgs = {
last?: InputMaybe<Scalars['Int']['input']>;
};
export type WorkflowArtifact = {
__typename?: 'WorkflowArtifact';
contentType?: Maybe<Scalars['String']['output']>;
createdAt: Scalars['DateTime']['output'];
downloadUrl?: Maybe<Scalars['String']['output']>;
fileSizeBytes?: Maybe<Scalars['Int']['output']>;
filename: Scalars['String']['output'];
id: Scalars['ID']['output'];
jobRun: JobRun;
name: Scalars['String']['output'];
updatedAt: Scalars['DateTime']['output'];
};
export type WorkflowJob = {
__typename?: 'WorkflowJob';
createdAt: Scalars['DateTime']['output'];
@@ -8499,6 +8544,7 @@ export type WorkflowRunMutationCreateWorkflowRunArgs = {
export type WorkflowRunMutationRetryWorkflowRunArgs = {
fromFailedJobs?: InputMaybe<Scalars['Boolean']['input']>;
workflowRunId: Scalars['ID']['input'];
};
@@ -8524,6 +8570,10 @@ export enum WorkflowRunStatus {
export enum WorkflowRunTriggerEventType {
Github = 'GITHUB',
GithubPullRequestOpened = 'GITHUB_PULL_REQUEST_OPENED',
GithubPullRequestReopened = 'GITHUB_PULL_REQUEST_REOPENED',
GithubPullRequestSynchronize = 'GITHUB_PULL_REQUEST_SYNCHRONIZE',
GithubPush = 'GITHUB_PUSH',
Manual = 'MANUAL'
}
@@ -9195,7 +9245,7 @@ export type CreateFingeprintMutationVariables = Exact<{
}>;
export type CreateFingeprintMutation = { __typename?: 'RootMutation', fingerprint: { __typename?: 'FingerprintMutation', createOrGetExistingFingerprint: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null } } };
export type CreateFingeprintMutation = { __typename?: 'RootMutation', fingerprint: { __typename?: 'FingerprintMutation', createOrGetExistingFingerprint: { __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 } }> } } } };
export type CreateKeystoreGenerationUrlMutationVariables = Exact<{ [key: string]: never; }>;
@@ -9411,7 +9461,7 @@ export type BuildsWithFingerprintByIdQueryVariables = Exact<{
}>;
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 } | 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 } } } };
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 } } } };
export type ViewBuildsOnAppQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -9499,6 +9549,18 @@ export type EnvironmentVariablesSharedWithSensitiveQueryVariables = Exact<{
export type EnvironmentVariablesSharedWithSensitiveQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentVariablesIncludingSensitive: Array<{ __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility: EnvironmentVariableVisibility, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } } };
export type FingerprintsByAppIdQueryVariables = Exact<{
appId: Scalars['String']['input'];
after?: InputMaybe<Scalars['String']['input']>;
first?: InputMaybe<Scalars['Int']['input']>;
before?: InputMaybe<Scalars['String']['input']>;
last?: InputMaybe<Scalars['Int']['input']>;
fingerprintFilter?: InputMaybe<FingerprintFilterInput>;
}>;
export type FingerprintsByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, fingerprintsPaginated: { __typename?: 'AppFingerprintsConnection', edges: Array<{ __typename?: 'AppFingerprintEdge', node: { __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 } }> } } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null, endCursor?: string | null } } } } };
export type GoogleServiceAccountKeyByIdQueryVariables = Exact<{
ascApiKeyId: Scalars['ID']['input'];
}>;
@@ -9614,7 +9676,7 @@ export type BuildFragment = { __typename?: 'Build', id: string, status: BuildSta
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: SubmissionAndroidTrack, 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, 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 } };
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 } | 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 } };
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 } };
export type EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any };
@@ -9622,7 +9684,7 @@ export type EnvironmentVariableFragment = { __typename?: 'EnvironmentVariable',
export type EnvironmentVariableWithSecretFragment = { __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility: EnvironmentVariableVisibility, type: EnvironmentSecretType };
export type FingerprintFragment = { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null };
export type FingerprintFragment = { __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 } }> } };
export type RuntimeFragment = { __typename?: 'Runtime', id: string, version: string };
@@ -0,0 +1,98 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
FingerprintFilterInput,
FingerprintFragment,
FingerprintsByAppIdQuery,
} from '../generated';
import { FingerprintFragmentNode } from '../types/Fingerprint';
export const FingerprintQuery = {
async byHashAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
hash,
}: {
appId: string;
hash: string;
}
): Promise<FingerprintFragment | null> {
const fingerprintConnection = await FingerprintQuery.getFingerprintsAsync(graphqlClient, {
appId,
fingerprintFilter: { hashes: [hash] },
first: 1,
});
const fingerprints = fingerprintConnection.edges.map(edge => edge.node);
return fingerprints[0] ?? null;
},
async getFingerprintsAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
first,
after,
last,
before,
fingerprintFilter,
}: {
appId: string;
first?: number;
after?: string;
last?: number;
before?: string;
fingerprintFilter?: FingerprintFilterInput;
}
): Promise<FingerprintsByAppIdQuery['app']['byId']['fingerprintsPaginated']> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<FingerprintsByAppIdQuery>(
gql`
query FingerprintsByAppId(
$appId: String!
$after: String
$first: Int
$before: String
$last: Int
$fingerprintFilter: FingerprintFilterInput
) {
app {
byId(appId: $appId) {
id
fingerprintsPaginated(
after: $after
first: $first
before: $before
last: $last
filter: $fingerprintFilter
) {
edges {
node {
id
...FingerprintFragment
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
}
${print(FingerprintFragmentNode)}
`,
{ appId, after, first, before, last, fingerprintFilter },
{ additionalTypenames: ['Fingerprint'] }
)
.toPromise()
);
return data.app?.byId.fingerprintsPaginated;
},
};
@@ -5,5 +5,23 @@ export const FingerprintFragmentNode = gql`
id
hash
debugInfoUrl
builds(first: 1) {
edges {
node {
id
... on Build {
platform
}
}
}
}
updates(first: 1) {
edges {
node {
id
platform
}
}
}
}
`;
@@ -0,0 +1,234 @@
import { getConfig } from '@expo/config';
import { vol } from 'memfs';
import * as prompts from '../../../prompts';
import { ensureNonExemptEncryptionIsDefinedForManagedProjectAsync } from '../exemptEncryption';
jest.mock('fs');
jest.mock('../../../prompts');
beforeEach(async () => {
jest.resetAllMocks();
vol.reset();
});
test('prompts non-exempt encryption is not used', async () => {
const projectRoot = '/project';
vol.fromJSON(
{
'package.json': JSON.stringify({}),
'app.json': JSON.stringify({
expo: {
name: 'myproject',
slug: 'myproject',
},
}),
},
projectRoot
);
jest.mocked(prompts.confirmAsync).mockReset().mockResolvedValueOnce(true);
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync({
projectDir: projectRoot,
exp: getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp,
nonInteractive: false,
});
expect(getConfig(projectRoot, { skipSDKVersionRequirement: true }).rootConfig).toEqual({
expo: {
ios: {
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
},
},
name: 'myproject',
slug: 'myproject',
},
});
});
test('does not prompt if the value is already set', async () => {
const projectRoot = '/project';
vol.fromJSON(
{
'package.json': JSON.stringify({}),
'app.json': JSON.stringify({
name: 'myproject',
slug: 'myproject',
ios: {
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
},
},
}),
},
projectRoot
);
// Reset but no mock values.
jest.mocked(prompts.confirmAsync).mockReset();
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync({
projectDir: projectRoot,
exp: getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp,
nonInteractive: false,
});
expect(prompts.confirmAsync).toHaveBeenCalledTimes(0);
expect(getConfig(projectRoot, { skipSDKVersionRequirement: true }).rootConfig).toEqual({
ios: {
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
},
},
name: 'myproject',
slug: 'myproject',
});
});
test('prompts non-exempt encryption in CI', async () => {
const projectRoot = '/project';
vol.fromJSON(
{
'package.json': JSON.stringify({}),
'app.json': JSON.stringify({
expo: {
name: 'myproject',
slug: 'myproject',
},
}),
},
projectRoot
);
// Reset but no mock values.
jest.mocked(prompts.confirmAsync).mockReset();
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync({
projectDir: projectRoot,
exp: getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp,
nonInteractive: false,
});
expect(getConfig(projectRoot, { skipSDKVersionRequirement: true }).rootConfig).toEqual({
expo: {
ios: {
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
},
},
name: 'myproject',
slug: 'myproject',
},
});
});
test('prompts non-exempt encryption is not used but app.config.js', async () => {
const projectRoot = '/project';
vol.fromJSON(
{
'package.json': JSON.stringify({}),
'app.config.js':
`module.exports = ` +
JSON.stringify({
expo: {
name: 'myproject',
slug: 'myproject',
},
}),
},
projectRoot
);
jest.mocked(prompts.confirmAsync).mockReset().mockResolvedValueOnce(true);
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync({
projectDir: projectRoot,
exp: getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp,
nonInteractive: false,
});
// Not set.
expect(getConfig(projectRoot, { skipSDKVersionRequirement: true }).rootConfig).toEqual({});
});
test('prompts non-exempt encryption is used', async () => {
const projectRoot = '/project';
vol.fromJSON(
{
'package.json': JSON.stringify({}),
'app.json': JSON.stringify({
expo: {
name: 'myproject',
slug: 'myproject',
},
}),
},
projectRoot
);
jest
.mocked(prompts.confirmAsync)
.mockReset()
.mockResolvedValueOnce(false)
// Are you sure?
.mockResolvedValueOnce(true);
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync({
projectDir: projectRoot,
exp: getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp,
nonInteractive: false,
});
expect(prompts.confirmAsync).toHaveBeenCalledTimes(2);
expect(getConfig(projectRoot, { skipSDKVersionRequirement: true }).rootConfig).toEqual({
expo: {
name: 'myproject',
slug: 'myproject',
},
});
});
test('prompts non-exempt encryption is used but backed out', async () => {
const projectRoot = '/project';
vol.fromJSON(
{
'package.json': JSON.stringify({}),
'app.json': JSON.stringify({
expo: {
name: 'myproject',
slug: 'myproject',
},
}),
},
projectRoot
);
jest
.mocked(prompts.confirmAsync)
.mockReset()
.mockResolvedValueOnce(false)
// Are you sure?
.mockResolvedValueOnce(false);
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync({
projectDir: projectRoot,
exp: getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp,
nonInteractive: false,
});
expect(getConfig(projectRoot, { skipSDKVersionRequirement: true }).rootConfig).toEqual({
expo: {
name: 'myproject',
slug: 'myproject',
ios: {
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
},
},
},
});
});
@@ -29,7 +29,7 @@ describe(getApplePlatformFromSdkRoot, () => {
// Update `getApplePlatformFromSdkRoot` to be compatible with new Apple Platforms
test('all enumerations work with the function', () => {
expect(Object.values(ApplePlatform).sort()).toEqual(
[ApplePlatform.IOS, ApplePlatform.TV_OS, ApplePlatform.MAC_OS].sort()
[ApplePlatform.IOS, ApplePlatform.TV_OS, ApplePlatform.MAC_OS, ApplePlatform.VISION_OS].sort()
);
});
test('existing SDKs work with the function', () => {
@@ -0,0 +1,117 @@
import { ExpoConfig, getProjectConfigDescription, modifyConfigAsync } from '@expo/config';
import chalk from 'chalk';
import Log, { learnMore } from '../../log';
import { confirmAsync } from '../../prompts';
/** Non-exempt encryption must be set on every build in App Store Connect, we move it to before the build process to attempt only setting it once for the entire life-cycle of the project. */
export async function ensureNonExemptEncryptionIsDefinedForManagedProjectAsync({
projectDir,
exp,
nonInteractive,
}: {
projectDir: string;
exp: ExpoConfig;
nonInteractive: boolean;
}): Promise<void> {
// TODO: We could add bare workflow support in the future.
// TODO: We could add wizard support for non-exempt encryption in the future.
if (exp.ios?.infoPlist?.ITSAppUsesNonExemptEncryption == null) {
await configureNonExemptEncryptionAsync({
projectDir,
exp,
nonInteractive,
});
} else {
Log.debug(`ITSAppUsesNonExemptEncryption is defined in the app config.`);
}
}
async function configureNonExemptEncryptionAsync({
projectDir,
exp,
nonInteractive,
}: {
projectDir: string;
exp: ExpoConfig;
nonInteractive: boolean;
}): Promise<void> {
const description = getProjectConfigDescription(projectDir);
if (nonInteractive) {
Log.warn(
chalk`${description} is missing {bold ios.infoPlist.ITSAppUsesNonExemptEncryption} boolean. Manual configuration is required in App Store Connect before the app can be tested.`
);
return;
}
let onlyExemptEncryption = await confirmAsync({
message: `iOS app only uses standard/exempt encryption? ${chalk.dim(
learnMore(
'https://developer.apple.com/documentation/Security/complying-with-encryption-export-regulations'
)
)}`,
initial: true,
});
if (!onlyExemptEncryption) {
const confirm = await confirmAsync({
message: `Are you sure your app uses non-exempt encryption? Selecting 'Yes' will require annual self-classification reports for the US government.`,
initial: true,
});
if (!confirm) {
Log.warn(
chalk`Set {bold ios.infoPlist.ITSAppUsesNonExemptEncryption} in ${description} to release Apple builds faster.`
);
onlyExemptEncryption = true;
}
}
const ITSAppUsesNonExemptEncryption = !onlyExemptEncryption;
// Only set this value if the answer is no, this enables developers to see the more in-depth prompt in App Store Connect. They can set the value manually in the app.json to avoid the EAS prompt in subsequent builds.
if (ITSAppUsesNonExemptEncryption) {
Log.warn(
`You'll need to manually configure the encryption status in App Store Connect before your build can be tested.`
);
return;
}
// NOTE: Is is it possible to assert that the config needs to be modifiable before building the app?
const modification = await modifyConfigAsync(
projectDir,
{
ios: {
...(exp.ios ?? {}),
infoPlist: {
...(exp.ios?.infoPlist ?? {}),
ITSAppUsesNonExemptEncryption,
},
},
},
{
skipSDKVersionRequirement: true,
}
);
if (modification.type !== 'success') {
Log.log();
if (modification.type === 'warn') {
// The project is using a dynamic config, give the user a helpful log and bail out.
Log.log(chalk.yellow(modification.message));
}
const edits = {
ios: {
infoPlist: {
ITSAppUsesNonExemptEncryption,
},
},
};
Log.log(chalk.cyan(`Add the following to ${description}:`));
Log.log();
Log.log(JSON.stringify(edits, null, 2));
Log.log();
}
}
@@ -7,6 +7,7 @@ import {
ensureAppExistsAsync,
ensureBundleIdExistsWithNameAsync,
} from '../../credentials/ios/appstore/ensureAppExists';
import { ensureTestFlightGroupExistsAsync } from '../../credentials/ios/appstore/ensureTestFlightGroup';
import Log from '../../log';
import { getBundleIdentifierAsync } from '../../project/ios/bundleIdentifier';
import { promptAsync } from '../../prompts';
@@ -92,6 +93,16 @@ async function createAppStoreConnectAppAsync(
sku,
});
try {
await ensureTestFlightGroupExistsAsync(app);
} catch (error: any) {
// This process is not critical to the app submission so we shouldn't let it fail the entire process.
Log.error(
'Failed to create an internal TestFlight group. This can be done manually in the App Store Connect website.'
);
Log.error(error);
}
return {
ascAppIdentifier: app.id,
};
+13 -4
View File
@@ -7,11 +7,12 @@ import Log from '../log';
import { ora } from '../ora';
export type FingerprintOptions = {
workflow: Workflow;
workflow?: Workflow;
platforms: string[];
debug?: boolean;
env: Env | undefined;
cwd?: string;
ignorePaths?: string[];
};
export function diffFingerprint(
@@ -85,12 +86,20 @@ async function createFingerprintWithoutLoggingAsync(
> {
const Fingerprint = require(fingerprintPath);
const fingerprintOptions: Record<string, any> = {};
const ignorePaths = [];
if (options.workflow === Workflow.MANAGED) {
ignorePaths.push('android/**/*');
ignorePaths.push('ios/**/*');
}
if (options.ignorePaths) {
ignorePaths.push(...options.ignorePaths);
}
if (ignorePaths.length > 0) {
fingerprintOptions.ignorePaths = ignorePaths;
}
if (options.platforms) {
fingerprintOptions.platforms = [...options.platforms];
}
if (options.workflow === Workflow.MANAGED) {
fingerprintOptions.ignorePaths = ['android/**/*', 'ios/**/*'];
}
if (options.debug) {
fingerprintOptions.debug = true;
}
Generated
+7 -32
View File
@@ -1359,10 +1359,10 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b"
integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==
"@expo/[email protected].5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@expo/apple-utils/-/apple-utils-2.1.5.tgz#a947697f10a7ab0f07f2d2fd075efdae9825e85e"
integrity sha512-Yy4BWVoViYePwQk8rqvhosolS5MTTMgcsl9Aq3rzJrd1BH8C1V/h6N5xelqo+/V+ww2k8Bom7UTE1NlGcKHxRw==
"@expo/[email protected].6":
version "2.1.6"
resolved "https://registry.yarnpkg.com/@expo/apple-utils/-/apple-utils-2.1.6.tgz#293fafccffd9afa24c9992963d74052558d5c5a1"
integrity sha512-6UkfLL/1AIRa082rlh26WG2K5Hzd67c8CIdNlHvrFmMEHlGl1PcVFZoUnNRFEROeTmrXW5/Sv4gJfmLXLTq3cQ==
"@expo/bunyan@^4.0.0":
version "4.0.0"
@@ -13233,16 +13233,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==
@@ -13398,7 +13389,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==
@@ -13419,13 +13410,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"
@@ -14632,16 +14616,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==