Compare commits

..
120 changed files with 1430 additions and 2299 deletions

No files matched your search

-25
View File
@@ -6,21 +6,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
### 🎉 New features
### 🐛 Bug fixes
- Make Apple Platform detection more robust. ([#1417](https://github.com/expo/eas-cli/pull/1417) by [@quinlanj](https://github.com/quinlanj))
- Fix creating EAS secret from file. ([#1423](https://github.com/expo/eas-cli/pull/1423) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
## [2.2.0](https://github.com/expo/eas-cli/releases/tag/v2.2.0) - 2022-10-04
### 🛠 Breaking changes
- Prompt before configuring EAS project. ([#1356](https://github.com/expo/eas-cli/pull/1356) by [@wschurman](https://github.com/wschurman))
- Adds paginated support to device commands. Removes the ability to delete multiple devices at once. ([#1381](https://github.com/expo/eas-cli/pull/1381) by [@kgc00](https://github.com/kgc00))
### 🎉 New features
@@ -32,25 +18,14 @@ This is the log of notable changes to EAS CLI and related packages.
- Add pagination to build commands. ([#1353](https://github.com/expo/eas-cli/pull/1353) by [@kgc00](https://github.com/kgc00))
- Provide suggestions for developers when archive size is large. ([#1363](https://github.com/expo/eas-cli/pull/1363) by [@szdziedzic](https://github.com/szdziedzic))
- Improve `eas init` command. ([#1376](https://github.com/expo/eas-cli/pull/1376) by [@wschurman](https://github.com/wschurman))
- Warn about outdated channel configuration before build when using eas update. ([#1397](https://github.com/expo/eas-cli/pull/1397) by [@kbrandwijk](https://github.com/kbrandwijk))
- Validate that owner and projectId and slug all are in alignment. ([#1405](https://github.com/expo/eas-cli/pull/1405) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
- Fix automatic eas.json generation when running `eas build`. ([#1415](https://github.com/expo/eas-cli/pull/1415) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Standardize the GQL 'Update' schema using UpdateFragmentNode. ([#1348](https://github.com/expo/eas-cli/pull/1348) by [@kgc00](https://github.com/kgc00))
- Remove 'owner' app config field dependency. ([#1368](https://github.com/expo/eas-cli/pull/1368), [#1369](https://github.com/expo/eas-cli/pull/1369), [#1371](https://github.com/expo/eas-cli/pull/1371), [#1372](https://github.com/expo/eas-cli/pull/1372), [#1373](https://github.com/expo/eas-cli/pull/1373), by [@wschurman](https://github.com/wschurman))
- Add Xcode 14 image. ([#1365](https://github.com/expo/eas-cli/pull/1365) by [@szdziedzic](https://github.com/szdziedzic))
- Update max `versionCode` for Android. ([#1400](https://github.com/expo/eas-cli/pull/1400) by [@wkozyra95](https://github.com/wkozyra95))
- Add and use command context to declare command dependencies. ([#1383](https://github.com/expo/eas-cli/pull/1383), [#1384](https://github.com/expo/eas-cli/pull/1384), [#1387](https://github.com/expo/eas-cli/pull/1387), [#1388](https://github.com/expo/eas-cli/pull/1388), [#1390](https://github.com/expo/eas-cli/pull/1390), [#1391](https://github.com/expo/eas-cli/pull/1391), [#1394](https://github.com/expo/eas-cli/pull/1394), [#1402](https://github.com/expo/eas-cli/pull/1402), [#1403](https://github.com/expo/eas-cli/pull/1403), by [@wschurman](https://github.com/wschurman))
- Fix typo in the ad hoc build message. ([#1407](https://github.com/expo/eas-cli/pull/1407) by [@Simek](https://github.com/Simek))
- Improve errors and error messages formatting related to **eas.json**. ([#1414](https://github.com/expo/eas-cli/pull/1414) by [@Simek](https://github.com/Simek))
- Handle errors from platform-specific kill switches to disable free tier builds. ([#1401](https://github.com/expo/eas-cli/pull/1401) by [@szdziedzic](https://github.com/szdziedzic))
- Surface invalid eas.json errors in an optional project context. ([#1418](https://github.com/expo/eas-cli/pull/1418) by [@quinlanj](https://github.com/quinlanj))
- Improve message when project ID doesn't match either slug or owner. ([#1420](https://github.com/expo/eas-cli/pull/1420) by [@brentvatne](https://github.com/brentvatne))
## [2.1.0](https://github.com/expo/eas-cli/releases/tag/v2.1.0) - 2022-09-05
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "2.2.1"
"version": "2.1.0"
}
+133 -179
View File
@@ -125,7 +125,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -142,7 +142,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -159,7 +159,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -173,7 +173,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -210,20 +210,19 @@ create a branch
```
USAGE
$ eas branch:create [NAME] [--json --non-interactive]
$ eas branch:create [NAME] [--json]
ARGUMENTS
NAME Name of the branch to create
FLAGS
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--json return a json with the new branch ID and name.
DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -231,20 +230,19 @@ delete a branch
```
USAGE
$ eas branch:delete [NAME] [--json --non-interactive]
$ eas branch:delete [NAME] [--json]
ARGUMENTS
NAME Name of the branch to delete
FLAGS
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--json return JSON with the edited branch's ID and name.
DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -252,11 +250,12 @@ list all branches
```
USAGE
$ eas branch:list [--offset <value>] [--limit <value>] [--json --non-interactive]
$ eas branch:list [--offset <value>] [--limit <value>] [--json] [--non-interactive]
FLAGS
--json Enable JSON output, non-JSON messages will be printed to stderr.
--limit=<value> The number of items to fetch each query. Defaults to 50 and is capped at 100.
--limit=<value> The number of query items to list at once. The default value is 50 (the maximum is 100). Using a
lower value may help increase command speed.
--non-interactive Run the command in non-interactive mode.
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
@@ -264,7 +263,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -272,19 +271,18 @@ rename a branch
```
USAGE
$ eas branch:rename [--from <value>] [--to <value>] [--json --non-interactive]
$ eas branch:rename [--from <value>] [--to <value>] [--json]
FLAGS
--from=<value> current name of the branch.
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--to=<value> new name of the branch.
--from=<value> current name of the branch.
--json return a json with the edited branch's ID and name.
--to=<value> new name of the branch.
DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -292,14 +290,15 @@ view a branch
```
USAGE
$ eas branch:view [NAME] [--offset <value>] [--limit <value>] [--json --non-interactive]
$ eas branch:view [NAME] [--offset <value>] [--limit <value>] [--json] [--non-interactive]
ARGUMENTS
NAME Name of the branch to view
FLAGS
--json Enable JSON output, non-JSON messages will be printed to stderr.
--limit=<value> The number of items to fetch each query. Defaults to 25 and is capped at 50.
--limit=<value> The number of query items to list at once. The default value is 50 (the maximum is 100). Using a
lower value may help increase command speed.
--non-interactive Run the command in non-interactive mode.
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
@@ -307,7 +306,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -315,29 +314,29 @@ 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>] [--json --non-interactive]
$ eas build [-p android|ios|all] [--json] [--profile <value>] [--non-interactive] [--local] [--output
<value>] [--wait] [--clear-cache] [--auto-submit | --auto-submit-with-profile <value>] [-m <value>]
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 same name as the
build profile
--auto-submit-with-profile=PROFILE_NAME Submit on build complete using the submit profile with provided name
--clear-cache Clear cache before the build
--json Enable JSON output, non-JSON messages will be printed to stderr.
--json Enable JSON output, non-JSON messages will be printed to stderr
--local Run build locally [experimental]
--non-interactive Run the command in non-interactive mode.
--non-interactive Run command in non-interactive mode
--output=<value> Output path for local build
--profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined
in eas.json.
--[no-]wait Wait for build(s) to complete
DESCRIPTION
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -345,16 +344,13 @@ cancel a build
```
USAGE
$ eas build:cancel [BUILD_ID] [--non-interactive]
FLAGS
--non-interactive Run the command in non-interactive mode.
$ eas build:cancel [BUILD_ID]
DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -371,7 +367,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:inspect`
@@ -379,12 +375,9 @@ inspect the state of the project at specific build stages, useful for troublesho
```
USAGE
$ eas build:inspect -p android|ios -s archive|pre-build|post-build -o <value> [-e <value>] [--force] [-v]
$ eas build:inspect -p android|ios -s archive|pre-build|post-build -o <value> [--profile <value>] [--force] [-v]
FLAGS
-e, --profile=PROFILE_NAME
Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.
-o, --output=OUTPUT_DIRECTORY
(required) Output directory.
@@ -402,11 +395,14 @@ FLAGS
--force
Delete OUTPUT_DIRECTORY if it already exists.
--profile=PROFILE_NAME
Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.
DESCRIPTION
inspect the state of the project at specific build stages, useful for troubleshooting
```
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -414,10 +410,10 @@ list all builds for your project
```
USAGE
$ eas build:list [--platform all|android|ios] [--status new|in-queue|in-progress|errored|finished|canceled]
[--distribution store|internal|simulator] [--channel <value>] [--appVersion <value>] [--appBuildVersion <value>]
[--sdkVersion <value>] [--runtimeVersion <value>] [--appIdentifier <value>] [--buildProfile <value>]
[--gitCommitHash <value>] [--offset <value>] [--limit <value>] [--json --non-interactive]
$ eas build:list [--platform all|android|ios] [--json] [--status
new|in-queue|in-progress|errored|finished|canceled] [--distribution store|internal|simulator] [--channel <value>]
[--appVersion <value>] [--appBuildVersion <value>] [--sdkVersion <value>] [--runtimeVersion <value>]
[--appIdentifier <value>] [--buildProfile <value>] [--gitCommitHash <value>] [--limit <value>]
FLAGS
--appBuildVersion=<value>
@@ -428,12 +424,8 @@ FLAGS
--distribution=(store|internal|simulator)
--gitCommitHash=<value>
--json Enable JSON output, non-JSON messages will be printed
to stderr.
--limit=<value> The number of items to fetch each query. Defaults to 10
and is capped at 50.
--non-interactive Run the command in non-interactive mode.
--offset=<value> Start queries from specified index. Use for paginating
results. Defaults to 0.
to stderr
--limit=<value>
--platform=(all|android|ios)
--runtimeVersion=<value>
--sdkVersion=<value>
@@ -443,7 +435,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:submit`
@@ -451,17 +443,17 @@ submit app binary to App Store and/or Play Store
```
USAGE
$ eas build:submit [-p android|ios|all] [-e <value>] [--latest | --id <value> | --path <value> | --url <value>]
[--verbose] [--wait] [--non-interactive]
$ eas build:submit [-p android|ios|all] [--profile <value>] [--latest | --id <value> | --path <value> | --url
<value>] [--verbose] [--wait] [--non-interactive]
FLAGS
-e, --profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in
eas.json.
-p, --platform=(android|ios|all)
--id=<value> ID of the build to submit
--latest Submit the latest build for specified platform
--non-interactive Run command in non-interactive mode
--path=<value> Path to the .apk/.aab/.ipa file
--profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in
eas.json.
--url=<value> App archive url
--verbose Always print logs from Submission Service
--[no-]wait Wait for submission to complete
@@ -479,18 +471,18 @@ Update version of an app.
```
USAGE
$ eas build:version:set [-p android|ios] [-e <value>]
$ eas build:version:set [-p android|ios] [--profile <value>]
FLAGS
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined in
eas.json.
-p, --platform=(android|ios)
--profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined in
eas.json.
DESCRIPTION
Update version of an app.
```
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/build/version/set.ts)_
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -498,18 +490,18 @@ Update a version in native code with a value stored on EAS servers
```
USAGE
$ eas build:version:sync [-p android|ios|all] [-e <value>]
$ eas build:version:sync [-p android|ios|all] [--profile <value>]
FLAGS
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined in
eas.json.
-p, --platform=(android|ios|all)
--profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined in
eas.json.
DESCRIPTION
Update a version in native code with a value stored on EAS servers
```
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/build/version/sync.ts)_
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -520,13 +512,13 @@ USAGE
$ eas build:view [BUILD_ID] [--json]
FLAGS
--json Enable JSON output, non-JSON messages will be printed to stderr.
--json Enable JSON output, non-JSON messages will be printed to stderr
DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -534,20 +526,19 @@ create a channel
```
USAGE
$ eas channel:create [NAME] [--json --non-interactive]
$ eas channel:create [NAME] [--json]
ARGUMENTS
NAME Name of the channel to create
FLAGS
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--json print output as a JSON object with the new channel ID, name and branch mapping.
DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -555,21 +546,20 @@ point a channel at a new branch
```
USAGE
$ eas channel:edit [NAME] [--branch <value>] [--json --non-interactive]
$ eas channel:edit [NAME] [--branch <value>] [--json]
ARGUMENTS
NAME Name of the channel to edit
FLAGS
--branch=<value> Name of the branch to point to
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--branch=<value> Name of the branch to point to
--json Print output as a JSON object with the channel ID, name and branch mapping
DESCRIPTION
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -577,19 +567,16 @@ list all channels
```
USAGE
$ eas channel:list [--offset <value>] [--limit <value>] [--json --non-interactive]
$ eas channel:list [--json]
FLAGS
--json Enable JSON output, non-JSON messages will be printed to stderr.
--limit=<value> The number of items to fetch each query. Defaults to 10 and is capped at 25.
--non-interactive Run the command in non-interactive mode.
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
--json print output as a JSON object with the channel ID, name and branch mapping.
DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:view [NAME]`
@@ -597,22 +584,19 @@ view a channel
```
USAGE
$ eas channel:view [NAME] [--json --non-interactive] [--offset <value>] [--limit <value>]
$ eas channel:view [NAME] [--json]
ARGUMENTS
NAME Name of the channel to view
FLAGS
--json Enable JSON output, non-JSON messages will be printed to stderr.
--limit=<value> The number of items to fetch each query. Defaults to 50 and is capped at 100.
--non-interactive Run the command in non-interactive mode.
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
--json print output as a JSON object with the channel ID, name and branch mapping.
DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -620,18 +604,17 @@ display project configuration (app.json + eas.json)
```
USAGE
$ eas config [-p android|ios] [-e <value>]
$ eas config [-p android|ios] [--profile <value>]
FLAGS
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined in
eas.json.
-p, --platform=(android|ios)
--profile=<value>
DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -648,7 +631,7 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -662,7 +645,7 @@ DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -670,19 +653,17 @@ remove a registered device from your account
```
USAGE
$ eas device:delete [--apple-team-id <value>] [--udid <value>] [--json --non-interactive]
$ eas device:delete [--apple-team-id <value>] [--udid <value>]
FLAGS
--apple-team-id=<value> The Apple team ID on which to find the device
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--udid=<value> The Apple device ID to disable
--apple-team-id=<value>
--udid=<value>...
DESCRIPTION
remove a registered device from your account
```
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -690,20 +671,16 @@ list all registered devices for your account
```
USAGE
$ eas device:list [--apple-team-id <value>] [--offset <value>] [--limit <value>] [--json --non-interactive]
$ eas device:list [--apple-team-id <value>]
FLAGS
--apple-team-id=<value>
--json Enable JSON output, non-JSON messages will be printed to stderr.
--limit=<value> The number of items to fetch each query. Defaults to 50 and is capped at 100.
--non-interactive Run the command in non-interactive mode.
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
DESCRIPTION
list all registered devices for your account
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -717,7 +694,7 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -731,7 +708,7 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -759,12 +736,7 @@ create or link an EAS project
```
USAGE
$ eas init [--force --id <value>] [--non-interactive ]
FLAGS
--force Whether to overwrite any existing project ID
--id=<value> ID of the EAS project to link
--non-interactive Run the command in non-interactive mode.
$ eas init
DESCRIPTION
create or link an EAS project
@@ -809,16 +781,16 @@ generate the local store configuration from the app stores
```
USAGE
$ eas metadata:pull [-e <value>]
$ eas metadata:pull [--profile <value>]
FLAGS
-e, --profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.
--profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.
DESCRIPTION
generate the local store configuration from the app stores
```
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -826,16 +798,16 @@ sync the local store configuration to the app stores
```
USAGE
$ eas metadata:push [-e <value>]
$ eas metadata:push [--profile <value>]
FLAGS
-e, --profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.
--profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.
DESCRIPTION
sync the local store configuration to the app stores
```
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas open`
@@ -849,7 +821,7 @@ DESCRIPTION
open the project page in a web browser
```
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/open.ts)_
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -863,7 +835,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -871,12 +843,7 @@ create or link an EAS project
```
USAGE
$ eas project:init [--force --id <value>] [--non-interactive ]
FLAGS
--force Whether to overwrite any existing project ID
--id=<value> ID of the EAS project to link
--non-interactive Run the command in non-interactive mode.
$ eas project:init
DESCRIPTION
create or link an EAS project
@@ -885,7 +852,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/project/init.ts)_
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/project/init.ts)_
## `eas secret:create`
@@ -893,22 +860,19 @@ create an environment secret on the current project or owner account
```
USAGE
$ eas secret:create [--scope account|project] [--name <value>] [--value <value>] [--type string|file] [--force]
[--non-interactive]
$ eas secret:create [--scope account|project] [--name <value>] [--value <value>] [--force]
FLAGS
--force Delete and recreate existing secrets
--name=<value> Name of the secret
--non-interactive Run the command in non-interactive mode.
--scope=(account|project) [default: project] Scope for the secret
--type=(string|file) The type of secret
--value=<value> Text value or path to a file to store in the secret
--value=<value> Value of the secret
DESCRIPTION
create an environment secret on the current project or owner account
```
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -916,17 +880,16 @@ delete an environment secret by ID
```
USAGE
$ eas secret:delete [--id <value>] [--non-interactive]
$ eas secret:delete [--id <value>]
FLAGS
--id=<value> ID of the secret to delete
--non-interactive Run the command in non-interactive mode.
--id=<value> ID of the secret to delete
DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -940,7 +903,7 @@ DESCRIPTION
list environment secrets available for your current app
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/secret/list.ts)_
## `eas submit`
@@ -948,17 +911,17 @@ submit app binary to App Store and/or Play Store
```
USAGE
$ eas submit [-p android|ios|all] [-e <value>] [--latest | --id <value> | --path <value> | --url <value>]
[--verbose] [--wait] [--non-interactive]
$ eas submit [-p android|ios|all] [--profile <value>] [--latest | --id <value> | --path <value> | --url
<value>] [--verbose] [--wait] [--non-interactive]
FLAGS
-e, --profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in
eas.json.
-p, --platform=(android|ios|all)
--id=<value> ID of the build to submit
--latest Submit the latest build for specified platform
--non-interactive Run command in non-interactive mode
--path=<value> Path to the .apk/.aab/.ipa file
--profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in
eas.json.
--url=<value> App archive url
--verbose Always print logs from Submission Service
--[no-]wait Wait for submission to complete
@@ -970,7 +933,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -979,7 +942,7 @@ publish an update group
```
USAGE
$ eas update [--branch <value>] [--message <value>] [--republish | --input-dir <value> | --skip-bundler]
[--group <value> | | ] [-p android|ios|all] [--auto] [--private-key-path <value>] [--json --non-interactive]
[--group <value> | | ] [-p android|ios|all] [--json] [--auto] [--private-key-path <value>] [--non-interactive]
FLAGS
-p, --platform=(android|ios|all) [default: all]
@@ -987,9 +950,9 @@ FLAGS
--branch=<value> Branch to publish the update group on
--group=<value> Update group to republish
--input-dir=<value> [default: dist] Location of the bundle
--json Enable JSON output, non-JSON messages will be printed to stderr.
--json Enable JSON output, non-JSON messages will be printed to stderr
--message=<value> A short message describing the update
--non-interactive Run the command in non-interactive mode.
--non-interactive Run command in non-interactive mode
--private-key-path=<value> File containing the PEM-encoded private key corresponding to the certificate in
expo-updates' configuration. Defaults to a file named "private-key.pem" in the
certificate's directory.
@@ -1000,7 +963,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/update/index.ts)_
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1017,7 +980,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1025,42 +988,38 @@ delete all the updates in an update group
```
USAGE
$ eas update:delete [GROUPID] [--json --non-interactive]
$ eas update:delete [GROUPID] [--json]
ARGUMENTS
GROUPID The ID of an update group to delete.
FLAGS
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--json Return a json with the group ID of the deleted updates.
DESCRIPTION
delete all the updates in an update group
```
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:list`
view the recent updates
view the recent updates for a branch
```
USAGE
$ eas update:list [--branch <value> | --all] [--offset <value>] [--limit <value>] [--json --non-interactive]
$ eas update:list [--branch <value> | --all] [--json]
FLAGS
--all List updates on all branches
--branch=<value> List updates only on this branch
--json Enable JSON output, non-JSON messages will be printed to stderr.
--limit=<value> The number of items to fetch each query. Defaults to 25 and is capped at 50.
--non-interactive Run the command in non-interactive mode.
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
--all List all updates associated with this project
--branch=<value> List all updates on this branch
--json Return a json with all of the recent update groups.
DESCRIPTION
view the recent updates
view the recent updates for a branch
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/update/list.ts)_
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:view GROUPID`
@@ -1074,13 +1033,13 @@ ARGUMENTS
GROUPID The ID of an update group.
FLAGS
--json Enable JSON output, non-JSON messages will be printed to stderr.
--json Return a json with the updates belonging to the group.
DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/update/view.ts)_
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1088,11 +1047,10 @@ create a webhook
```
USAGE
$ eas webhook:create [--event BUILD|SUBMIT] [--url <value>] [--secret <value>] [--non-interactive]
$ eas webhook:create [--event BUILD|SUBMIT] [--url <value>] [--secret <value>]
FLAGS
--event=(BUILD|SUBMIT) Event type that triggers the webhook
--non-interactive Run the command in non-interactive mode.
--secret=<value> Secret used to create a hash signature of the request payload, provided in the
'Expo-Signature' header.
--url=<value> Webhook URL
@@ -1101,7 +1059,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1109,19 +1067,16 @@ delete a webhook
```
USAGE
$ eas webhook:delete [ID] [--non-interactive]
$ eas webhook:delete [ID]
ARGUMENTS
ID ID of the webhook to delete
FLAGS
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1138,7 +1093,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1146,12 +1101,11 @@ update a webhook
```
USAGE
$ eas webhook:update --id <value> [--event BUILD|SUBMIT] [--url <value>] [--secret <value>] [--non-interactive]
$ eas webhook:update --id <value> [--event BUILD|SUBMIT] [--url <value>] [--secret <value>]
FLAGS
--event=(BUILD|SUBMIT) Event type that triggers the webhook
--id=<value> (required) Webhook ID
--non-interactive Run the command in non-interactive mode.
--secret=<value> Secret used to create a hash signature of the request payload, provided in the
'Expo-Signature' header.
--url=<value> Webhook URL
@@ -1160,7 +1114,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1177,7 +1131,7 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v2.2.1/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v2.1.0/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
+1 -188
View File
@@ -11200,32 +11200,7 @@
{
"name": "appleDevices",
"description": "",
"args": [
{
"name": "limit",
"description": "",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "offset",
"description": "",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
@@ -14273,50 +14248,6 @@
}
]
},
{
"kind": "OBJECT",
"name": "BuildOrBuildJobQuery",
"description": "",
"fields": [
{
"name": "byId",
"description": "Look up EAS Build or Classic Build Job by ID",
"args": [
{
"name": "buildOrBuildJobId",
"description": "",
"type": {
"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": "UNION",
"name": "EASBuildOrClassicBuildJob",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "BuildParamsInput",
@@ -16530,27 +16461,6 @@
],
"possibleTypes": null
},
{
"kind": "UNION",
"name": "EASBuildOrClassicBuildJob",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": [
{
"kind": "OBJECT",
"name": "Build",
"ofType": null
},
{
"kind": "OBJECT",
"name": "BuildJob",
"ofType": null
}
]
},
{
"kind": "ENUM",
"name": "EASServiceMetric",
@@ -17774,87 +17684,6 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "searchRepositories",
"description": "",
"args": [
{
"name": "githubAppInstallationId",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "page",
"description": "",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": "1",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "perPage",
"description": "",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": "50",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "query",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"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": "GitHubAppInstallationAccessibleRepository",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -24341,22 +24170,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "buildOrBuildJob",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "BuildOrBuildJobQuery",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "buildPublicData",
"description": "Top-level query object for querying BuildPublicData publicly.",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "2.2.1",
"version": "2.1.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -14,7 +14,7 @@
"@expo/config-plugins": "5.0.1",
"@expo/config-types": "46.0.2",
"@expo/eas-build-job": "0.2.94",
"@expo/eas-json": "2.2.0",
"@expo/eas-json": "2.1.0",
"@expo/json-file": "8.2.36",
"@expo/multipart-body-parser": "1.1.0",
"@expo/package-manager": "0.0.56",
+3 -25
View File
@@ -18,7 +18,7 @@ import {
} from '../graphql/generated';
import { BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log, { learnMore, link } from '../log';
import Log, { learnMore } from '../log';
import { Ora, ora } from '../ora';
import {
appPlatformDisplayNames,
@@ -140,34 +140,12 @@ function handleBuildRequestError(error: any, platform: Platform): never {
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
) {
Log.error(
`EAS Build is down for maintenance. Try again later. Check ${link(
'https://status.expo.dev/'
)} for updates.`
'EAS Build is down for maintenance. Try again later. Check https://status.expo.dev/ for updates.'
);
throw new Error('Build request failed.');
} else if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED') {
Log.error(
`EAS Build free tier is temporarily disabled and we are not accepting any new builds. Try again later. ${learnMore(
'https://expo.fyi/eas-build-queues'
)}`
);
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED_IOS'
) {
Log.error(
`EAS Build free tier is temporarily disabled for iOS and we are not accepting any new builds. Try again later. ${learnMore(
'https://expo.fyi/eas-build-queues'
)}`
);
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED_ANDROID'
) {
Log.error(
`EAS Build free tier is temporarily disabled for Android and we are not accepting any new builds. Try again later. ${learnMore(
'https://expo.fyi/eas-build-queues'
)}`
'EAS Build free tier is temporarily disabled. Try again later. Check https://status.expo.dev/ for updates.'
);
throw new Error('Build request failed.');
} else if (
+4 -4
View File
@@ -7,9 +7,9 @@ import { v4 as uuidv4 } from 'uuid';
import { TrackingContext } from '../analytics/common';
import { Analytics, BuildEvent } from '../analytics/events';
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
import { CredentialsContext } from '../credentials/context';
import { BuildResourceClass } from '../graphql/generated';
import { getExpoConfig } from '../project/expoConfig';
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { Actor } from '../user/User';
@@ -31,7 +31,7 @@ export async function createBuildContextAsync<T extends Platform>({
resourceClass,
message,
actor,
getDynamicProjectConfigAsync,
projectId,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
@@ -45,9 +45,9 @@ export async function createBuildContextAsync<T extends Platform>({
resourceClass: BuildResourceClass;
message?: string;
actor: Actor;
getDynamicProjectConfigAsync: DynamicConfigContextFn;
projectId: string;
}): Promise<BuildContext<T>> {
const { exp, projectId } = await getDynamicProjectConfigAsync({ env: buildProfile.env });
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
const projectName = exp.slug;
const account = await getOwnerAccountForProjectIdAsync(projectId);
+1 -1
View File
@@ -6,7 +6,7 @@ import semver from 'semver';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.110';
const PLUGIN_PACKAGE_VERSION = '0.0.99';
export interface LocalBuildOptions {
enable: boolean;
@@ -10,7 +10,6 @@ import {
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
import {
AppPlatform,
BuildFragment,
@@ -42,7 +41,6 @@ import {
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { validateBuildProfileConfigMatchesProjectConfigAsync } from '../update/utils';
import { Actor } from '../user/User';
import { printJsonOnlyOutput } from '../utils/json';
import { ProfileData, getProfilesAsync } from '../utils/profiles';
@@ -94,7 +92,7 @@ export async function runBuildAndSubmitAsync(
projectDir: string,
flags: BuildFlags,
actor: Actor,
getDynamicProjectConfigAsync: DynamicConfigContextFn
projectId: string
): Promise<void> {
await getVcsClient().ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
@@ -143,7 +141,7 @@ export async function runBuildAndSubmitAsync(
],
easJsonCliConfig,
actor,
getDynamicProjectConfigAsync,
projectId,
});
if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile });
@@ -238,7 +236,7 @@ async function prepareAndStartBuildAsync({
resourceClass,
easJsonCliConfig,
actor,
getDynamicProjectConfigAsync,
projectId,
}: {
projectDir: string;
flags: BuildFlags;
@@ -247,7 +245,7 @@ async function prepareAndStartBuildAsync({
resourceClass: BuildResourceClass;
easJsonCliConfig: EasJson['cli'];
actor: Actor;
getDynamicProjectConfigAsync: DynamicConfigContextFn;
projectId: string;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName,
@@ -262,7 +260,7 @@ async function prepareAndStartBuildAsync({
easJsonCliConfig,
message: flags.message,
actor,
getDynamicProjectConfigAsync,
projectId,
});
if (moreBuilds) {
@@ -274,14 +272,6 @@ async function prepareAndStartBuildAsync({
)}`
);
}
await validateBuildProfileConfigMatchesProjectConfigAsync(
buildCtx.exp,
buildProfile,
buildCtx.projectId,
flags.nonInteractive
);
await validateAppVersionRuntimePolicySupportAsync(buildCtx.projectDir, buildCtx.exp);
if (easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE) {
validateAppConfigForRemoteVersionSource(buildCtx.exp, buildProfile.platform);
@@ -335,6 +325,7 @@ async function prepareAndStartSubmissionAsync({
const submissionCtx = await createSubmissionContextAsync({
platform,
projectDir,
projectId: build.project.id,
profile: submitProfile,
archiveFlags: { id: build.id },
nonInteractive,
@@ -342,8 +333,6 @@ async function prepareAndStartSubmissionAsync({
credentialsCtx: buildCtx.credentialsCtx,
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
actor: buildCtx.user,
projectId: buildCtx.projectId,
exp: buildCtx.exp,
});
if (moreBuilds) {
+149 -56
View File
@@ -1,4 +1,10 @@
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import * as PackageManagerUtils from '@expo/package-manager';
import { Command } from '@oclif/core';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import semver from 'semver';
import {
AnalyticsEvent,
@@ -6,13 +12,74 @@ import {
initAsync as initAnalyticsAsync,
logEvent,
} from '../analytics/rudderstackClient';
import { getUserAsync } from '../user/User';
import ActorContextField from './context/ActorContextField';
import ContextField from './context/ContextField';
import { DynamicProjectConfigContextField } from './context/DynamicProjectConfigContextField';
import { OptionalProjectConfigContextField } from './context/OptionalProjectConfigContextField';
import ProjectConfigContextField from './context/ProjectConfigContextField';
import ProjectDirContextField from './context/ProjectDirContextField';
import { learnMore } from '../log';
import { getExpoConfig } from '../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
import { Actor, getUserAsync } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { setVcsClient } from '../vcs';
import GitClient from '../vcs/clients/git';
export interface CommandConfiguration {
/**
* When user data is unavailable locally, determines if the command will
* force the user to log in. By default, all commands require authentication.
*/
allowUnauthenticated?: boolean;
/**
* Whether a command can be run outside of an Expo project directory.
* By default, all commands must be run inside an Expo project directory.
*/
canRunOutsideProject?: boolean;
}
export interface ContextOptions {
nonInteractive: boolean;
}
export abstract class ContextField<T> {
abstract getValueAsync(options: ContextOptions): Promise<T>;
}
export class ProjectIdContextField extends ContextField<string> {
async getValueAsync({ nonInteractive }: ContextOptions): Promise<string> {
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
return await getProjectIdAsync(exp, { nonInteractive });
}
}
export class OptionalProjectIdContextField extends ContextField<string | undefined> {
async getValueAsync({ nonInteractive }: ContextOptions): Promise<string | undefined> {
const projectDir = await findProjectRootAsync();
if (!projectDir) {
return undefined;
}
const exp = getExpoConfig(projectDir);
return await getProjectIdAsync(exp, { nonInteractive });
}
}
export class ActorContextField extends ContextField<Actor> {
async getValueAsync({ nonInteractive }: ContextOptions): Promise<Actor> {
return await ensureLoggedInAsync({ nonInteractive });
}
}
export const EASCommandProjectIdContext = {
projectId: new ProjectIdContextField(),
};
export const EASCommandProjectIdIfProjectDirContext = {
projectId: new OptionalProjectIdContextField(),
};
export const EASCommandLoggedInContext = {
actor: new ActorContextField(),
};
type ContextInput<
T extends {
@@ -31,55 +98,10 @@ type ContextOutput<
};
export default abstract class EasCommand extends Command {
protected static readonly ContextOptions = {
/**
* Require this command to be run when logged-in. Returns the logged-in actor in the context.
*/
LoggedIn: {
actor: new ActorContextField(),
},
/**
* Require the project to be identified and registered on server if this command is being
* run within a project directory, null otherwise.
*/
OptionalProjectConfig: {
projectConfig: new OptionalProjectConfigContextField(),
},
/**
* Require this command to be run in a project directory. Return the project directory in the context.
*/
ProjectDir: {
projectDir: new ProjectDirContextField(),
},
/**
* Provides functions to load the project config when dynamic config options are needed (custom Env for example).
*/
DynamicProjectConfig: {
// eslint-disable-next-line async-protect/async-suffix
getDynamicProjectConfigAsync: new DynamicProjectConfigContextField(),
},
/**
* Require the project to be identified and registered on server. Returns the project config in the context.
*/
ProjectConfig: {
projectConfig: new ProjectConfigContextField(),
},
};
/**
* Context allows for subclasses (commands) to declare their prerequisites in a type-safe manner.
* These declarative definitions each output a context property that is the result of the prerequisite being
* satisfied. These allow a unified common interface to be shared amongst commands in order to provide a more
* consistent CLI experience.
*
* For example, let's say a command needs the EAS project ID to make a GraphQL mutation. It should declare that
* it requires the `ProjectConfig` context, and then call `getContextAsync` to get the project ID.
*/
static contextDefinition: ContextInput = {};
/**
* Execute the context in the contextDefinition to satisfy command prerequisites.
*/
protected commandConfiguration: CommandConfiguration = {};
protected async getContextAsync<
C extends {
[name: string]: any;
@@ -105,8 +127,20 @@ export default abstract class EasCommand extends Command {
async run(): Promise<any> {
await initAnalyticsAsync();
// this is needed for logEvent call below as it identifies the user in the analytics system
await getUserAsync();
if (!this.commandConfiguration.canRunOutsideProject) {
const projectDir = await findProjectRootAsync();
await this.applyCliConfigAsync(projectDir);
await this.ensureEasCliIsNotInDependenciesAsync(projectDir);
}
if (!this.commandConfiguration.allowUnauthenticated) {
const { flags } = await this.parse();
const nonInteractive = (flags as any)['non-interactive'] ?? false;
await ensureLoggedInAsync({ nonInteractive });
} else {
await getUserAsync();
}
logEvent(AnalyticsEvent.ACTION, {
// id is assigned by oclif in constructor based on the filepath:
// commands/submit === submit, commands/build/list === build:list
@@ -121,4 +155,63 @@ export default abstract class EasCommand extends Command {
await flushAnalyticsAsync();
return super.finally(err);
}
private async applyCliConfigAsync(projectDir: string): Promise<void> {
const easJsonAccessor = new EasJsonAccessor(projectDir);
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
throw new Error(
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
);
}
if (config?.requireCommit) {
setVcsClient(new GitClient());
}
}
private async ensureEasCliIsNotInDependenciesAsync(projectDir: string): Promise<void> {
let printCliVersionWarning = false;
const consoleWarn = (msg?: string): void => {
if (msg) {
// eslint-disable-next-line no-console
console.warn(chalk.yellow(msg));
} else {
// eslint-disable-next-line no-console
console.warn();
}
};
if (await this.isEasCliInDependenciesAsync(projectDir)) {
printCliVersionWarning = true;
consoleWarn(`Found ${chalk.bold('eas-cli')} in your project dependencies.`);
}
const maybeRepoRoot = PackageManagerUtils.findWorkspaceRoot(projectDir) ?? projectDir;
if (maybeRepoRoot !== projectDir && (await this.isEasCliInDependenciesAsync(maybeRepoRoot))) {
printCliVersionWarning = true;
consoleWarn(`Found ${chalk.bold('eas-cli')} in your monorepo dependencies.`);
}
if (printCliVersionWarning) {
consoleWarn(
`It's recommended to use the ${chalk.bold(
'"cli.version"'
)} field in eas.json to enforce the ${chalk.bold('eas-cli')} version for your project.`
);
consoleWarn(
learnMore('https://github.com/expo/eas-cli#enforcing-eas-cli-version-for-your-project')
);
consoleWarn();
}
}
private async isEasCliInDependenciesAsync(dir: string): Promise<boolean> {
const packageJsonPath = path.join(dir, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
return (
packageJson?.dependencies?.['eas-cli'] !== undefined ||
packageJson?.devDependencies?.['eas-cli'] !== undefined
);
}
}
@@ -1,9 +1,11 @@
import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClient';
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
import { getUserAsync } from '../../user/User';
import EasCommand from '../EasCommand';
import { ensureLoggedInAsync } from '../../user/actions';
import EasCommand, { CommandConfiguration } from '../EasCommand';
jest.mock('../../user/User');
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
jest.mock('../../analytics/rudderstackClient', () => {
const { AnalyticsEvent } = jest.requireActual('../../analytics/rudderstackClient');
return {
@@ -30,13 +32,21 @@ beforeEach(() => {
.mocked(getUserAsync)
.mockReset()
.mockImplementation(async () => mockJester);
jest
.mocked(ensureLoggedInAsync)
.mockReset()
.mockImplementation(async () => mockJester);
jest.mocked(initAsync).mockReset();
jest.mocked(flushAsync).mockReset();
jest.mocked(logEvent).mockReset();
});
const createTestEasCommand = (): typeof EasCommand => {
const createTestEasCommand = (authValue: boolean): typeof EasCommand => {
class TestEasCommand extends EasCommand {
protected override commandConfiguration: CommandConfiguration = {
allowUnauthenticated: !authValue,
};
async runAsync(): Promise<void> {}
}
@@ -53,29 +63,37 @@ describe(EasCommand.name, () => {
//
// See https://github.com/oclif/command/blob/master/src/command.ts#L80
// and look for "Config.load"
it('ensures the user data is read', async () => {
const TestEasCommand = createTestEasCommand();
it('ensures the user is logged in', async () => {
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(ensureLoggedInAsync).toHaveBeenCalled();
}, 15_000);
it('ensures the user data is read from cache', async () => {
const TestEasCommand = createTestEasCommand(false);
await TestEasCommand.run();
expect(jest.mocked(getUserAsync)).toBeCalledTimes(1);
}, 15_000);
expect(jest.mocked(ensureLoggedInAsync)).not.toBeCalled();
});
it('initializes analytics', async () => {
const TestEasCommand = createTestEasCommand();
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(initAsync).toHaveBeenCalled();
});
it('flushes analytics', async () => {
const TestEasCommand = createTestEasCommand();
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(flushAsync).toHaveBeenCalled();
});
it('logs events', async () => {
const TestEasCommand = createTestEasCommand();
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(logEvent).toHaveBeenCalledWith('action', {
@@ -86,7 +104,7 @@ describe(EasCommand.name, () => {
describe('after exceptions', () => {
it('flushes analytics', async () => {
const TestEasCommand = createTestEasCommand();
const TestEasCommand = createTestEasCommand(true);
try {
await TestEasCommand.run().then(() => {
throw new Error('foo');
@@ -1,9 +0,0 @@
import { Actor } from '../../user/User';
import ContextField, { ContextOptions } from './ContextField';
import { ensureLoggedInAsync } from './contextUtils/ensureLoggedInAsync';
export default class ActorContextField extends ContextField<Actor> {
async getValueAsync({ nonInteractive }: ContextOptions): Promise<Actor> {
return await ensureLoggedInAsync({ nonInteractive });
}
}
@@ -1,7 +0,0 @@
export interface ContextOptions {
nonInteractive: boolean;
}
export default abstract class ContextField<T> {
abstract getValueAsync(options: ContextOptions): Promise<T>;
}
@@ -1,31 +0,0 @@
import { ExpoConfig } from '@expo/config-types';
import { ExpoConfigOptions, getExpoConfig } from '../../project/expoConfig';
import ContextField, { ContextOptions } from './ContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
export type DynamicConfigContextFn = (options?: ExpoConfigOptions) => Promise<{
projectId: string;
exp: ExpoConfig;
projectDir: string;
}>;
export class DynamicProjectConfigContextField extends ContextField<DynamicConfigContextFn> {
async getValueAsync({ nonInteractive }: ContextOptions): Promise<DynamicConfigContextFn> {
return async (options?: ExpoConfigOptions) => {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
const expBefore = getExpoConfig(projectDir, options);
const projectId = await getProjectIdAsync(expBefore, {
nonInteractive,
env: options?.env,
});
const exp = getExpoConfig(projectDir, options);
return {
exp,
projectDir,
projectId,
};
};
}
}
@@ -1,49 +0,0 @@
import { ExpoConfig } from '@expo/config-types';
import { InvalidEasJsonError } from '@expo/eas-json/build/errors';
import { getExpoConfig } from '../../project/expoConfig';
import ContextField, { ContextOptions } from './ContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
export class OptionalProjectConfigContextField extends ContextField<
| {
projectId: string;
exp: ExpoConfig;
projectDir: string;
}
| undefined
> {
async getValueAsync({ nonInteractive }: ContextOptions): Promise<
| {
projectId: string;
exp: ExpoConfig;
projectDir: string;
}
| undefined
> {
let projectDir: string;
try {
projectDir = await findProjectDirAndVerifyProjectSetupAsync();
if (!projectDir) {
return undefined;
}
} catch (e) {
if (e instanceof InvalidEasJsonError) {
throw e;
}
return undefined;
}
const expBefore = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(expBefore, {
nonInteractive,
});
const exp = getExpoConfig(projectDir);
return {
exp,
projectDir,
projectId,
};
}
}
@@ -1,31 +0,0 @@
import { ExpoConfig } from '@expo/config-types';
import { getExpoConfig } from '../../project/expoConfig';
import ContextField, { ContextOptions } from './ContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
export default class ProjectConfigContextField extends ContextField<{
projectId: string;
exp: ExpoConfig;
projectDir: string;
}> {
async getValueAsync({ nonInteractive }: ContextOptions): Promise<{
projectId: string;
exp: ExpoConfig;
projectDir: string;
}> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
const expBefore = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(expBefore, {
nonInteractive,
});
const exp = getExpoConfig(projectDir);
return {
projectId,
exp,
projectDir,
};
}
}
@@ -1,8 +0,0 @@
import ContextField from './ContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
export default class ProjectDirContextField extends ContextField<string> {
async getValueAsync(): Promise<string> {
return await findProjectDirAndVerifyProjectSetupAsync();
}
}
@@ -1,62 +0,0 @@
import { vol } from 'memfs';
import { findProjectRootAsync } from '../findProjectDirAndVerifyProjectSetupAsync';
jest.mock('@expo/config');
jest.mock('fs');
jest.mock('../../../../prompts');
beforeEach(() => {
jest.resetAllMocks();
});
describe(findProjectRootAsync, () => {
beforeEach(() => {
vol.reset();
});
it('throws if not inside the project directory', async () => {
vol.fromJSON(
{
'./README.md': '1',
},
'/app'
);
await expect(findProjectRootAsync({ cwd: '/app' })).rejects.toThrow(
'Run this command inside a project directory.'
);
});
it('defaults to process.cwd() if defaultToProcessCwd = true', async () => {
const spy = jest.spyOn(process, 'cwd').mockReturnValue('/this/is/fake/process/cwd');
try {
vol.fromJSON(
{
'./README.md': '1',
},
'/app'
);
const projectDir = await findProjectRootAsync({
cwd: '/app',
defaultToProcessCwd: true,
});
expect(projectDir).toBe('/this/is/fake/process/cwd');
} finally {
spy.mockRestore();
}
});
it('returns the root directory of the project', async () => {
vol.fromJSON(
{
'./README.md': '1',
'./package.json': '2',
'./src/index.ts': '3',
},
'/app'
);
const projectRoot = await findProjectRootAsync({ cwd: '/app/src' });
expect(projectRoot).toBe('/app');
});
});
@@ -1,168 +0,0 @@
import { getConfig, modifyConfigAsync } from '@expo/config';
import { vol } from 'memfs';
import { Role } from '../../../../graphql/generated';
import { AppQuery } from '../../../../graphql/queries/AppQuery';
import { learnMore } from '../../../../log';
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { ensureLoggedInAsync } from '../../contextUtils/ensureLoggedInAsync';
import { findProjectRootAsync } from '../findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from '../getProjectIdAsync';
jest.mock('@expo/config');
jest.mock('fs');
jest.mock('../../../../graphql/queries/AppQuery');
jest.mock('../../contextUtils/ensureLoggedInAsync');
jest.mock('../../contextUtils/findProjectDirAndVerifyProjectSetupAsync');
jest.mock('../../../../user/User');
jest.mock('../../../../ora', () => ({
ora: () => ({
start: () => ({ succeed: () => {}, fail: () => {} }),
}),
}));
jest.mock('../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
describe(getProjectIdAsync, () => {
beforeEach(() => {
jest.mocked(ensureLoggedInAsync).mockResolvedValue({
__typename: 'User',
id: 'user_id',
username: 'notnotbrent',
primaryAccount: {
id: 'account_id_1',
name: 'notnotbrent',
users: [{ role: Role.Owner, actor: { id: 'user_id' } }],
},
accounts: [
{
id: 'account_id_1',
name: 'notnotbrent',
users: [{ role: Role.Owner, actor: { id: 'user_id' } }],
},
{
id: 'account_id_2',
name: 'dominik',
users: [{ role: Role.ViewOnly, actor: { id: 'user_id' } }],
},
],
isExpoAdmin: false,
});
vol.fromJSON(
{
'./README.md': '1',
'./package.json': '2',
'./src/index.ts': '3',
},
'/app'
);
jest.mocked(findProjectRootAsync).mockResolvedValue('/app');
});
it('gets the project ID from app config if exists', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@notnotbrent/test',
slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any,
});
await expect(
getProjectIdAsync(
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).resolves.toEqual('1234');
});
it('throws when the owner is out of sync', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', owner: 'wat', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@notnotbrent/test',
slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any,
});
await expect(
getProjectIdAsync(
{ name: 'test', slug: 'test', owner: 'wat', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Project identified by "extra.eas.projectId" (notnotbrent) is not owned by owner specified in the "owner" field (wat). ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
});
it('throws when the slug is out of sync', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@notnotbrent/test',
slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any,
});
await expect(
getProjectIdAsync(
{ name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Slug for project identified by "extra.eas.projectId" (test) does not match the "slug" field (wat). ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
});
it('fetches the project ID when not in app config, and sets it in the config', async () => {
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'success',
config: { expo: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } } },
});
jest
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
.mockImplementation(async () => '2345');
const projectId = await getProjectIdAsync(
{ name: 'test', slug: 'test' },
{
nonInteractive: false,
}
);
expect(projectId).toEqual('2345');
expect(modifyConfigAsync).toHaveBeenCalledTimes(1);
expect(modifyConfigAsync).toHaveBeenCalledWith('/app', {
extra: { eas: { projectId: '2345' } },
});
});
it('throws if writing the ID back to the config fails', async () => {
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'fail',
config: null,
});
jest
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
.mockImplementation(async () => '4567');
await expect(
getProjectIdAsync({ name: 'test', slug: 'test' }, { nonInteractive: false })
).rejects.toThrow();
});
});
@@ -1,91 +0,0 @@
import { Errors } from '@oclif/core';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { ApiV2Error } from '../../../api';
import Log, { learnMore } from '../../../log';
import { promptAsync } from '../../../prompts';
import { Actor, getUserAsync, loginAsync } from '../../../user/User';
import { retryUsernamePasswordAuthWithOTPAsync } from '../../../user/otp';
/**
* Ensure that there is a logged-in actor. Show a login prompt if not.
*
* @param nonInteractive whether the log-in prompt if logged-out should be interactive
* @returns logged-in Actor
*
* @deprecated Should not be used outside of context functions.
*/
export async function ensureLoggedInAsync({
nonInteractive,
}: {
nonInteractive: boolean;
}): Promise<Actor> {
let user: Actor | undefined;
try {
user = await getUserAsync();
} catch {}
if (!user) {
Log.warn('An Expo user account is required to proceed.');
await showLoginPromptAsync({ nonInteractive, printNewLine: true });
user = await getUserAsync();
}
return nullthrows(user);
}
/**
* Prompt the user to log in.
*
* @deprecated Should not be used outside of context functions, except in the AccountLogin command.
*/
export async function showLoginPromptAsync({
nonInteractive = false,
printNewLine = false,
} = {}): Promise<void> {
if (nonInteractive) {
Errors.error(
`Either log in with ${chalk.bold('eas login')} or set the ${chalk.bold(
'EXPO_TOKEN'
)} environment variable if you're using EAS CLI on CI (${learnMore(
'https://docs.expo.dev/accounts/programmatic-access/',
{ dim: false }
)})`
);
}
if (printNewLine) {
Log.newLine();
}
Log.log('Log in to EAS');
const { username, password } = await promptAsync([
{
type: 'text',
name: 'username',
message: 'Email or username',
},
{
type: 'password',
name: 'password',
message: 'Password',
},
]);
try {
await loginAsync({
username,
password,
});
} catch (e) {
if (e instanceof ApiV2Error && e.expoApiV2ErrorCode === 'ONE_TIME_PASSWORD_REQUIRED') {
await retryUsernamePasswordAuthWithOTPAsync(
username,
password,
e.expoApiV2ErrorMetadata as any
);
} else {
throw e;
}
}
}
@@ -1,118 +0,0 @@
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import * as PackageManagerUtils from '@expo/package-manager';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import pkgDir from 'pkg-dir';
import semver from 'semver';
import { learnMore } from '../../../log';
import { easCliVersion } from '../../../utils/easCli';
import { getVcsClient, setVcsClient } from '../../../vcs';
import GitClient from '../../../vcs/clients/git';
async function applyCliConfigAsync(projectDir: string): Promise<void> {
const easJsonAccessor = new EasJsonAccessor(projectDir);
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
throw new Error(
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
);
}
if (config?.requireCommit) {
setVcsClient(new GitClient());
}
}
async function ensureEasCliIsNotInDependenciesAsync(projectDir: string): Promise<void> {
let printCliVersionWarning = false;
const consoleWarn = (msg?: string): void => {
if (msg) {
// eslint-disable-next-line no-console
console.warn(chalk.yellow(msg));
} else {
// eslint-disable-next-line no-console
console.warn();
}
};
if (await isEasCliInDependenciesAsync(projectDir)) {
printCliVersionWarning = true;
consoleWarn(`Found ${chalk.bold('eas-cli')} in your project dependencies.`);
}
const maybeRepoRoot = PackageManagerUtils.findWorkspaceRoot(projectDir) ?? projectDir;
if (maybeRepoRoot !== projectDir && (await isEasCliInDependenciesAsync(maybeRepoRoot))) {
printCliVersionWarning = true;
consoleWarn(`Found ${chalk.bold('eas-cli')} in your monorepo dependencies.`);
}
if (printCliVersionWarning) {
consoleWarn(
`It's recommended to use the ${chalk.bold(
'"cli.version"'
)} field in eas.json to enforce the ${chalk.bold('eas-cli')} version for your project.`
);
consoleWarn(
learnMore('https://github.com/expo/eas-cli#enforcing-eas-cli-version-for-your-project')
);
consoleWarn();
}
}
async function isEasCliInDependenciesAsync(dir: string): Promise<boolean> {
const packageJsonPath = path.join(dir, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
return (
packageJson?.dependencies?.['eas-cli'] !== undefined ||
packageJson?.devDependencies?.['eas-cli'] !== undefined
);
}
/**
* @returns the project root directory.
*
* @deprecated Should not be used outside of context functions.
*/
export async function findProjectRootAsync({
cwd,
defaultToProcessCwd = false,
}: {
cwd?: string;
defaultToProcessCwd?: boolean;
} = {}): Promise<string> {
const projectRootDir = await pkgDir(cwd);
if (!projectRootDir) {
if (!defaultToProcessCwd) {
throw new Error('Run this command inside a project directory.');
} else {
return process.cwd();
}
} else {
let vcsRoot;
try {
vcsRoot = path.normalize(await getVcsClient().getRootPathAsync());
} catch {}
if (vcsRoot && vcsRoot.startsWith(projectRootDir) && vcsRoot !== projectRootDir) {
throw new Error(
`package.json is outside of the current git repository (project root: ${projectRootDir}, git root: ${vcsRoot}.`
);
}
return projectRootDir;
}
}
/**
* Determine the project root directory and ensure some constraints about the project setup
* like CLI version and dependencies.
* @returns the project root directory
*
* @deprecated Should not be used outside of context functions.
*/
export async function findProjectDirAndVerifyProjectSetupAsync(): Promise<string> {
const projectDir = await findProjectRootAsync();
await applyCliConfigAsync(projectDir);
await ensureEasCliIsNotInDependenciesAsync(projectDir);
return projectDir;
}
@@ -1,149 +0,0 @@
import { getProjectConfigDescription, modifyConfigAsync } from '@expo/config';
import { ExpoConfig } from '@expo/config-types';
import { Env } from '@expo/eas-build-job';
import chalk from 'chalk';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import Log, { learnMore } from '../../../log';
import { ora } from '../../../ora';
import { getExpoConfig } from '../../../project/expoConfig';
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { toAppPrivacy } from '../../../project/projectUtils';
import { Actor } from '../../../user/User';
import { ensureLoggedInAsync } from './ensureLoggedInAsync';
import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync';
/**
* Save an EAS project ID to the appropriate field in the app config.
*
* @deprecated Should not be used outside of context functions except in the init command.
*/
export async function saveProjectIdToAppConfigAsync(
projectDir: string,
projectId: string,
options: { env?: Env } = {}
): Promise<void> {
const exp = getExpoConfig(projectDir, options);
const result = await modifyConfigAsync(projectDir, {
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
});
switch (result.type) {
case 'success':
break;
case 'warn': {
Log.warn();
Log.warn(
`Warning: Your project uses dynamic app configuration, and the EAS project ID can't automatically be added to it.`
);
Log.warn(
chalk.dim(
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs'
)
);
Log.warn();
Log.warn(
`To complete the setup process, set "${chalk.bold(
'extra.eas.projectId'
)}" in your ${chalk.bold(getProjectConfigDescription(projectDir))}:`
);
Log.warn();
Log.warn(chalk.bold(JSON.stringify({ expo: { extra: { eas: { projectId } } } }, null, 2)));
Log.warn();
throw new Error(result.message);
}
case 'fail':
throw new Error(result.message);
default:
throw new Error('Unexpected result type from modifyConfigAsync');
}
}
/**
* Get the EAS project ID from the app config. If the project ID is not set in the config,
* use the owner/slug to identify an EAS project on the server (asking for confirmation first),
* and attempt to save the EAS project ID to the appropriate field in the app config. If unable to
* save to the app config, throw an error.
*
* @returns the EAS project ID
*
* @deprecated Should not be used outside of context functions.
*/
export async function getProjectIdAsync(
exp: ExpoConfig,
options: { env?: Env; nonInteractive: boolean }
): Promise<string> {
const localProjectId = exp.extra?.eas?.projectId;
if (localProjectId) {
// check that the local project ID matches account and slug
const appForProjectId = await AppQuery.byIdAsync(localProjectId);
if (exp.owner && exp.owner !== appForProjectId.ownerAccount.name) {
throw new Error(
`Project config: Project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) is not owned by owner specified in the "owner" field (${exp.owner}). ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
}
if (exp.slug && exp.slug !== appForProjectId.slug) {
throw new Error(
`Project config: Slug for project identified by "extra.eas.projectId" (${
appForProjectId.slug
}) does not match the "slug" field (${exp.slug}). ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
}
return localProjectId;
}
const projectDir = await findProjectRootAsync();
if (!projectDir) {
throw new Error('This command must be run inside a project directory.');
}
Log.warn('EAS project not configured.');
const getAccountNameForEASProjectSync = (exp: ExpoConfig, user: Actor): string => {
if (exp.owner) {
return exp.owner;
}
switch (user.__typename) {
case 'User':
return user.username;
case 'Robot':
throw new Error(
'The "owner" manifest property is required when using robot users. See: https://docs.expo.dev/versions/latest/config/app/#owner'
);
}
};
const actor = await ensureLoggedInAsync({
nonInteractive: options.nonInteractive,
});
const projectId = await fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync(
{
accountName: getAccountNameForEASProjectSync(exp, actor),
projectName: exp.slug,
privacy: toAppPrivacy(exp.privacy),
},
{
nonInteractive: options.nonInteractive,
},
actor
);
const spinner = ora(`Linking local project to EAS project ${projectId}`).start();
try {
await saveProjectIdToAppConfigAsync(projectDir, projectId, options);
spinner.succeed(`Linked local project to EAS project ${projectId}`);
} catch (e: any) {
spinner.fail();
throw e;
}
return projectId;
}
@@ -1,11 +1,16 @@
import EasCommand from '../../commandUtils/EasCommand';
import { showLoginPromptAsync } from '../../commandUtils/context/contextUtils/ensureLoggedInAsync';
import EasCommand, { CommandConfiguration } from '../../commandUtils/EasCommand';
import Log from '../../log';
import { showLoginPromptAsync } from '../../user/actions';
export default class AccountLogin extends EasCommand {
static override description = 'log in with your Expo account';
static override aliases = ['login'];
protected override commandConfiguration: CommandConfiguration = {
allowUnauthenticated: true,
canRunOutsideProject: true,
};
async runAsync(): Promise<void> {
await showLoginPromptAsync();
Log.log('Logged in');
@@ -1,4 +1,4 @@
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { CommandConfiguration } from '../../commandUtils/EasCommand';
import Log from '../../log';
import { logoutAsync } from '../../user/User';
@@ -6,6 +6,11 @@ export default class AccountLogout extends EasCommand {
static override description = 'log out';
static override aliases = ['logout'];
protected override commandConfiguration: CommandConfiguration = {
allowUnauthenticated: true,
canRunOutsideProject: true,
};
async runAsync(): Promise<void> {
await logoutAsync();
Log.log('Logged out');
@@ -1,6 +1,6 @@
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { CommandConfiguration } from '../../commandUtils/EasCommand';
import Log from '../../log';
import { getActorDisplayName, getUserAsync } from '../../user/User';
@@ -8,6 +8,11 @@ export default class AccountView extends EasCommand {
static override description = 'show the username you are logged in as';
static override aliases = ['whoami'];
protected override commandConfiguration: CommandConfiguration = {
allowUnauthenticated: true,
canRunOutsideProject: true,
};
async runAsync(): Promise<void> {
const user = await getUserAsync();
if (user) {
+5 -1
View File
@@ -1,4 +1,4 @@
import EasCommand from '../commandUtils/EasCommand';
import EasCommand, { CommandConfiguration } from '../commandUtils/EasCommand';
import Log from '../log';
import UserSettings from '../user/UserSettings';
@@ -7,6 +7,10 @@ export default class AnalyticsView extends EasCommand {
static override args = [{ name: 'STATUS', options: ['on', 'off'] }];
protected override commandConfiguration: CommandConfiguration = {
allowUnauthenticated: true,
};
async runAsync(): Promise<void> {
const { STATUS: status } = (await this.parse(AnalyticsView)).args;
if (status) {
@@ -2,7 +2,7 @@ import chalk from 'chalk';
import gql from 'graphql-tag';
import { getDefaultBranchNameAsync } from '../../branch/utils';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
@@ -62,7 +62,7 @@ export default class BranchCreate extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
@@ -70,9 +70,7 @@ export default class BranchCreate extends EasCommand {
args: { name },
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
} = await this.parse(BranchCreate);
const {
projectConfig: { projectId },
} = await this.getContextAsync(BranchCreate, {
const { projectId } = await this.getContextAsync(BranchCreate, {
nonInteractive,
});
@@ -2,7 +2,7 @@ import chalk from 'chalk';
import gql from 'graphql-tag';
import { selectBranchOnAppAsync } from '../../branch/queries';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
@@ -77,7 +77,7 @@ export default class BranchDelete extends EasCommand {
static override description = 'delete a branch';
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
static override args = [
@@ -104,9 +104,7 @@ export default class BranchDelete extends EasCommand {
enableJsonOutput();
}
const {
projectConfig: { projectId },
} = await this.getContextAsync(BranchDelete, { nonInteractive });
const { projectId } = await this.getContextAsync(BranchDelete, { nonInteractive });
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
if (!branchName) {
+3 -5
View File
@@ -1,5 +1,5 @@
import { listAndRenderBranchesOnAppAsync } from '../../branch/queries';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination';
import { enableJsonOutput } from '../../utils/json';
@@ -13,14 +13,12 @@ export default class BranchList extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(BranchList);
const {
projectConfig: { projectId },
} = await this.getContextAsync(BranchList, {
const { projectId } = await this.getContextAsync(BranchList, {
nonInteractive: flags['non-interactive'],
});
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
@@ -2,7 +2,7 @@ import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
@@ -63,16 +63,14 @@ export default class BranchRename extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
let {
flags: { json: jsonFlag, from: currentName, to: newName, 'non-interactive': nonInteractive },
} = await this.parse(BranchRename);
const {
projectConfig: { projectId },
} = await this.getContextAsync(BranchRename, {
const { projectId } = await this.getContextAsync(BranchRename, {
nonInteractive,
});
if (jsonFlag) {
+3 -5
View File
@@ -1,5 +1,5 @@
import { selectBranchOnAppAsync } from '../../branch/queries';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import {
EasPaginatedQueryFlags,
@@ -27,7 +27,7 @@ export default class BranchView extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
@@ -36,9 +36,7 @@ export default class BranchView extends EasCommand {
flags,
} = await this.parse(BranchView);
const { 'non-interactive': nonInteractive } = flags;
const {
projectConfig: { projectId },
} = await this.getContextAsync(BranchView, {
const { projectId } = await this.getContextAsync(BranchView, {
nonInteractive,
});
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
@@ -1,7 +1,7 @@
import chalk from 'chalk';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
@@ -129,7 +129,7 @@ export default class BuildCancel extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
@@ -137,9 +137,7 @@ export default class BuildCancel extends EasCommand {
args: { BUILD_ID: buildIdFromArg },
flags: { 'non-interactive': nonInteractive },
} = await this.parse(BuildCancel);
const {
projectConfig: { projectId },
} = await this.getContextAsync(BuildCancel, {
const { projectId } = await this.getContextAsync(BuildCancel, {
nonInteractive,
});
@@ -4,10 +4,11 @@ import chalk from 'chalk';
import { cleanUpOldEasBuildGradleScriptAsync } from '../../build/android/syncProjectConfiguration';
import { ensureProjectConfiguredAsync } from '../../build/configure';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import Log, { learnMore } from '../../log';
import { RequestedPlatform } from '../../platform';
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
@@ -26,14 +27,12 @@ export default class BuildConfigure extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildConfigure);
const {
projectConfig: { exp, projectId, projectDir },
} = await this.getContextAsync(BuildConfigure, {
const { projectId } = await this.getContextAsync(BuildConfigure, {
nonInteractive: false,
});
@@ -43,6 +42,7 @@ export default class BuildConfigure extends EasCommand {
await getVcsClient().ensureRepoExistsAsync();
const projectDir = await findProjectRootAsync();
const expoUpdatesIsInstalled = isExpoUpdatesInstalled(projectDir);
const platform =
@@ -62,6 +62,8 @@ export default class BuildConfigure extends EasCommand {
// configure expo-updates
if (expoUpdatesIsInstalled) {
const exp = getExpoConfig(projectDir);
if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
+11 -15
View File
@@ -3,16 +3,19 @@ import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import figures from 'figures';
import fs from 'fs-extra';
import path from 'path';
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import { UserInputResourceClass } from '../../build/types';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { StatuspageServiceName } from '../../graphql/generated';
import Log, { link } from '../../log';
import { RequestedPlatform, selectRequestedPlatformAsync } from '../../platform';
import { findProjectRootAsync } from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
import uniq from '../../utils/expodash/uniq';
import { enableJsonOutput } from '../../utils/json';
@@ -97,9 +100,8 @@ export default class Build extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...EASCommandLoggedInContext,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
@@ -110,10 +112,12 @@ export default class Build extends EasCommand {
}
const flags = this.sanitizeFlags(rawFlags);
const { actor, getDynamicProjectConfigAsync, projectDir } = await this.getContextAsync(Build, {
const { actor, projectId } = await this.getContextAsync(Build, {
nonInteractive: flags.nonInteractive,
});
const projectDir = await findProjectRootAsync();
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
if (!flags.localBuildOptions.enable) {
@@ -126,12 +130,7 @@ export default class Build extends EasCommand {
const flagsWithPlatform = await this.ensurePlatformSelectedAsync(flags);
await runBuildAndSubmitAsync(
projectDir,
flagsWithPlatform,
actor,
getDynamicProjectConfigAsync
);
await runBuildAndSubmitAsync(projectDir, flagsWithPlatform, actor, projectId);
}
private sanitizeFlags(
@@ -229,9 +228,6 @@ async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
if (!(await fs.pathExists(EasJsonAccessor.formatEasJsonPath(projectDir)))) {
return;
}
const easJsonAccessor = new EasJsonAccessor(projectDir);
const profileNames = await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor);
const platformAndProfileNames: [Platform, string][] = profileNames.flatMap(profileName => [
+12 -11
View File
@@ -5,10 +5,14 @@ import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../../commandUtils/EasCommand';
import Log from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import { findProjectRootAsync } from '../../project/projectUtils';
import { getTmpDirectory } from '../../utils/paths';
import { getVcsClient } from '../../vcs';
@@ -62,19 +66,15 @@ export default class BuildInspect extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...EASCommandLoggedInContext,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildInspect);
const { actor, getDynamicProjectConfigAsync, projectDir } = await this.getContextAsync(
BuildInspect,
{
nonInteractive: false,
}
);
const { actor, projectId } = await this.getContextAsync(BuildInspect, {
nonInteractive: false,
});
const outputDirectory = path.resolve(process.cwd(), flags.output);
const tmpWorkingdir = path.join(getTmpDirectory(), uuidv4());
@@ -93,6 +93,7 @@ export default class BuildInspect extends EasCommand {
await vcs.makeShallowCopyAsync(tmpWorkingdir);
await this.copyToOutputDirAsync(tmpWorkingdir, outputDirectory);
} else {
const projectDir = await findProjectRootAsync();
try {
await runBuildAndSubmitAsync(
projectDir,
@@ -114,7 +115,7 @@ export default class BuildInspect extends EasCommand {
},
},
actor,
getDynamicProjectConfigAsync
projectId
);
if (!flags.verbose) {
Log.log(chalk.green('Build successful'));
+3 -5
View File
@@ -2,7 +2,7 @@ import { Flags } from '@oclif/core';
import { BUILDS_LIMIT, listAndRenderBuildsOnAppAsync } from '../../build/queries';
import { BuildDistributionType, BuildStatus } from '../../build/types';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import {
EasPaginatedQueryFlags,
@@ -56,7 +56,7 @@ export default class BuildList extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
@@ -69,9 +69,7 @@ export default class BuildList extends EasCommand {
distribution: buildDistribution,
'non-interactive': nonInteractive,
} = flags;
const {
projectConfig: { projectId },
} = await this.getContextAsync(BuildList, {
const { projectId } = await this.getContextAsync(BuildList, {
nonInteractive,
});
if (jsonFlag) {
@@ -4,7 +4,10 @@ import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../../commandUtils/EasCommand';
import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../../../commandUtils/EasCommand';
import { AppVersionMutation } from '../../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
import { toAppPlatform } from '../../../graphql/types/AppPlatform';
@@ -12,8 +15,12 @@ import Log from '../../../log';
import { selectPlatformAsync } from '../../../platform';
import { VERSION_CODE_REQUIREMENTS, isValidVersionCode } from '../../../project/android/versions';
import { getApplicationIdentifierAsync } from '../../../project/applicationIdentifier';
import { getExpoConfig } from '../../../project/expoConfig';
import { BUILD_NUMBER_REQUIREMENTS, isValidBuildNumber } from '../../../project/ios/versions';
import { getDisplayNameForProjectIdAsync } from '../../../project/projectUtils';
import {
findProjectRootAsync,
getDisplayNameForProjectIdAsync,
} from '../../../project/projectUtils';
import {
ensureVersionSourceIsRemoteAsync,
getBuildVersionName,
@@ -38,19 +45,17 @@ export default class BuildVersionSetView extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...EASCommandLoggedInContext,
...EASCommandProjectIdContext,
};
public async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildVersionSetView);
const { actor, getDynamicProjectConfigAsync, projectDir } = await this.getContextAsync(
BuildVersionSetView,
{
nonInteractive: false,
}
);
const { actor, projectId } = await this.getContextAsync(BuildVersionSetView, {
nonInteractive: false,
});
const projectDir = await findProjectRootAsync();
const platform = await selectPlatformAsync(flags.platform);
const easJsonAccessor = new EasJsonAccessor(projectDir);
@@ -61,7 +66,7 @@ export default class BuildVersionSetView extends EasCommand {
flags.profile ?? undefined
);
const { exp, projectId } = await getDynamicProjectConfigAsync({ env: profile.env });
const exp = getExpoConfig(projectDir, { env: profile.env });
const displayName = await getDisplayNameForProjectIdAsync(projectId);
validateAppConfigForRemoteVersionSource(exp, platform);
@@ -6,7 +6,10 @@ import chalk from 'chalk';
import { updateNativeVersionsAsync as updateAndroidNativeVersionsAsync } from '../../../build/android/version';
import { updateNativeVersionsAsync as updateIosNativeVersionsAsync } from '../../../build/ios/version';
import EasCommand from '../../../commandUtils/EasCommand';
import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../../../commandUtils/EasCommand';
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
import { toAppPlatform } from '../../../graphql/types/AppPlatform';
import Log from '../../../log';
@@ -18,9 +21,11 @@ import {
import { getAppBuildGradleAsync } from '../../../project/android/gradleUtils';
import { VERSION_CODE_REQUIREMENTS, isValidVersionCode } from '../../../project/android/versions';
import { getApplicationIdentifierAsync } from '../../../project/applicationIdentifier';
import { getExpoConfig } from '../../../project/expoConfig';
import { resolveXcodeBuildContextAsync } from '../../../project/ios/scheme';
import { resolveTargetsAsync } from '../../../project/ios/target';
import { BUILD_NUMBER_REQUIREMENTS, isValidBuildNumber } from '../../../project/ios/versions';
import { findProjectRootAsync } from '../../../project/projectUtils';
import {
ensureVersionSourceIsRemoteAsync,
getBuildVersionName,
@@ -55,19 +60,16 @@ export default class BuildVersionSyncView extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...EASCommandLoggedInContext,
...EASCommandProjectIdContext,
};
public async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildVersionSyncView);
const { actor, getDynamicProjectConfigAsync, projectDir } = await this.getContextAsync(
BuildVersionSyncView,
{
nonInteractive: true,
}
);
const { actor, projectId } = await this.getContextAsync(BuildVersionSyncView, {
nonInteractive: true,
});
const projectDir = await findProjectRootAsync();
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
const easJsonAccessor = new EasJsonAccessor(projectDir);
@@ -81,9 +83,7 @@ export default class BuildVersionSyncView extends EasCommand {
profileName: flags.profile ?? undefined,
});
for (const profileInfo of buildProfiles) {
const { exp, projectId } = await getDynamicProjectConfigAsync({
env: profileInfo.profile.env,
});
const exp = getExpoConfig(projectDir, { env: profileInfo.profile.env });
validateAppConfigForRemoteVersionSource(exp, profileInfo.platform);
const platformDisplayName = appPlatformDisplayNames[toAppPlatform(profileInfo.platform)];
+3 -5
View File
@@ -1,5 +1,5 @@
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasJsonOnlyFlag } from '../../commandUtils/flags';
import { BuildFragment } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
@@ -18,7 +18,7 @@ export default class BuildView extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
@@ -26,9 +26,7 @@ export default class BuildView extends EasCommand {
args: { BUILD_ID: buildId },
flags,
} = await this.parse(BuildView);
const {
projectConfig: { projectId },
} = await this.getContextAsync(BuildView, {
const { projectId } = await this.getContextAsync(BuildView, {
nonInteractive: true,
});
if (flags.json) {
@@ -2,7 +2,7 @@ import chalk from 'chalk';
import gql from 'graphql-tag';
import { BranchNotFoundError } from '../../branch/utils';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
@@ -71,7 +71,7 @@ export default class ChannelCreate extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
@@ -79,9 +79,7 @@ export default class ChannelCreate extends EasCommand {
args: { name: channelName },
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
} = await this.parse(ChannelCreate);
const {
projectConfig: { projectId },
} = await this.getContextAsync(ChannelCreate, {
const { projectId } = await this.getContextAsync(ChannelCreate, {
nonInteractive,
});
if (jsonFlag) {
@@ -1,7 +1,7 @@
import gql from 'graphql-tag';
import { selectChannelOnAppAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
@@ -30,7 +30,7 @@ export default class ChannelDelete extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
@@ -38,9 +38,7 @@ export default class ChannelDelete extends EasCommand {
args: { name: nameArg },
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
} = await this.parse(ChannelDelete);
const {
projectConfig: { projectId },
} = await this.getContextAsync(ChannelDelete, {
const { projectId } = await this.getContextAsync(ChannelDelete, {
nonInteractive,
});
if (jsonFlag) {
@@ -4,7 +4,7 @@ import gql from 'graphql-tag';
import { selectBranchOnAppAsync } from '../../branch/queries';
import { selectChannelOnAppAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
@@ -66,7 +66,7 @@ export default class ChannelEdit extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
@@ -74,9 +74,7 @@ export default class ChannelEdit extends EasCommand {
args,
flags: { branch: branchFlag, json, 'non-interactive': nonInteractive },
} = await this.parse(ChannelEdit);
const {
projectConfig: { projectId },
} = await this.getContextAsync(ChannelEdit, {
const { projectId } = await this.getContextAsync(ChannelEdit, {
nonInteractive,
});
if (json) {
@@ -1,5 +1,5 @@
import { CHANNELS_LIMIT, listAndRenderChannelsOnAppAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import {
EasPaginatedQueryFlags,
@@ -18,16 +18,14 @@ export default class ChannelList extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(ChannelList);
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
const {
projectConfig: { projectId },
} = await this.getContextAsync(ChannelList, {
const { projectId } = await this.getContextAsync(ChannelList, {
nonInteractive,
});
if (jsonFlag) {
@@ -2,7 +2,7 @@ import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { BranchMapping, getBranchMapping } from '../../channel/utils';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { UpdateBranch } from '../../graphql/generated';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
@@ -312,7 +312,7 @@ export default class ChannelRollout extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
@@ -326,9 +326,7 @@ export default class ChannelRollout extends EasCommand {
'non-interactive': nonInteractive,
},
} = await this.parse(ChannelRollout);
const {
projectConfig: { projectId },
} = await this.getContextAsync(ChannelRollout, {
const { projectId } = await this.getContextAsync(ChannelRollout, {
nonInteractive,
});
if (jsonFlag) {
@@ -4,7 +4,7 @@ import {
listAndRenderBranchesAndUpdatesOnChannelAsync,
selectChannelOnAppAsync,
} from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination';
import { enableJsonOutput } from '../../utils/json';
@@ -26,7 +26,7 @@ export default class ChannelView extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
@@ -36,9 +36,7 @@ export default class ChannelView extends EasCommand {
} = await this.parse(ChannelView);
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
const {
projectConfig: { projectId },
} = await this.getContextAsync(ChannelView, {
const { projectId } = await this.getContextAsync(ChannelView, {
nonInteractive,
});
if (jsonFlag) {
+13 -13
View File
@@ -4,17 +4,19 @@ import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../commandUtils/EasCommand';
import EasCommand, { CommandConfiguration } from '../commandUtils/EasCommand';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log from '../log';
import { appPlatformEmojis } from '../platform';
import { getExpoConfig } from '../project/expoConfig';
import { findProjectRootAsync } from '../project/projectUtils';
import { selectAsync } from '../prompts';
export default class Config extends EasCommand {
static override description = 'display project configuration (app.json + eas.json)';
static override flags = {
platform: Flags.enum<Platform>({ char: 'p', options: [Platform.ANDROID, Platform.IOS] }),
platform: Flags.enum({ char: 'p', options: ['android', 'ios'] }),
profile: Flags.string({
char: 'e',
description:
@@ -23,17 +25,18 @@ export default class Config extends EasCommand {
}),
};
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
protected override commandConfiguration: CommandConfiguration = {
allowUnauthenticated: true,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(Config);
const { platform: maybePlatform, profile: maybeProfile } = flags;
const { getDynamicProjectConfigAsync, projectDir } = await this.getContextAsync(Config, {
nonInteractive: false,
});
const { platform: maybePlatform, profile: maybeProfile } = flags as {
platform?: Platform;
profile?: string;
};
const projectDir = await findProjectRootAsync();
const accessor = new EasJsonAccessor(projectDir);
const profileName =
@@ -61,10 +64,7 @@ export default class Config extends EasCommand {
]));
const profile = await EasJsonUtils.getBuildProfileAsync(accessor, platform, profileName);
const { exp: config } = await getDynamicProjectConfigAsync({
env: profile.env,
isPublicConfig: true,
});
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
Log.addNewLineIfNone();
Log.log(chalk.bold(getProjectConfigDescription(projectDir)));
+13 -7
View File
@@ -1,7 +1,12 @@
import { Flags } from '@oclif/core';
import EasCommand from '../commandUtils/EasCommand';
import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../commandUtils/EasCommand';
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
import { getExpoConfig } from '../project/expoConfig';
import { findProjectRootAsync } from '../project/projectUtils';
export default class Credentials extends EasCommand {
static override description = 'manage credentials';
@@ -11,16 +16,17 @@ export default class Credentials extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.OptionalProjectConfig,
...EASCommandLoggedInContext,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(Credentials);
const { actor, projectConfig } = await this.getContextAsync(Credentials, {
nonInteractive: false,
});
const { actor, projectId } = await this.getContextAsync(Credentials, { nonInteractive: false });
await new SelectPlatform(actor, projectConfig ?? null, flags.platform).runAsync();
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
await new SelectPlatform(actor, { projectId, exp }, flags.platform).runAsync();
}
}
@@ -1,4 +1,7 @@
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../../commandUtils/EasCommand';
import AppStoreApi from '../../credentials/ios/appstore/AppStoreApi';
import { createContextAsync } from '../../devices/context';
import DeviceManager from '../../devices/manager';
@@ -7,21 +10,17 @@ export default class DeviceCreate extends EasCommand {
static override description = 'register new Apple Devices to use for internal distribution';
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.OptionalProjectConfig,
...EASCommandLoggedInContext,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
// this command is interactive by design
const { actor, projectConfig } = await this.getContextAsync(DeviceCreate, {
const { actor, projectId } = await this.getContextAsync(DeviceCreate, {
nonInteractive: false,
});
const ctx = await createContextAsync({
appStore: new AppStoreApi(),
user: actor,
projectId: projectConfig?.projectId,
});
const ctx = await createContextAsync({ appStore: new AppStoreApi(), user: actor, projectId });
const manager = new DeviceManager(ctx);
await manager.createAsync();
}
+177 -95
View File
@@ -2,161 +2,151 @@ import { Device, DeviceStatus } from '@expo/apple-utils';
import { Flags } from '@oclif/core';
import assert from 'assert';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { PaginatedQueryOptions, getPaginatedQueryOptions } from '../../commandUtils/pagination';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { chooseDevicesToDeleteAsync } from '../../credentials/ios/actions/DeviceUtils';
import { AppleDeviceMutation } from '../../credentials/ios/api/graphql/mutations/AppleDeviceMutation';
import {
AppleDeviceQuery,
AppleDeviceQueryResult,
AppleDevicesByTeamIdentifierQueryResult,
} from '../../credentials/ios/api/graphql/queries/AppleDeviceQuery';
import { AppleTeamQuery } from '../../credentials/ios/api/graphql/queries/AppleTeamQuery';
import { authenticateAsync, getRequestContext } from '../../credentials/ios/appstore/authenticate';
import {
selectAppleDeviceOnAppleTeamAsync,
selectAppleTeamOnAccountAsync,
} from '../../devices/queries';
import formatDevice from '../../devices/utils/formatDevice';
import { AppleDevice, Maybe } from '../../graphql/generated';
import Log from '../../log';
import { ora } from '../../ora';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { toggleConfirmAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
export default class DeviceDelete extends EasCommand {
static override description = 'remove a registered device from your account';
static override flags = {
'apple-team-id': Flags.string({ description: 'The Apple team ID on which to find the device' }),
udid: Flags.string({ description: 'The Apple device ID to disable' }),
...EasNonInteractiveAndJsonFlags,
'apple-team-id': Flags.string(),
udid: Flags.string({ multiple: true }),
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(DeviceDelete);
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
let { 'apple-team-id': appleTeamIdentifier, udid } = flags;
const {
projectConfig: { projectId },
} = await this.getContextAsync(DeviceDelete, {
nonInteractive: paginatedQueryOptions.nonInteractive,
let {
flags: { 'apple-team-id': appleTeamIdentifier, udid: udids },
} = await this.parse(DeviceDelete);
const { projectId } = await this.getContextAsync(DeviceDelete, {
nonInteractive: false,
});
const account = await getOwnerAccountForProjectIdAsync(projectId);
let appleTeamName;
if (paginatedQueryOptions.json) {
enableJsonOutput();
}
const account = await getOwnerAccountForProjectIdAsync(projectId);
if (!appleTeamIdentifier) {
const appleTeam = await selectAppleTeamOnAccountAsync({
accountName: account.name,
selectionPromptTitle: `What Apple team would you like to list devices for?`,
paginatedQueryOptions,
});
appleTeamIdentifier = appleTeam.appleTeamIdentifier;
appleTeamName = appleTeam.appleTeamName;
appleTeamIdentifier = await this.askForAppleTeamAsync(account.name);
}
assert(appleTeamIdentifier, 'No team identifier is specified');
const chosenDevice = udid
? await AppleDeviceQuery.getByDeviceIdentifierAsync(account.name, udid)
: await selectAppleDeviceOnAppleTeamAsync({
accountName: account.name,
appleTeamIdentifier,
selectionPromptTitle: `Which device would you like to disable?`,
paginatedQueryOptions,
});
const appleDevicesResult = await this.getDevicesForTeamAsync(account.name, appleTeamIdentifier);
this.logChosenDevice(chosenDevice, appleTeamName, appleTeamIdentifier, paginatedQueryOptions);
if (!(await this.shouldRemoveDeviceFromExpoAsync(paginatedQueryOptions))) {
if (!appleDevicesResult) {
return;
}
await this.removeDeviceFromExpoAsync(chosenDevice);
const { appleTeamName, appleDevices } = appleDevicesResult;
if (await this.shouldDisableDeviceOnAppleAsync(paginatedQueryOptions)) {
await this.disableDeviceOnAppleAsync(chosenDevice, appleTeamIdentifier);
}
}
const chosenDevices = await this.chooseDevicesToDeleteAsync(appleDevices, udids);
async shouldDisableDeviceOnAppleAsync({
nonInteractive,
}: PaginatedQueryOptions): Promise<boolean> {
if (!nonInteractive) {
if (chosenDevices.length === 0) {
Log.newLine();
return await toggleConfirmAsync({
message: 'Do you want to disable this device on your Apple account as well?',
});
Log.warn('No devices were chosen to be removed.');
return;
}
return true;
this.logChosenDevices(chosenDevices, appleTeamName, appleTeamIdentifier);
const hasRemoved = await this.askAndRemoveFromExpoAsync(chosenDevices);
if (!hasRemoved) {
return;
}
await this.askAndDisableOnAppleAsync(chosenDevices, appleTeamIdentifier);
}
async disableDeviceOnAppleAsync(
device: AppleDevice | AppleDeviceQueryResult,
async askAndDisableOnAppleAsync(
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
appleTeamIdentifier: string
): Promise<void> {
Log.newLine();
const deleteOnApple = await toggleConfirmAsync({
message: 'Do you want to disable these devices on your Apple account as well?',
});
if (!deleteOnApple) {
return;
}
const ctx = await authenticateAsync({ teamId: appleTeamIdentifier });
const context = getRequestContext(ctx);
Log.addNewLineIfNone();
const removeAppleSpinner = ora('Disabling device on Apple').start();
const removeAppleSpinner = ora('Disabling devices on Apple').start();
try {
const appleValidatedDevices = await Device.getAsync(context);
const appleValidatedDevice = appleValidatedDevices.find(d => d.id === device.id);
if (appleValidatedDevice) {
await appleValidatedDevice.updateAsync({ status: DeviceStatus.DISABLED });
const chosenDeviceIdentifiers = chosenDevices.map(cd => cd.identifier);
const allDevices = await Device.getAsync(context);
const realDevices = allDevices.filter(d =>
chosenDeviceIdentifiers.includes(d.attributes.udid)
);
for (const device of realDevices) {
await device.updateAsync({ status: DeviceStatus.DISABLED });
}
removeAppleSpinner.succeed('Disabled device on Apple');
removeAppleSpinner.succeed('Disabled devices on Apple');
} catch (err) {
removeAppleSpinner.fail();
throw err;
}
}
async shouldRemoveDeviceFromExpoAsync({
nonInteractive,
}: PaginatedQueryOptions): Promise<boolean> {
if (!nonInteractive) {
Log.warn(`You are about to remove the Apple device listed above from your Expo account.`);
Log.newLine();
async askAndRemoveFromExpoAsync(
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[]
): Promise<boolean> {
Log.warn(
`You are about to remove the Apple device${
chosenDevices.length > 1 ? 's' : ''
} listed above from your Expo account.`
);
Log.newLine();
return await toggleConfirmAsync({
message: 'Are you sure you wish to proceed?',
});
const confirmed = await toggleConfirmAsync({
message: 'Are you sure you wish to proceed?',
});
if (confirmed) {
const removalSpinner = ora(`Removing Apple devices on Expo`).start();
try {
for (const chosenDevice of chosenDevices) {
await AppleDeviceMutation.deleteAppleDeviceAsync(chosenDevice.id);
}
removalSpinner.succeed('Removed Apple devices from Expo');
} catch (err) {
removalSpinner.fail();
throw err;
}
}
return true;
return confirmed;
}
async removeDeviceFromExpoAsync(
chosenDevice: AppleDevice | AppleDeviceQueryResult
): Promise<void> {
const removalSpinner = ora(`Removing Apple device on Expo`).start();
try {
await AppleDeviceMutation.deleteAppleDeviceAsync(chosenDevice.id);
removalSpinner.succeed('Removed Apple device from Expo');
} catch (err) {
removalSpinner.fail();
throw err;
}
}
logChosenDevice(
device: AppleDevice | AppleDeviceQueryResult,
logChosenDevices(
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
appleTeamName: Maybe<string> | undefined,
appleTeamIdentifier: string,
{ json }: PaginatedQueryOptions
appleTeamIdentifier: string
): void {
if (json) {
printJsonOnlyOutput(device);
} else {
Log.addNewLineIfNone();
Log.addNewLineIfNone();
chosenDevices.forEach(device => {
Log.log(
formatDevice(device, {
appleTeamName,
@@ -164,6 +154,98 @@ export default class DeviceDelete extends EasCommand {
})
);
Log.newLine();
});
}
async chooseDevicesToDeleteAsync(
appleDevices: AppleDeviceQueryResult[],
udids: string[]
): Promise<(AppleDevice | AppleDeviceQueryResult)[]> {
let chosenDevices: (AppleDeviceQueryResult | AppleDevice)[] = [];
Log.newLine();
if (udids) {
udids.forEach(udid => {
const foundDevice = appleDevices.find(device => device.identifier === udid);
if (foundDevice) {
chosenDevices.push(foundDevice);
} else {
Log.warn(`No device found with UDID ${udid}.`);
}
});
}
if (chosenDevices.length === 0) {
Log.addNewLineIfNone();
chosenDevices = await chooseDevicesToDeleteAsync(appleDevices);
Log.newLine();
}
return chosenDevices;
}
async getDevicesForTeamAsync(
accountName: string,
appleTeamIdentifier: string
): Promise<AppleDevicesByTeamIdentifierQueryResult | undefined> {
const devicesSpinner = ora().start('Fetching the list of devices for the team…');
try {
const result = await AppleDeviceQuery.getAllForAppleTeamAsync(
accountName,
appleTeamIdentifier
);
if (result?.appleDevices.length) {
const { appleTeamName, appleDevices } = result;
devicesSpinner.succeed(
`Found ${appleDevices.length} devices for team ${appleTeamName ?? appleTeamIdentifier}`
);
return result;
} else {
devicesSpinner.fail(`Couldn't find any devices for the team ${appleTeamIdentifier}`);
return;
}
} catch (e) {
devicesSpinner.fail(`Something went wrong and we couldn't fetch the device list`);
throw e;
}
}
async askForAppleTeamAsync(accountName: string): Promise<string | undefined> {
const teamSpinner = ora().start('Fetching the list of teams for the project…');
try {
const teams = await AppleTeamQuery.getAllForAccountAsync(accountName);
if (teams.length > 0) {
teamSpinner.succeed('Fetched the list of teams for the project');
if (teams.length === 1) {
return teams[0].appleTeamIdentifier;
}
const result = await promptAsync({
type: 'select',
name: 'appleTeamIdentifier',
message: 'What Apple Team would you like to list devices for?',
choices: teams.map(team => ({
title: team.appleTeamName
? `${team.appleTeamName} (ID: ${team.appleTeamIdentifier})`
: team.appleTeamIdentifier,
value: team.appleTeamIdentifier,
})),
});
return result.appleTeamIdentifier;
} else {
teamSpinner.fail(`Couldn't find any teams for the account ${accountName}`);
return;
}
} catch (e) {
teamSpinner.fail(`Something went wrong and we couldn't fetch the list of teams`);
throw e;
}
}
}
+76 -42
View File
@@ -1,68 +1,102 @@
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination';
import {
listAndRenderAppleDevicesOnAppleTeamAsync,
selectAppleTeamOnAccountAsync,
} from '../../devices/queries';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { AppleDeviceQuery } from '../../credentials/ios/api/graphql/queries/AppleDeviceQuery';
import { AppleTeamQuery } from '../../credentials/ios/api/graphql/queries/AppleTeamQuery';
import formatDevice from '../../devices/utils/formatDevice';
import Log from '../../log';
import { Ora, ora } from '../../ora';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { enableJsonOutput } from '../../utils/json';
import { promptAsync } from '../../prompts';
export default class BuildList extends EasCommand {
static override description = 'list all registered devices for your account';
static override flags = {
'apple-team-id': Flags.string(),
...EasPaginatedQueryFlags,
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildList);
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
const {
projectConfig: { projectId },
} = await this.getContextAsync(BuildList, {
nonInteractive: paginatedQueryOptions.nonInteractive,
let appleTeamIdentifier = (await this.parse(BuildList)).flags['apple-team-id'];
const { projectId } = await this.getContextAsync(BuildList, {
nonInteractive: false,
});
let appleTeamIdentifier = flags['apple-team-id'];
let appleTeamName;
if (paginatedQueryOptions.json) {
enableJsonOutput();
}
const account = await getOwnerAccountForProjectIdAsync(projectId);
// if they don't provide a team id, fetch devices on their account
let spinner: Ora;
if (!appleTeamIdentifier) {
const selectedAppleTeam = await selectAppleTeamOnAccountAsync({
accountName: account.name,
paginatedQueryOptions: {
...paginatedQueryOptions,
offset: 0,
limit: undefined,
},
selectionPromptTitle: 'What Apple team would you like to list devices for?',
});
appleTeamIdentifier = selectedAppleTeam.appleTeamIdentifier;
appleTeamName = selectedAppleTeam.appleTeamName;
spinner = ora().start('Fetching the list of teams for the project…');
try {
const teams = await AppleTeamQuery.getAllForAccountAsync(account.name);
if (teams.length > 0) {
spinner.succeed();
if (teams.length === 1) {
appleTeamIdentifier = teams[0].appleTeamIdentifier;
} else {
const result = await promptAsync({
type: 'select',
name: 'appleTeamIdentifier',
message: 'What Apple Team would you like to list devices for?',
choices: teams.map(team => ({
title: team.appleTeamName
? `${team.appleTeamName} (ID: ${team.appleTeamIdentifier})`
: team.appleTeamIdentifier,
value: team.appleTeamIdentifier,
})),
});
appleTeamIdentifier = result.appleTeamIdentifier;
}
} else {
spinner.fail(`Couldn't find any teams for the account ${account.name}`);
}
} catch (e) {
spinner.fail(`Something went wrong and we couldn't fetch the list of teams`);
throw e;
}
}
assert(appleTeamIdentifier, 'No team identifier is specified');
await listAndRenderAppleDevicesOnAppleTeamAsync({
accountName: account.name,
appleTeam: {
appleTeamIdentifier,
appleTeamName,
},
paginatedQueryOptions,
});
spinner = ora().start('Fetching the list of devices for the team…');
try {
const result = await AppleDeviceQuery.getAllForAppleTeamAsync(
account.name,
appleTeamIdentifier
);
if (result?.appleDevices.length) {
const { appleTeamName, appleDevices } = result;
spinner.succeed(
`Found ${appleDevices.length} devices for team ${appleTeamName ?? appleTeamIdentifier}`
);
const list = appleDevices
.map(device =>
formatDevice(device, { appleTeamName, appleTeamIdentifier: appleTeamIdentifier! })
)
.join(`\n\n${chalk.dim('———')}\n\n`);
Log.log(`\n${list}`);
} else {
spinner.fail(`Couldn't find any devices for the team ${appleTeamIdentifier}`);
}
} catch (e) {
spinner.fail(`Something went wrong and we couldn't fetch the device list`);
throw e;
}
}
}
+3 -5
View File
@@ -1,4 +1,4 @@
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { AppleDeviceQuery } from '../../credentials/ios/api/graphql/queries/AppleDeviceQuery';
import formatDevice from '../../devices/utils/formatDevice';
import Log from '../../log';
@@ -11,7 +11,7 @@ export default class DeviceView extends EasCommand {
static override args = [{ name: 'UDID' }];
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
@@ -30,9 +30,7 @@ If you are not sure what is the UDID of the device you are looking for, run:
);
throw new Error('Device UDID is missing');
}
const {
projectConfig: { projectId },
} = await this.getContextAsync(DeviceView, {
const { projectId } = await this.getContextAsync(DeviceView, {
nonInteractive: true,
});
const account = await getOwnerAccountForProjectIdAsync(projectId);
+7 -9
View File
@@ -1,23 +1,20 @@
import { Platform } from '@expo/eas-build-job';
import envinfo from 'envinfo';
import EasCommand from '../commandUtils/EasCommand';
import EasCommand, { CommandConfiguration } from '../commandUtils/EasCommand';
import Log from '../log';
import { findProjectRootAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { easCliVersion } from '../utils/easCli';
export default class Diagnostics extends EasCommand {
static override description = 'display environment info';
static override contextDefinition = {
...this.ContextOptions.ProjectDir,
protected override commandConfiguration: CommandConfiguration = {
allowUnauthenticated: true,
};
async runAsync(): Promise<void> {
const { projectDir } = await this.getContextAsync(Diagnostics, {
nonInteractive: true,
});
const info = await envinfo.run(
{
System: ['OS', 'Shell'],
@@ -43,11 +40,12 @@ export default class Diagnostics extends EasCommand {
);
Log.log(info.trimEnd());
await this.printWorkflowAsync(projectDir);
await this.printWorkflowAsync();
Log.newLine();
}
private async printWorkflowAsync(projectDir: string): Promise<void> {
private async printWorkflowAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const androidWorkflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const iosWorkflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
+12 -7
View File
@@ -1,14 +1,19 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import path from 'path';
import { ensureProjectConfiguredAsync } from '../../build/configure';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../../commandUtils/EasCommand';
import { CredentialsContext } from '../../credentials/context';
import Log, { learnMore } from '../../log';
import { createMetadataContextAsync } from '../../metadata/context';
import { downloadMetadataAsync } from '../../metadata/download';
import { handleMetadataError } from '../../metadata/errors';
import { findProjectRootAsync } from '../../project/projectUtils';
export default class MetadataPull extends EasCommand {
static override description = 'generate the local store configuration from the app stores';
@@ -22,21 +27,21 @@ export default class MetadataPull extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...EASCommandProjectIdContext,
...EASCommandLoggedInContext,
};
async runAsync(): Promise<void> {
Log.warn('EAS Metadata is in beta and subject to breaking changes.');
const { flags } = await this.parse(MetadataPull);
const {
actor,
projectConfig: { exp, projectId, projectDir },
} = await this.getContextAsync(MetadataPull, {
const { actor, projectId } = await this.getContextAsync(MetadataPull, {
nonInteractive: false,
});
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
// this command is interactive (all nonInteractive flags passed to utility functions are false)
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
+12 -7
View File
@@ -1,12 +1,17 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import { ensureProjectConfiguredAsync } from '../../build/configure';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../../commandUtils/EasCommand';
import { CredentialsContext } from '../../credentials/context';
import Log, { learnMore } from '../../log';
import { createMetadataContextAsync } from '../../metadata/context';
import { handleMetadataError } from '../../metadata/errors';
import { uploadMetadataAsync } from '../../metadata/upload';
import { findProjectRootAsync } from '../../project/projectUtils';
export default class MetadataPush extends EasCommand {
static override description = 'sync the local store configuration to the app stores';
@@ -20,21 +25,21 @@ export default class MetadataPush extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...EASCommandProjectIdContext,
...EASCommandLoggedInContext,
};
async runAsync(): Promise<void> {
Log.warn('EAS Metadata is in beta and subject to breaking changes.');
const { flags } = await this.parse(MetadataPush);
const {
actor,
projectConfig: { exp, projectId, projectDir },
} = await this.getContextAsync(MetadataPush, {
const { actor, projectId } = await this.getContextAsync(MetadataPush, {
nonInteractive: false,
});
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
// this command is interactive (all nonInteractive flags passed to utility functions are false)
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
+7 -6
View File
@@ -1,24 +1,25 @@
import openBrowserAsync from 'better-opn';
import { getProjectDashboardUrl } from '../build/utils/url';
import EasCommand from '../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../commandUtils/EasCommand';
import { ora } from '../ora';
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
import { getExpoConfig } from '../project/expoConfig';
import { findProjectRootAsync, getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
export default class Open extends EasCommand {
static override description = 'open the project page in a web browser';
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
// this command is interactive by nature (only really run by humans in a terminal)
const {
projectConfig: { projectId, exp },
} = await this.getContextAsync(Open, {
const { projectId } = await this.getContextAsync(Open, {
nonInteractive: false,
});
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const account = await getOwnerAccountForProjectIdAsync(projectId);
@@ -2,17 +2,17 @@ import { AppJSONConfig, PackageJSONConfig, getConfig } from '@expo/config';
import chalk from 'chalk';
import { vol } from 'memfs';
import ActorContextField from '../../../commandUtils/context/ActorContextField';
import ProjectDirContextField from '../../../commandUtils/context/ProjectDirContextField';
import { saveProjectIdToAppConfigAsync } from '../../../commandUtils/context/contextUtils/getProjectIdAsync';
import { jester } from '../../../credentials/__tests__/fixtures-constants';
import { AppMutation } from '../../../graphql/mutations/AppMutation';
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { findProjectRootAsync, saveProjectIdToAppConfigAsync } from '../../../project/projectUtils';
import { confirmAsync, promptAsync } from '../../../prompts';
import { ensureLoggedInAsync } from '../../../user/actions';
import ProjectInit from '../init';
jest.mock('fs');
jest.mock('@expo/config');
jest.mock('../../../project/projectUtils');
jest.mock('../../../prompts');
jest.mock('../../../user/actions');
jest.mock('../../../graphql/mutations/AppMutation');
@@ -22,7 +22,6 @@ jest.mock('../../../ora', () => ({
}),
}));
jest.mock('../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
let originalProcessArgv: string[];
@@ -73,14 +72,15 @@ function mockTestProject(options: {
const mockManifest = { exp: appJSON.expo };
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
jest.spyOn(ProjectDirContextField.prototype, 'getValueAsync').mockResolvedValue('/test-project');
jest.spyOn(ActorContextField.prototype, 'getValueAsync').mockResolvedValue(jester);
jest.mocked(findProjectRootAsync).mockResolvedValue('/test-project');
}
const commandOptions = { root: '/test-project' } as any;
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(ensureLoggedInAsync).mockResolvedValue(jester);
});
describe(ProjectInit.name, () => {
@@ -1,6 +1,6 @@
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { AppInfoQuery, AppInfoQueryVariables } from '../../graphql/generated';
import Log from '../../log';
@@ -33,13 +33,11 @@ export default class ProjectInfo extends EasCommand {
static override description = 'information about the current project';
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
const {
projectConfig: { projectId },
} = await this.getContextAsync(ProjectInfo, {
const { projectId } = await this.getContextAsync(ProjectInfo, {
nonInteractive: true,
});
+14 -14
View File
@@ -4,15 +4,18 @@ import nullthrows from 'nullthrows';
import terminalLink from 'terminal-link';
import { getProjectDashboardUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { saveProjectIdToAppConfigAsync } from '../../commandUtils/context/contextUtils/getProjectIdAsync';
import EasCommand, { EASCommandLoggedInContext } from '../../commandUtils/EasCommand';
import { AppPrivacy, Role } from '../../graphql/generated';
import { AppMutation } from '../../graphql/mutations/AppMutation';
import Log from '../../log';
import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { toAppPrivacy } from '../../project/projectUtils';
import {
findProjectRootAsync,
saveProjectIdToAppConfigAsync,
toAppPrivacy,
} from '../../project/projectUtils';
import { confirmAsync, promptAsync } from '../../prompts';
import { Actor } from '../../user/User';
@@ -41,8 +44,7 @@ export default class ProjectInit extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.ProjectDir,
...EASCommandLoggedInContext,
};
private static async saveProjectIdAndLogSuccessAsync(
@@ -55,9 +57,9 @@ export default class ProjectInit extends EasCommand {
private static async initializeWithExplicitIDAsync(
projectId: string,
projectDir: string,
{ force, nonInteractive }: InitializeMethodOptions
): Promise<void> {
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const existingProjectId = exp.extra?.eas?.projectId;
@@ -99,10 +101,8 @@ export default class ProjectInit extends EasCommand {
}
}
private static async initializeWithInteractiveSelectionAsync(
actor: Actor,
projectDir: string
): Promise<void> {
private static async initializeWithInteractiveSelectionAsync(actor: Actor): Promise<void> {
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const existingProjectId = exp.extra?.eas?.projectId;
@@ -110,7 +110,7 @@ export default class ProjectInit extends EasCommand {
Log.succeed(
`Project already linked (ID: ${chalk.bold(
existingProjectId
)}). To re-configure, remove the "extra.eas.projectId" field from your app config.`
)}). To re-configure, remove the extra.eas.projectId field from your app config.`
);
return;
}
@@ -218,12 +218,12 @@ export default class ProjectInit extends EasCommand {
const {
flags: { id, force, 'non-interactive': nonInteractive },
} = await this.parse(ProjectInit);
const { actor, projectDir } = await this.getContextAsync(ProjectInit, { nonInteractive });
const { actor } = await this.getContextAsync(ProjectInit, { nonInteractive });
if (id) {
await ProjectInit.initializeWithExplicitIDAsync(id, projectDir, { force, nonInteractive });
await ProjectInit.initializeWithExplicitIDAsync(id, { force, nonInteractive });
} else {
await ProjectInit.initializeWithInteractiveSelectionAsync(actor, projectDir);
await ProjectInit.initializeWithInteractiveSelectionAsync(actor);
}
}
}
@@ -4,7 +4,7 @@ import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
import {
@@ -50,7 +50,7 @@ export default class EnvironmentSecretCreate extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
@@ -64,9 +64,7 @@ export default class EnvironmentSecretCreate extends EasCommand {
'non-interactive': nonInteractive,
},
} = await this.parse(EnvironmentSecretCreate);
const {
projectConfig: { projectId },
} = await this.getContextAsync(EnvironmentSecretCreate, {
const { projectId } = await this.getContextAsync(EnvironmentSecretCreate, {
nonInteractive,
});
@@ -172,7 +170,6 @@ export default class EnvironmentSecretCreate extends EasCommand {
if (!(await fs.pathExists(secretFilePath))) {
throw new Error(`File "${secretValue}" does not exist`);
}
secretValue = await fs.readFile(secretFilePath, 'base64');
}
if (scope === EnvironmentSecretScope.PROJECT) {
@@ -1,6 +1,6 @@
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
import {
@@ -22,16 +22,14 @@ export default class EnvironmentSecretDelete extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
let {
flags: { id, 'non-interactive': nonInteractive },
} = await this.parse(EnvironmentSecretDelete);
const {
projectConfig: { projectId },
} = await this.getContextAsync(EnvironmentSecretDelete, {
const { projectId } = await this.getContextAsync(EnvironmentSecretDelete, {
nonInteractive,
});
let secret: EnvironmentSecretWithScope | undefined;
+3 -5
View File
@@ -2,7 +2,7 @@ import chalk from 'chalk';
import Table from 'cli-table3';
import dateFormat from 'dateformat';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EnvironmentSecretsQuery } from '../../graphql/queries/EnvironmentSecretsQuery';
import { EnvironmentSecretTypeToSecretType } from '../../graphql/types/EnvironmentSecret';
import Log from '../../log';
@@ -11,13 +11,11 @@ export default class EnvironmentSecretList extends EasCommand {
static override description = 'list environment secrets available for your current app';
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
const {
projectConfig: { projectId },
} = await this.getContextAsync(EnvironmentSecretList, {
const { projectId } = await this.getContextAsync(EnvironmentSecretList, {
nonInteractive: true,
});
+11 -13
View File
@@ -2,7 +2,10 @@ import { EasJsonAccessor } from '@expo/eas-json';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../commandUtils/EasCommand';
import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../commandUtils/EasCommand';
import { StatuspageServiceName, SubmissionFragment } from '../graphql/generated';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log from '../log';
@@ -13,6 +16,7 @@ import {
selectRequestedPlatformAsync,
toPlatforms,
} from '../platform';
import { findProjectRootAsync } from '../project/projectUtils';
import { SubmitArchiveFlags, createSubmissionContextAsync } from '../submit/context';
import {
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish,
@@ -90,22 +94,18 @@ export default class Submit extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.ProjectDir,
...EASCommandLoggedInContext,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(Submit);
const {
actor,
projectConfig: { exp, projectId, projectDir },
} = await this.getContextAsync(Submit, {
nonInteractive: false,
});
const { actor, projectId } = await this.getContextAsync(Submit, { nonInteractive: false });
const flags = this.sanitizeFlags(rawFlags);
const projectDir = await findProjectRootAsync();
await maybeWarnAboutEasOutagesAsync([StatuspageServiceName.EasSubmit]);
const flagsWithPlatform = await this.ensurePlatformSelectedAsync(flags);
@@ -120,16 +120,14 @@ export default class Submit extends EasCommand {
const submissions: SubmissionFragment[] = [];
for (const submissionProfile of submissionProfiles) {
// this command doesn't make use of env when getting the project config
const ctx = await createSubmissionContextAsync({
platform: submissionProfile.platform,
projectDir,
projectId,
profile: submissionProfile.profile,
archiveFlags: flagsWithPlatform.archiveFlags,
nonInteractive: flagsWithPlatform.nonInteractive,
actor,
exp,
projectId,
});
if (submissionProfiles.length > 1) {
@@ -5,11 +5,13 @@ import assert from 'assert';
import chalk from 'chalk';
import { getEASUpdateURL } from '../../api';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { AppPlatform } from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import { RequestedPlatform, appPlatformDisplayNames } from '../../platform';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
installExpoUpdatesAsync,
isExpoUpdatesInstalledOrAvailable,
} from '../../project/projectUtils';
@@ -33,7 +35,7 @@ export default class UpdateConfigure extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
@@ -41,12 +43,12 @@ export default class UpdateConfigure extends EasCommand {
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
);
const { flags } = await this.parse(UpdateConfigure);
const {
projectConfig: { projectId, exp, projectDir },
} = await this.getContextAsync(UpdateConfigure, {
const { projectId } = await this.getContextAsync(UpdateConfigure, {
nonInteractive: true,
});
const platform = flags.platform as RequestedPlatform;
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
if (!isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion)) {
await installExpoUpdatesAsync(projectDir);
@@ -10,7 +10,7 @@ import { getEASUpdateURL } from '../../api';
import { selectBranchOnAppAsync } from '../../branch/queries';
import { BranchNotFoundError, getDefaultBranchNameAsync } from '../../branch/utils';
import { getUpdateGroupUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import fetch from '../../fetch';
@@ -27,7 +27,9 @@ import { BranchQuery } from '../../graphql/queries/BranchQuery';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log, { learnMore, link } from '../../log';
import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getOwnerAccountForProjectIdAsync,
installExpoUpdatesAsync,
isExpoUpdatesInstalledOrAvailable,
@@ -167,7 +169,7 @@ export default class UpdatePublish extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
@@ -186,7 +188,7 @@ export default class UpdatePublish extends EasCommand {
'non-interactive': nonInteractive,
json: jsonFlag,
} = flags;
const { getDynamicProjectConfigAsync } = await this.getContextAsync(UpdatePublish, {
const { projectId } = await this.getContextAsync(UpdatePublish, {
nonInteractive,
});
@@ -198,11 +200,12 @@ export default class UpdatePublish extends EasCommand {
// If a group was specified, that means we are republishing it.
republish = republish || !!group;
const { exp, projectId, projectDir } = await getDynamicProjectConfigAsync({
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir, {
isPublicConfig: true,
});
const { exp: expPrivate } = await getDynamicProjectConfigAsync({
const expPrivate = getExpoConfig(projectDir, {
isPublicConfig: false,
});
@@ -501,7 +504,7 @@ export default class UpdatePublish extends EasCommand {
publishSpinner.succeed('Published!');
} catch (e) {
publishSpinner.fail('Failed to publish updates');
publishSpinner.fail('Failed to published updates');
throw e;
}
+3 -5
View File
@@ -1,7 +1,7 @@
import { Flags } from '@oclif/core';
import { selectBranchOnAppAsync } from '../../branch/queries';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import {
EasPaginatedQueryFlags,
@@ -33,15 +33,13 @@ export default class UpdateList extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(UpdateList);
const { branch: branchFlag, all, json: jsonFlag, 'non-interactive': nonInteractive } = flags;
const {
projectConfig: { projectId },
} = await this.getContextAsync(UpdateList, {
const { projectId } = await this.getContextAsync(UpdateList, {
nonInteractive,
});
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
@@ -1,6 +1,6 @@
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { WebhookType } from '../../graphql/generated';
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
@@ -26,14 +26,12 @@ export default class WebhookCreate extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(WebhookCreate);
const {
projectConfig: { projectId },
} = await this.getContextAsync(WebhookCreate, {
const { projectId } = await this.getContextAsync(WebhookCreate, {
nonInteractive: flags['non-interactive'],
});
const webhookInputParams = await prepareInputParamsAsync(flags);
@@ -2,7 +2,7 @@ import assert from 'assert';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { WebhookFragment } from '../../graphql/generated';
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
@@ -28,7 +28,7 @@ export default class WebhookDelete extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
@@ -36,9 +36,7 @@ export default class WebhookDelete extends EasCommand {
args: { ID: webhookId },
flags: { 'non-interactive': nonInteractive },
} = await this.parse(WebhookDelete);
const {
projectConfig: { projectId },
} = await this.getContextAsync(WebhookDelete, {
const { projectId } = await this.getContextAsync(WebhookDelete, {
nonInteractive,
});
@@ -1,7 +1,7 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { WebhookType } from '../../graphql/generated';
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
import Log from '../../log';
@@ -20,16 +20,14 @@ export default class WebhookList extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> {
const {
flags: { event },
} = await this.parse(WebhookList);
const {
projectConfig: { projectId },
} = await this.getContextAsync(WebhookList, {
const { projectId } = await this.getContextAsync(WebhookList, {
nonInteractive: true,
});
@@ -90,7 +90,6 @@ export async function getAppLookupParamsFromContextAsync(
ctx: CredentialsContext,
gradleContext?: GradleBuildContext
): Promise<AppLookupParams> {
ctx.ensureProjectContext();
const projectName = ctx.exp.slug;
const projectId = ctx.projectId;
const account = await getOwnerAccountForProjectIdAsync(projectId);
+9 -39
View File
@@ -4,7 +4,6 @@ import { EasJson } from '@expo/eas-json';
import chalk from 'chalk';
import Log from '../log';
import { getExpoConfig } from '../project/expoConfig';
import { confirmAsync } from '../prompts';
import { Actor } from '../user/User';
import * as AndroidGraphqlClient from './android/api/GraphqlClient';
@@ -28,19 +27,16 @@ export class CredentialsContext {
private shouldAskAuthenticateAppStore: boolean = true;
private projectInfo: CredentialsContextProjectInfo | null;
private projectInfo: CredentialsContextProjectInfo;
constructor(
private options: {
// if null, this implies not running in a project context
projectInfo: CredentialsContextProjectInfo | null;
easJsonCliConfig?: EasJson['cli'];
nonInteractive: boolean;
projectDir: string;
user: Actor;
env?: Env;
}
) {
constructor(options: {
projectInfo: CredentialsContextProjectInfo;
easJsonCliConfig?: EasJson['cli'];
nonInteractive: boolean;
projectDir: string;
user: Actor;
env?: Env;
}) {
this.easJsonCliConfig = options.easJsonCliConfig;
this.projectDir = options.projectDir;
this.user = options.user;
@@ -48,40 +44,14 @@ export class CredentialsContext {
this.projectInfo = options.projectInfo;
}
static getExpoConfigInProject(
projectDir: string,
{ env }: { env?: Env } = {}
): ExpoConfig | null {
try {
return getExpoConfig(projectDir, { env });
} catch {
// ignore error, context might be created outside of expo project
return null;
}
}
get hasProjectContext(): boolean {
return !!this.projectInfo;
}
get exp(): ExpoConfig {
this.ensureProjectContext();
return this.projectInfo!.exp;
}
get projectId(): string {
this.ensureProjectContext();
return this.projectInfo!.projectId;
}
public ensureProjectContext(): void {
if (this.hasProjectContext) {
return;
}
// trigger getConfig error
getExpoConfig(this.options.projectDir);
}
async bestEffortAppStoreAuthenticateAsync(): Promise<void> {
if (this.appStore.authCtx || !this.shouldAskAuthenticateAppStore) {
// skip prompts if already have apple ctx or already asked about it
@@ -1,8 +0,0 @@
import { AppleTeamFragment } from '../../../graphql/generated';
export function formatAppleTeam({
appleTeamIdentifier,
appleTeamName,
}: Pick<AppleTeamFragment, 'appleTeamIdentifier' | 'appleTeamName'>): string {
return `Team ID: ${appleTeamIdentifier}${appleTeamName ? `, Team name: ${appleTeamName}` : ''}`;
}
@@ -14,3 +14,7 @@ export async function resolveAppleTeamIfAuthenticatedAsync(
appleTeamName: ctx.appStore.authCtx.team.name,
});
}
export function formatAppleTeam({ appleTeamIdentifier, appleTeamName }: AppleTeamFragment): string {
return `Team ID: ${appleTeamIdentifier}${appleTeamName ? `, Team name: ${appleTeamName}` : ''}`;
}
@@ -20,7 +20,7 @@ import {
ascApiKeyIssuerIdSchema,
} from '../credentials';
import { isAscApiKeyValidAndTrackedAsync } from '../validators/validateAscApiKey';
import { formatAppleTeam } from './AppleTeamFormatting';
import { formatAppleTeam } from './AppleTeamUtils';
export enum AppStoreApiKeyPurpose {
SUBMISSION_SERVICE = 'EAS Submit',
@@ -82,7 +82,6 @@ export async function assignBuildCredentialsAsync(
}
export async function getAppFromContextAsync(ctx: CredentialsContext): Promise<App> {
ctx.ensureProjectContext();
const projectName = ctx.exp.slug;
const projectId = ctx.projectId;
const account = await getOwnerAccountForProjectIdAsync(projectId);
@@ -7,7 +7,7 @@ import {
} from '../../../graphql/generated';
import Log from '../../../log';
import { ora } from '../../../ora';
import { getApplePlatformFromTarget } from '../../../project/ios/target';
import { getApplePlatformFromSdkRoot } from '../../../project/ios/target';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import { AppleProvisioningProfileMutationResult } from '../api/graphql/mutations/AppleProvisioningProfileMutation';
@@ -46,7 +46,7 @@ export class ConfigureProvisioningProfile {
return null;
}
const applePlatform = await getApplePlatformFromTarget(this.target);
const applePlatform = await getApplePlatformFromSdkRoot(this.target);
const profilesFromApple = await ctx.appStore.listProvisioningProfilesAsync(
this.app.bundleIdentifier,
applePlatform
@@ -13,7 +13,7 @@ export async function chooseDevicesAsync(
const { devices } = await promptAsync({
type: 'multiselect',
name: 'devices',
message: 'Select devices for the ad hoc build:',
message: 'Select devices for the adhoc build:',
hint: '- Space to select. Return to submit',
choices: allDevices.map(device => ({
value: device,
@@ -26,11 +26,26 @@ export async function chooseDevicesAsync(
return devices;
}
export async function chooseDevicesToDeleteAsync(
allDevices: AppleDeviceFragment[]
): Promise<AppleDevice[]> {
const { devices } = await promptAsync({
type: 'multiselect',
name: 'devices',
message: 'Which devices do you want to remove?',
choices: allDevices.map(device => ({
value: device,
title: formatDeviceLabel(device),
selected: false,
})),
instructions: false,
});
return devices;
}
export function formatDeviceLabel(device: AppleDeviceFragment): string {
const deviceDetails = formatDeviceDetails(device);
return `${device.identifier}${deviceDetails !== '' ? ` ${deviceDetails}` : ''}${
device.name ? ` (${device.name})` : ''
}`;
return `${device.name ?? device.identifier}${deviceDetails !== '' ? ` ${deviceDetails}` : ''}`;
}
function formatDeviceDetails(device: AppleDeviceFragment): string {
@@ -17,7 +17,7 @@ import { filterRevokedDistributionCertsFromEasServers } from '../appstore/Creden
import { AppleTooManyCertsError } from '../appstore/distributionCertificate';
import { distributionCertificateSchema } from '../credentials';
import { validateDistributionCertificateAsync } from '../validators/validateDistributionCertificate';
import { formatAppleTeam } from './AppleTeamFormatting';
import { formatAppleTeam } from './AppleTeamUtils';
export function formatDistributionCertificate(
distributionCertificate: AppleDistributionCertificateFragment,
@@ -1,6 +1,6 @@
import chalk from 'chalk';
import { getApplePlatformFromTarget } from '../../../project/ios/target';
import { getApplePlatformFromSdkRoot } from '../../../project/ios/target';
import { CredentialsContext } from '../../context';
import {
DistributionCertificate,
@@ -29,7 +29,7 @@ export async function generateProvisioningProfileAsync(
const appleAuthCtx = await ctx.appStore.ensureAuthenticatedAsync();
const type = appleAuthCtx.team.inHouse ? 'Enterprise ' : 'AppStore';
const profileName = `*[expo] ${bundleIdentifier} ${type} ${new Date().toISOString()}`; // Apple drops [ if its the first char (!!)
const applePlatform = await getApplePlatformFromTarget(target);
const applePlatform = await getApplePlatformFromSdkRoot(target);
return await ctx.appStore.createProvisioningProfileAsync(
bundleIdentifier,
distCert,
@@ -11,7 +11,7 @@ import { filterRevokedAndUntrackedPushKeysFromEasServersAsync } from '../appstor
import { APPLE_KEYS_TOO_MANY_GENERATED_ERROR } from '../appstore/pushKey';
import { pushKeySchema } from '../credentials';
import { isPushKeyValidAndTrackedAsync } from '../validators/validatePushKey';
import { formatAppleTeam } from './AppleTeamFormatting';
import { formatAppleTeam } from './AppleTeamUtils';
export async function provideOrGeneratePushKeyAsync(ctx: CredentialsContext): Promise<PushKey> {
if (!ctx.nonInteractive) {
@@ -13,7 +13,7 @@ import {
IosDistributionType,
} from '../../../graphql/generated';
import Log from '../../../log';
import { getApplePlatformFromTarget } from '../../../project/ios/target';
import { getApplePlatformFromSdkRoot } from '../../../project/ios/target';
import { confirmAsync, pressAnyKeyToContinueAsync, promptAsync } from '../../../prompts';
import differenceBy from '../../../utils/expodash/differenceBy';
import { CredentialsContext } from '../../context';
@@ -121,7 +121,7 @@ export class SetUpAdhocProvisioningProfile {
);
// 4. Reuse or create the profile on Apple Developer Portal
const applePlatform = await getApplePlatformFromTarget(target);
const applePlatform = await getApplePlatformFromSdkRoot(target);
const profileType =
applePlatform === ApplePlatform.TV_OS
? ProfileType.TVOS_APP_ADHOC
@@ -6,7 +6,7 @@ import {
IosAppBuildCredentialsFragment,
IosDistributionType,
} from '../../../graphql/generated';
import { getApplePlatformFromTarget } from '../../../project/ios/target';
import { getApplePlatformFromSdkRoot } from '../../../project/ios/target';
import { confirmAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
@@ -114,7 +114,7 @@ export class SetUpProvisioningProfile {
}
// See if the profile we have exists on the Apple Servers
const applePlatform = await getApplePlatformFromTarget(this.target);
const applePlatform = await getApplePlatformFromSdkRoot(this.target);
const existingProfiles = await ctx.appStore.listProvisioningProfilesAsync(
this.app.bundleIdentifier,
applePlatform
@@ -2,7 +2,6 @@ import assert from 'assert';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { DeviceNotFoundError } from '../../../../../devices/utils/errors';
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
import {
AppleDevice,
@@ -10,12 +9,10 @@ import {
AppleDevicesByAppleTeamQuery,
AppleDevicesByIdentifierQuery,
AppleDevicesByTeamIdentifierQuery,
AppleDevicesByTeamIdentifierQueryVariables,
AppleTeamFragment,
} from '../../../../../graphql/generated';
import { AppleDeviceFragmentNode } from '../../../../../graphql/types/credentials/AppleDevice';
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
import { formatAppleTeam } from '../../../actions/AppleTeamFormatting';
export type AppleDeviceFragmentWithAppleTeam = AppleDeviceFragment & {
appleTeam: AppleTeamFragment;
@@ -83,12 +80,10 @@ export const AppleDeviceQuery = {
return appleDevices;
},
async getAllForAppleTeamAsync({
accountName,
appleTeamIdentifier,
offset,
limit,
}: AppleDevicesByTeamIdentifierQueryVariables): Promise<AppleDeviceFragment[]> {
async getAllForAppleTeamAsync(
accountName: string,
appleTeamIdentifier: string
): Promise<AppleDevicesByTeamIdentifierQueryResult | null> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppleDevicesByTeamIdentifierQuery>(
@@ -96,8 +91,6 @@ export const AppleDeviceQuery = {
query AppleDevicesByTeamIdentifier(
$accountName: String!
$appleTeamIdentifier: String!
$offset: Int
$limit: Int
) {
account {
byName(accountName: $accountName) {
@@ -106,38 +99,33 @@ export const AppleDeviceQuery = {
id
appleTeamIdentifier
appleTeamName
appleDevices(offset: $offset, limit: $limit) {
appleDevices {
id
identifier
name
deviceClass
enabled
model
}
}
}
}
}
`,
{ accountName, appleTeamIdentifier, offset, limit },
{ accountName, appleTeamIdentifier },
{
additionalTypenames: ['AppleDevice', 'AppleTeam'],
}
)
.toPromise()
);
const [appleTeam] = data.account.byName.appleTeams;
const { appleDevices } = appleTeam;
if (!appleDevices) {
throw new Error(`Could not find devices on Apple team -- ${formatAppleTeam(appleTeam)}`);
}
return appleDevices;
return data.account.byName.appleTeams[0];
},
async getByDeviceIdentifierAsync(
accountName: string,
identifier: string
): Promise<AppleDevicesByIdentifierQueryResult> {
): Promise<AppleDevicesByIdentifierQueryResult | null> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppleDevicesByIdentifierQuery>(
@@ -148,7 +136,6 @@ export const AppleDeviceQuery = {
id
appleDevices(identifier: $identifier) {
id
model
identifier
name
deviceClass
@@ -171,12 +158,6 @@ export const AppleDeviceQuery = {
.toPromise()
);
const device = data.account.byName.appleDevices[0];
if (!device) {
throw new DeviceNotFoundError(
`Device with id ${identifier} was not found on account ${accountName}.`
);
}
return device;
return data.account.byName.appleDevices[0] ?? null;
},
};
@@ -6,34 +6,29 @@ import {
AppleTeamByIdentifierQuery,
AppleTeamFragment,
AppleTeamsByAccountNameQuery,
AppleTeamsByAccountNameQueryVariables,
} from '../../../../../graphql/generated';
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
export const AppleTeamQuery = {
async getAllForAccountAsync({
accountName,
offset,
limit,
}: AppleTeamsByAccountNameQueryVariables): Promise<AppleTeamFragment[]> {
async getAllForAccountAsync(accountName: string): Promise<AppleTeamFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppleTeamsByAccountNameQuery>(
gql`
query AppleTeamsByAccountName($accountName: String!, $offset: Int, $limit: Int) {
query AppleTeamsByAccountName($accountName: String!) {
account {
byName(accountName: $accountName) {
id
appleTeams(offset: $offset, limit: $limit) {
appleTeams {
id
...AppleTeamFragment
appleTeamName
appleTeamIdentifier
}
}
}
}
${print(AppleTeamFragmentNode)}
`,
{ accountName, offset, limit },
{ accountName },
{
additionalTypenames: ['AppleTeam'],
}
@@ -41,8 +36,9 @@ export const AppleTeamQuery = {
.toPromise()
);
return data.account.byName.appleTeams ?? [];
return data.account.byName.appleTeams;
},
async getByAppleTeamIdentifierAsync(
accountId: string,
appleTeamIdentifier: string
@@ -6,7 +6,7 @@ import nullthrows from 'nullthrows';
import { IosAppBuildCredentialsFragment, IosDistributionType } from '../../../graphql/generated';
import Log from '../../../log';
import { getApplePlatformFromTarget } from '../../../project/ios/target';
import { getApplePlatformFromSdkRoot } from '../../../project/ios/target';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
import { ProfileClass } from '../appstore/provisioningProfile';
@@ -94,7 +94,7 @@ async function validateProvisioningProfileWithAppleAsync(
assert(buildCredentials.provisioningProfile, 'Provisioning Profile must be defined');
const { developerPortalIdentifier, provisioningProfile } = buildCredentials.provisioningProfile;
const applePlatform = await getApplePlatformFromTarget(target);
const applePlatform = await getApplePlatformFromSdkRoot(target);
const profilesFromApple = await ctx.appStore.listProvisioningProfilesAsync(
app.bundleIdentifier,
applePlatform,
@@ -5,7 +5,7 @@ import { CredentialsContext, CredentialsContextProjectInfo } from '../context';
export interface Action<T = void> {
actor: Actor;
projectInfo: CredentialsContextProjectInfo | null;
projectInfo: CredentialsContextProjectInfo;
runAsync(ctx: CredentialsContext): Promise<T>;
}
@@ -1,4 +1,3 @@
import { CredentialsContext } from '../context';
import { ActionInfo, IosActionType, Scope } from './Actions';
export const highLevelActions: ActionInfo[] = [
@@ -47,7 +46,7 @@ export const credentialsJsonActions: ActionInfo[] = [
},
];
export function getPushKeyActions(ctx: CredentialsContext): ActionInfo[] {
export function getPushKeyActions(): ActionInfo[] {
return [
{
value: IosActionType.SetUpPushKey,
@@ -57,7 +56,7 @@ export function getPushKeyActions(ctx: CredentialsContext): ActionInfo[] {
{
value: IosActionType.CreatePushKey,
title: 'Add a new push key',
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account,
scope: Scope.Project,
},
{
value: IosActionType.UseExistingPushKey,
@@ -77,7 +76,7 @@ export function getPushKeyActions(ctx: CredentialsContext): ActionInfo[] {
];
}
export function getAscApiKeyActions(ctx: CredentialsContext): ActionInfo[] {
export function getAscApiKeyActions(): ActionInfo[] {
return [
{
value: IosActionType.SetUpAscApiKeyForSubmissions,
@@ -92,7 +91,7 @@ export function getAscApiKeyActions(ctx: CredentialsContext): ActionInfo[] {
{
value: IosActionType.CreateAscApiKeyForSubmissions,
title: 'Add a new API Key For EAS Submit',
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account,
scope: Scope.Project,
},
{
value: IosActionType.RemoveAscApiKey,
@@ -107,7 +106,7 @@ export function getAscApiKeyActions(ctx: CredentialsContext): ActionInfo[] {
];
}
export function getBuildCredentialsActions(ctx: CredentialsContext): ActionInfo[] {
export function getBuildCredentialsActions(): ActionInfo[] {
return [
{
// This command will be triggered during build to ensure all credentials are ready
@@ -124,7 +123,7 @@ export function getBuildCredentialsActions(ctx: CredentialsContext): ActionInfo[
{
value: IosActionType.CreateDistributionCertificate,
title: `Distribution Certificate: Add a new one to your account`,
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account,
scope: Scope.Project,
},
{
value: IosActionType.RemoveDistributionCertificate,
@@ -60,34 +60,27 @@ export class ManageAndroid {
nonInteractive: false,
});
let gradleContext;
if (ctx.hasProjectContext) {
assert(buildProfile, 'buildProfile must be defined in a project context');
gradleContext = await this.createProjectContextAsync(ctx, buildProfile);
}
assert(buildProfile, 'buildProfile must be defined in a project context');
const gradleContext = await this.createProjectContextAsync(ctx, buildProfile);
while (true) {
try {
if (ctx.hasProjectContext) {
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx, gradleContext);
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(
appLookupParams
);
if (!appCredentials) {
displayEmptyAndroidCredentials(appLookupParams);
} else {
displayAndroidAppCredentials({ appLookupParams, appCredentials });
}
// copy legacy credentials if user is new to EAS and has legacy credentials
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(
ctx,
appLookupParams
);
if (canCopyLegacyCredentials) {
await promptUserAndCopyLegacyCredentialsAsync(ctx, appLookupParams);
}
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx, gradleContext);
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(
appLookupParams
);
if (!appCredentials) {
displayEmptyAndroidCredentials(appLookupParams);
} else {
displayAndroidAppCredentials({ appLookupParams, appCredentials });
}
// copy legacy credentials if user is new to EAS and has legacy credentials
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
if (canCopyLegacyCredentials) {
await promptUserAndCopyLegacyCredentialsAsync(ctx, appLookupParams);
}
const { action: chosenAction } = await promptAsync({
type: 'select',
name: 'action',
@@ -138,7 +131,6 @@ export class ManageAndroid {
ctx: CredentialsContext,
buildProfile: BuildProfile<Platform.ANDROID>
): Promise<GradleBuildContext | undefined> {
assert(ctx.hasProjectContext, 'createProjectContextAsync: must have project context.');
return await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
}
@@ -147,10 +139,6 @@ export class ManageAndroid {
action: AndroidActionType,
gradleContext?: GradleBuildContext
): Promise<void> {
assert(
ctx.hasProjectContext,
'You must be in your project directory in order to perform this action'
);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx, gradleContext);
if (action === AndroidActionType.CreateKeystore) {
const selectBuildCredentialsResult = await new SelectAndroidBuildCredentials(
@@ -14,7 +14,6 @@ import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { resolveTargetsAsync } from '../../project/ios/target';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
import { ensureActorHasPrimaryAccount } from '../../user/actions';
import { CredentialsContext } from '../context';
import {
AppStoreApiKeyPurpose,
@@ -68,41 +67,31 @@ export class ManageIos {
env: buildProfile?.env,
nonInteractive: false,
});
const buildCredentialsActions = getBuildCredentialsActions(ctx);
const pushKeyActions = getPushKeyActions(ctx);
const ascApiKeyActions = getAscApiKeyActions(ctx);
const buildCredentialsActions = getBuildCredentialsActions();
const pushKeyActions = getPushKeyActions();
const ascApiKeyActions = getAscApiKeyActions();
await ctx.bestEffortAppStoreAuthenticateAsync();
const getAccountForProjectAsync = async (projectId: string): Promise<AccountFragment> => {
return await getOwnerAccountForProjectIdAsync(projectId);
};
const account = await getOwnerAccountForProjectIdAsync(
this.callingAction.projectInfo.projectId
);
const account = ctx.hasProjectContext
? await getAccountForProjectAsync(ctx.projectId)
: ensureActorHasPrimaryAccount(ctx.user);
let app = null;
let targets = null;
if (ctx.hasProjectContext) {
assert(buildProfile, 'buildProfile must be defined in project context');
const projectContext = await this.createProjectContextAsync(ctx, account, buildProfile);
app = projectContext.app;
targets = projectContext.targets;
}
assert(buildProfile, 'buildProfile must be defined in project context');
const projectContext = await this.createProjectContextAsync(ctx, account, buildProfile);
const app = projectContext.app;
const targets = projectContext.targets;
while (true) {
try {
if (ctx.hasProjectContext) {
assert(targets && app);
const iosAppCredentialsMap: IosAppCredentialsMap = {};
for (const target of targets) {
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx, target);
iosAppCredentialsMap[target.targetName] =
await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(appLookupParams);
}
displayIosCredentials(app, iosAppCredentialsMap, targets);
assert(targets && app);
const iosAppCredentialsMap: IosAppCredentialsMap = {};
for (const target of targets) {
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx, target);
iosAppCredentialsMap[target.targetName] =
await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(appLookupParams);
}
displayIosCredentials(app, iosAppCredentialsMap, targets);
const { action: chosenAction } = await promptAsync({
type: 'select',
@@ -137,10 +126,6 @@ export class ManageIos {
return await this.callingAction.runAsync(ctx);
}
} else if (actionInfo.scope === Scope.Project) {
assert(
ctx.hasProjectContext,
'You must be in your project directory in order to perform this action'
);
await this.runProjectSpecificActionAsync(
ctx,
nullthrows(app, 'app must be defined in project context'),
@@ -172,8 +157,6 @@ export class ManageIos {
app: App;
targets: Target[];
}> {
assert(ctx.hasProjectContext, 'createProjectContextAsync: must have project context.');
const app = { account, projectName: ctx.exp.slug };
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
@@ -7,7 +7,7 @@ import { ManageIos } from './ManageIos';
export class SelectPlatform {
constructor(
public readonly actor: Actor,
public readonly projectInfo: CredentialsContextProjectInfo | null,
public readonly projectInfo: CredentialsContextProjectInfo,
private readonly flagPlatform?: string
) {}
@@ -84,17 +84,5 @@ describe(AccountResolver, () => {
expect(account).toEqual(user.accounts[0]);
});
});
describe('when outside project dir', () => {
it('asks the user to choose the account from his account list', async () => {
jest.mocked(prompts).mockImplementationOnce(async () => ({
account: user.accounts[0],
}));
const resolver = new AccountResolver(null, user);
const account = await resolver.resolveAccountAsync();
expect(account).toEqual(user.accounts[0]);
});
});
});
});
+3 -3
View File
@@ -4,7 +4,7 @@ import { Actor } from '../user/User';
export interface DeviceManagerContext {
appStore: AppStoreApi;
user: Actor;
projectId: string | null;
projectId: string;
}
export async function createContextAsync({
@@ -14,11 +14,11 @@ export async function createContextAsync({
}: {
appStore: AppStoreApi;
user: Actor;
projectId: string | undefined;
projectId: string;
}): Promise<DeviceManagerContext> {
return {
appStore,
user,
projectId: projectId ?? null,
projectId,
};
}
+3 -5
View File
@@ -46,11 +46,9 @@ export class AccountResolver {
constructor(private projectId: string | null, private user: Actor) {}
public async resolveAccountAsync(): Promise<AccountFragment> {
if (this.projectId) {
const account = await this.resolveProjectAccountAsync();
if (account) {
return account;
}
const account = await this.resolveProjectAccountAsync();
if (account) {
return account;
}
return await this.promptForAccountAsync();
}
-161
View File
@@ -1,161 +0,0 @@
import chalk from 'chalk';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { formatAppleTeam } from '../credentials/ios/actions/AppleTeamFormatting';
import { formatDeviceLabel } from '../credentials/ios/actions/DeviceUtils';
import { AppleDeviceQuery } from '../credentials/ios/api/graphql/queries/AppleDeviceQuery';
import { AppleTeamQuery } from '../credentials/ios/api/graphql/queries/AppleTeamQuery';
import { AppleDeviceFragment, AppleTeamFragment } from '../graphql/generated';
import Log from '../log';
import { printJsonOnlyOutput } from '../utils/json';
import {
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
import formatDevice, { AppleTeamIdAndName } from './utils/formatDevice';
export const TEAMS_LIMIT = 50;
export const DEVICES_LIMIT = 50;
export async function selectAppleTeamOnAccountAsync({
accountName,
selectionPromptTitle,
paginatedQueryOptions,
}: {
accountName: string;
selectionPromptTitle: string;
paginatedQueryOptions: PaginatedQueryOptions;
}): Promise<AppleTeamFragment> {
if (paginatedQueryOptions.nonInteractive) {
throw new Error('Unable to select an Apple team in non-interactive mode.');
} else {
const selectedAppleTeam = await paginatedQueryWithSelectPromptAsync({
limit: paginatedQueryOptions.limit ?? TEAMS_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
AppleTeamQuery.getAllForAccountAsync({
accountName,
limit,
offset,
}),
promptOptions: {
title: selectionPromptTitle,
createDisplayTextForSelectionPromptListItem: appleTeam =>
appleTeam.appleTeamName
? `${appleTeam.appleTeamName} (ID: ${appleTeam.appleTeamIdentifier})`
: appleTeam.appleTeamIdentifier,
getIdentifierForQueryItem: appleTeam => appleTeam.id,
},
});
if (!selectedAppleTeam) {
throw new Error(`Couldn't find any teams for the account ${accountName}`);
}
return selectedAppleTeam;
}
}
export async function selectAppleDeviceOnAppleTeamAsync({
accountName,
appleTeamIdentifier,
selectionPromptTitle,
paginatedQueryOptions,
}: {
accountName: string;
appleTeamIdentifier: string;
selectionPromptTitle: string;
paginatedQueryOptions: PaginatedQueryOptions;
}): Promise<AppleDeviceFragment> {
if (paginatedQueryOptions.nonInteractive) {
throw new Error('Unable to select an Apple device in non-interactive mode.');
} else {
const selectedAppleDevice = await paginatedQueryWithSelectPromptAsync({
limit: paginatedQueryOptions.limit ?? DEVICES_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
AppleDeviceQuery.getAllForAppleTeamAsync({
accountName,
appleTeamIdentifier,
limit,
offset,
}),
promptOptions: {
title: selectionPromptTitle,
createDisplayTextForSelectionPromptListItem: appleDevice => formatDeviceLabel(appleDevice),
getIdentifierForQueryItem: appleTeam => appleTeam.id,
},
});
if (!selectedAppleDevice) {
throw new Error(
`Couldn't find any devices on the Apple team with the id ${appleTeamIdentifier}`
);
}
return selectedAppleDevice;
}
}
export async function listAndRenderAppleDevicesOnAppleTeamAsync({
accountName,
appleTeam,
paginatedQueryOptions,
}: {
accountName: string;
appleTeam: AppleTeamIdAndName;
paginatedQueryOptions: PaginatedQueryOptions;
}): Promise<void> {
if (paginatedQueryOptions.nonInteractive) {
const devices = await AppleDeviceQuery.getAllForAppleTeamAsync({
accountName,
appleTeamIdentifier: appleTeam.appleTeamIdentifier,
offset: paginatedQueryOptions.offset,
limit: paginatedQueryOptions.limit,
});
renderPageOfAppleDevices({ devices, appleTeam, paginatedQueryOptions });
} else {
await paginatedQueryWithConfirmPromptAsync({
limit: paginatedQueryOptions.limit ?? DEVICES_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
AppleDeviceQuery.getAllForAppleTeamAsync({
accountName,
appleTeamIdentifier: appleTeam.appleTeamIdentifier,
limit,
offset,
}),
promptOptions: {
title: 'Load more devices?',
renderListItems: devices =>
renderPageOfAppleDevices({ devices, appleTeam, paginatedQueryOptions }),
},
});
}
}
function renderPageOfAppleDevices({
devices,
appleTeam,
paginatedQueryOptions,
}: {
devices: AppleDeviceFragment[];
appleTeam: AppleTeamIdAndName;
paginatedQueryOptions: PaginatedQueryOptions;
}): void {
if (paginatedQueryOptions.json) {
printJsonOnlyOutput(devices);
} else {
if (devices.length === 0) {
Log.log(
`Could not find devices on Apple team -- ${formatAppleTeam({
appleTeamIdentifier: appleTeam.appleTeamIdentifier,
appleTeamName: appleTeam.appleTeamName,
})}`
);
} else {
const list = devices
.map(device => formatDevice(device, appleTeam))
.join(`\n\n${chalk.dim('———')}\n\n`);
Log.log(`\n${list}`);
Log.addNewLineIfNone();
}
}
}
@@ -1,5 +0,0 @@
export class DeviceNotFoundError extends Error {
constructor(message?: string) {
super(message ?? 'Device not found.');
}
}
@@ -1,20 +1,15 @@
import { AppleDevice, AppleTeam } from '../../graphql/generated';
import formatFields from '../../utils/formatFields';
type Device = Pick<AppleDevice, 'id' | 'identifier' | 'name' | 'deviceClass' | 'enabled' | 'model'>;
type Device = Pick<AppleDevice, 'id' | 'identifier' | 'name' | 'deviceClass' | 'enabled'>;
export type AppleTeamIdAndName = Pick<AppleTeam, 'appleTeamIdentifier' | 'appleTeamName'>;
type Team = Pick<AppleTeam, 'appleTeamIdentifier' | 'appleTeamName'>;
export default function formatDevice(device: Device, team?: AppleTeamIdAndName): string {
export default function formatDevice(device: Device, team?: Team): string {
const fields = [
{ label: 'ID', value: device.id },
{ label: 'Name', value: device.name ?? 'Unknown' },
{
label: 'Class',
value: device.deviceClass
? `${device.deviceClass}${device.model ? ` ${device.model}` : ''}`
: 'Unknown',
},
{ label: 'Class', value: device.deviceClass ?? 'Unknown' },
{ label: 'UDID', value: device.identifier },
];
+3 -36
View File
@@ -1639,12 +1639,6 @@ export type AppleTeamAppleAppIdentifiersArgs = {
};
export type AppleTeamAppleDevicesArgs = {
limit?: InputMaybe<Scalars['Int']>;
offset?: InputMaybe<Scalars['Int']>;
};
export type AppleTeamAppleProvisioningProfilesArgs = {
appleAppIdentifierId?: InputMaybe<Scalars['ID']>;
};
@@ -2036,17 +2030,6 @@ export type BuildOrBuildJob = {
id: Scalars['ID'];
};
export type BuildOrBuildJobQuery = {
__typename?: 'BuildOrBuildJobQuery';
/** Look up EAS Build or Classic Build Job by ID */
byId: EasBuildOrClassicBuildJob;
};
export type BuildOrBuildJobQueryByIdArgs = {
buildOrBuildJobId: Scalars['ID'];
};
export type BuildParamsInput = {
resourceClass: BuildResourceClass;
};
@@ -2391,8 +2374,6 @@ export enum EasBuildDeprecationInfoType {
UserFacing = 'USER_FACING'
}
export type EasBuildOrClassicBuildJob = Build | BuildJob;
export enum EasServiceMetric {
AssetsRequests = 'ASSETS_REQUESTS',
BandwidthUsage = 'BANDWIDTH_USAGE',
@@ -2573,15 +2554,6 @@ export type GitHubAppQuery = {
__typename?: 'GitHubAppQuery';
appIdentifier: Scalars['String'];
clientIdentifier: Scalars['String'];
searchRepositories: Array<GitHubAppInstallationAccessibleRepository>;
};
export type GitHubAppQuerySearchRepositoriesArgs = {
githubAppInstallationId: Scalars['ID'];
page?: InputMaybe<Scalars['Int']>;
perPage?: InputMaybe<Scalars['Int']>;
query: Scalars['String'];
};
export type GitHubRepository = {
@@ -3495,7 +3467,6 @@ export type RootQuery = {
appleTeam: AppleTeamQuery;
asset: AssetQuery;
buildJobs: BuildJobQuery;
buildOrBuildJob: BuildOrBuildJobQuery;
/** Top-level query object for querying BuildPublicData publicly. */
buildPublicData: BuildPublicDataQuery;
builds: BuildQuery;
@@ -4826,12 +4797,10 @@ export type AppleDevicesByAppleTeamQuery = { __typename?: 'RootQuery', appleTeam
export type AppleDevicesByTeamIdentifierQueryVariables = Exact<{
accountName: Scalars['String'];
appleTeamIdentifier: Scalars['String'];
offset?: InputMaybe<Scalars['Int']>;
limit?: InputMaybe<Scalars['Int']>;
}>;
export type AppleDevicesByTeamIdentifierQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byName: { __typename?: 'Account', id: string, appleTeams: Array<{ __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null, appleDevices: Array<{ __typename?: 'AppleDevice', id: string, identifier: string, name?: string | null, deviceClass?: AppleDeviceClass | null, enabled?: boolean | null, model?: string | null }> }> } } };
export type AppleDevicesByTeamIdentifierQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byName: { __typename?: 'Account', id: string, appleTeams: Array<{ __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null, appleDevices: Array<{ __typename?: 'AppleDevice', id: string, identifier: string, name?: string | null, deviceClass?: AppleDeviceClass | null, enabled?: boolean | null }> }> } } };
export type AppleDevicesByIdentifierQueryVariables = Exact<{
accountName: Scalars['String'];
@@ -4839,7 +4808,7 @@ export type AppleDevicesByIdentifierQueryVariables = Exact<{
}>;
export type AppleDevicesByIdentifierQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byName: { __typename?: 'Account', id: string, appleDevices: Array<{ __typename?: 'AppleDevice', id: string, model?: string | null, identifier: string, name?: string | null, deviceClass?: AppleDeviceClass | null, enabled?: boolean | null, appleTeam: { __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null } }> } } };
export type AppleDevicesByIdentifierQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byName: { __typename?: 'Account', id: string, appleDevices: Array<{ __typename?: 'AppleDevice', id: string, identifier: string, name?: string | null, deviceClass?: AppleDeviceClass | null, enabled?: boolean | null, appleTeam: { __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null } }> } } };
export type AppleDistributionCertificateByAppQueryVariables = Exact<{
projectFullName: Scalars['String'];
@@ -4875,12 +4844,10 @@ export type ApplePushKeyByAccountQuery = { __typename?: 'RootQuery', account: {
export type AppleTeamsByAccountNameQueryVariables = Exact<{
accountName: Scalars['String'];
offset?: InputMaybe<Scalars['Int']>;
limit?: InputMaybe<Scalars['Int']>;
}>;
export type AppleTeamsByAccountNameQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byName: { __typename?: 'Account', id: string, appleTeams: Array<{ __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null }> } } };
export type AppleTeamsByAccountNameQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byName: { __typename?: 'Account', id: string, appleTeams: Array<{ __typename?: 'AppleTeam', id: string, appleTeamName?: string | null, appleTeamIdentifier: string }> } } };
export type AppleTeamByIdentifierQueryVariables = Exact<{
accountId: Scalars['ID'];
@@ -9,7 +9,6 @@ export const AppFragmentNode = gql`
slug
ownerAccount {
id
name
...AccountFragment
}
}
+3 -2
View File
@@ -6,6 +6,7 @@ import assert from 'assert';
import { CredentialsContext } from '../credentials/context';
import { getRequestContext } from '../credentials/ios/appstore/authenticate';
import { getExpoConfig } from '../project/expoConfig';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
import { Actor } from '../user/User';
@@ -42,7 +43,7 @@ export type MetadataAppStoreAuthentication = {
export async function createMetadataContextAsync(params: {
projectDir: string;
credentialsCtx: CredentialsContext;
exp: ExpoConfig;
exp?: ExpoConfig;
profileName?: string;
}): Promise<MetadataContext> {
const easJsonAccessor = new EasJsonAccessor(params.projectDir);
@@ -52,7 +53,7 @@ export async function createMetadataContextAsync(params: {
params.profileName
);
const exp = params.exp;
const exp = params.exp ?? getExpoConfig(params.projectDir);
const user = params.credentialsCtx.user;
const bundleIdentifier =
submitProfile.bundleIdentifier ?? (await getBundleIdentifierAsync(params.projectDir, exp));
@@ -0,0 +1,147 @@
import { getConfig, modifyConfigAsync } from '@expo/config';
import { vol } from 'memfs';
import { Role } from '../../graphql/generated';
import { Actor, getUserAsync } from '../../user/User';
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { findProjectRootAsync, getProjectIdAsync } from '../projectUtils';
jest.mock('@expo/config');
jest.mock('fs');
jest.mock('../fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
jest.mock('../../prompts');
jest.mock('../../user/User');
jest.mock('../../ora', () => ({
ora: () => ({
start: () => ({ succeed: () => {}, fail: () => {} }),
}),
}));
beforeEach(() => {
jest.resetAllMocks();
});
describe(findProjectRootAsync, () => {
beforeEach(() => {
vol.reset();
});
it('throws if not inside the project directory', async () => {
vol.fromJSON(
{
'./README.md': '1',
},
'/app'
);
await expect(findProjectRootAsync({ cwd: '/app' })).rejects.toThrow(
'Run this command inside a project directory.'
);
});
it('defaults to process.cwd() if defaultToProcessCwd = true', async () => {
const spy = jest.spyOn(process, 'cwd').mockReturnValue('/this/is/fake/process/cwd');
try {
vol.fromJSON(
{
'./README.md': '1',
},
'/app'
);
const projectDir = await findProjectRootAsync({ cwd: '/app', defaultToProcessCwd: true });
expect(projectDir).toBe('/this/is/fake/process/cwd');
} finally {
spy.mockRestore();
}
});
it('returns the root directory of the project', async () => {
vol.fromJSON(
{
'./README.md': '1',
'./package.json': '2',
'./src/index.ts': '3',
},
'/app'
);
const projectRoot = await findProjectRootAsync({ cwd: '/app/src' });
expect(projectRoot).toBe('/app');
});
});
describe(getProjectIdAsync, () => {
beforeEach(() => {
jest.mocked(getUserAsync).mockImplementation(
async (): Promise<Actor> => ({
__typename: 'User',
id: 'user_id',
username: 'notnotbrent',
primaryAccount: {
id: 'account_id_1',
name: 'notnotbrent',
users: [{ role: Role.Owner, actor: { id: 'user_id' } }],
},
accounts: [
{
id: 'account_id_1',
name: 'notnotbrent',
users: [{ role: Role.Owner, actor: { id: 'user_id' } }],
},
{
id: 'account_id_2',
name: 'dominik',
users: [{ role: Role.ViewOnly, actor: { id: 'user_id' } }],
},
],
isExpoAdmin: false,
})
);
});
it('gets the project ID from app config if exists', async () => {
await expect(
getProjectIdAsync(
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).resolves.toEqual('1234');
});
it('fetches the project ID when not in app config, and sets it in the config', async () => {
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'success',
config: { expo: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } } },
});
jest
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
.mockImplementation(async () => '2345');
const projectId = await getProjectIdAsync(
{ name: 'test', slug: 'test' },
{ nonInteractive: false },
{ cwd: '/app' }
);
expect(projectId).toBe('2345');
expect(modifyConfigAsync).toHaveBeenCalledTimes(1);
expect(modifyConfigAsync).toHaveBeenCalledWith('/app', {
extra: { eas: { projectId: '2345' } },
});
});
it('throws if writing the ID back to the config fails', async () => {
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'fail',
config: null,
});
jest
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
.mockImplementation(async () => '4567');
await expect(
getProjectIdAsync({ name: 'test', slug: 'test' }, { nonInteractive: false }, { cwd: '/app' })
).rejects.toThrow();
});
});
@@ -5,13 +5,13 @@ describe(isValidVersionCode, () => {
expect(isValidVersionCode(123)).toBe(true);
expect(isValidVersionCode(12.3)).toBe(false);
expect(isValidVersionCode(-123)).toBe(false);
expect(isValidVersionCode(2100000000)).toBe(true);
expect(isValidVersionCode(2100000001)).toBe(false);
expect(isValidVersionCode(2000000000)).toBe(true);
expect(isValidVersionCode(2000000001)).toBe(false);
expect(isValidVersionCode('123')).toBe(true);
expect(isValidVersionCode('12.3')).toBe(false);
expect(isValidVersionCode('-123')).toBe(false);
expect(isValidVersionCode('2100000000')).toBe(true);
expect(isValidVersionCode('2100000001')).toBe(false);
expect(isValidVersionCode('2000000000')).toBe(true);
expect(isValidVersionCode('2000000001')).toBe(false);
});
});
@@ -19,8 +19,8 @@ describe(getNextVersionCode, () => {
test('getNextVersionCode()', () => {
expect(getNextVersionCode(123)).toBe(124);
expect(() => getNextVersionCode(12.3)).toThrowError();
expect(getNextVersionCode(2099999999)).toBe(2100000000);
expect(() => getNextVersionCode(2100000000)).toThrowError();
expect(getNextVersionCode(1999999999)).toBe(2000000000);
expect(() => getNextVersionCode(2000000000)).toThrowError();
expect(getNextVersionCode('123')).toBe(124);
expect(() => getNextVersionCode('12.3')).toThrowError();
});
Loaded 100 of 120 files, more files were not shown because too many files have changed in this diff. Show more