Compare commits

...
53 Commits
Author SHA1 Message Date
Brent Vatne aadef09d1e [eas-cli] Pass through platform to export command 2022-09-21 15:30:16 -07:00
Szymon Dziedzic 9f94ddb606 [eas-json] add Xcode 14 image (#1365)
* [eas-json] add XCode 14 image

* bump eas-build-job version

* fix eas.schema.json

* update default and latest images

* update eas-build-job package

* update CHANGELOG.md
2022-09-21 13:43:02 +02:00
Will Schurman 6b101614ba [eas-cli] Update changelog for recent series of stacked commits (#1377) 2022-09-19 15:54:15 -07:00
Will Schurman 811568737b [6/n][eas-cli] Clean up some remaining project utils methods (#1373) 2022-09-19 15:31:07 -07:00
Will Schurman 36b5e0cee8 [5/n][eas-cli] Remove getProjectFullNameAsync (#1372) 2022-09-19 15:28:34 -07:00
Will Schurman 8681437081 [4/n][eas-cli] Remove getProjectAccountName (#1371) 2022-09-19 15:26:41 -07:00
Will Schurman 3e79916462 [2/n][eas-cli] Remove rest of getProjectAccountNameAsync callsites (#1369) 2022-09-19 15:24:13 -07:00
Will Schurman 0613b6c2cd [eas-cli][1/n] Remove some of getProjectAccountNameAsync (#1368) 2022-09-19 15:22:42 -07:00
kirb 63ed6f2b4e [eas-cli] Use confirmation when canceling builds (#1366) 2022-09-19 09:37:47 -07:00
Will Schurman 9985d618ef [eas-cli] Make optional nonInteractive arguments non-optional in utility functions (#1367) 2022-09-19 08:59:35 -07:00
Szymon Dziedzic 46c0ee6423 [eas-cli][ENG-6325] provide suggestions for developers when archive size is large (#1363)
* [eas-cli] provide suggestions for developers when archive size is large

* use learn more function

* apply suggestion from review

* apply suggestions from code review

* update CHANGELOG.md
2022-09-19 10:23:26 +02:00
Will Schurman 5585874c21 [eas-cli] Prompt before configuring EAS project (#1356) 2022-09-16 13:00:26 -07:00
kirb b288c31216 [eas-cli] add pagination for build queries (#1353) 2022-09-16 11:43:57 -07:00
kirb 738c8a21c8 [eas-cli] Add pagination to channel commands (#1352) 2022-09-16 11:32:57 -07:00
Dominik SokalandJuwan Wheatley ff5669ff30 [ENG-4730] support uploading files to EAS Secret (#1354)
* regenerate graphql code

* add support for uploading secret files to EAS Secret

* address PR feedback

* address PR feedback

* regenerate graphql code

* Update packages/eas-cli/src/commands/secret/create.ts

Co-authored-by: Juwan Wheatley <[email protected]>

* address PR feedback

* address PR feedback

* update CHANGELOG.md

Co-authored-by: Juwan Wheatley <[email protected]>
2022-09-16 15:47:22 +02:00
kirb fdf64a8673 [eas-cli] Standardize GQL Update type schema using Fragment (#1348) 2022-09-14 17:00:06 -07:00
kirb 2c7b442f06 [eas-cli] use paginated query API for update commands (#1323) 2022-09-14 16:49:46 -07:00
Dominik Sokal 9c09cb9034 Update CHANGELOG.md 2022-09-12 09:28:21 +02:00
Dominik Sokal e52d14586f [ENG-6129] support json5 in eas.json (#1350)
* support json5 in eas.json

* address PR feedback

* address more PR feedback

* fix test
2022-09-12 09:27:01 +02:00
Szymon Dziedzic 0f7646868f [eas-cli][ENG-6229] add -e as a shortcut for --profile flag (#1342)
* [eas-cli] add `-e` flag as a shortcut for `--profile` flag

* revert readme changes

* add description for profile flag on config command

* update CHANGELOG.md
2022-09-07 19:36:23 +02:00
Justin McBride 6fe4511280 Small updates to the language of the Device Create command (#1346) 2022-09-07 09:28:53 +02:00
Expo CI efc1551d62 update CHANGELOG.md 2022-09-05 12:11:53 +00:00
Dominik Sokal 0cc5b74081 v2.1.0 2022-09-05 14:02:46 +02:00
Wojciech Kozyra 6c6f9b079d [eas-cli] resolve paths from root when checking for gitignored values (#1336) 2022-09-05 14:00:26 +02:00
Dominik Sokal 7da0db0237 Update CHANGELOG.md 2022-09-05 13:56:02 +02:00
Dominik SokalandKim Brandwijk 66f916ac02 [ENG-3865] add support for uploading build artifacts (#1321)
* add support for uploading build artifacts

* handle auto-renaming artifactPath -> applicationArchivePath

* update eas.schema.json

* upgrade @expo/eas-build-job

* rename buildArtifactsPaths -> buildArtifactPaths

* Apply suggestions from code review

Co-authored-by: Kim Brandwijk <[email protected]>

* fix ts error

* address PR feedback

Co-authored-by: Kim Brandwijk <[email protected]>
2022-09-05 13:55:07 +02:00
Bartosz Kaszubowski 0e500ec827 [eas-cli] bump oclif help plugin (#1341)
* [eas-cli] bump oclif help plugin

* update CHANGELOG.md

* cleanup changelog
2022-09-05 13:54:26 +02:00
Dominik Sokal 00f1d24aa5 [ENG-6186] add "eas open" command (#1337)
* add "eas open" command

* update CHANGELOG.md
2022-09-05 11:53:56 +02:00
Dominik Sokal b909409c54 Update CHANGELOG.md 2022-09-05 10:28:46 +02:00
HBiedeandKim Brandwijk d18d7a6e21 Fix typos (#1340)
* Fix typos

* Revert “re-authentication”

Co-authored-by: Kim Brandwijk <[email protected]>

Co-authored-by: Kim Brandwijk <[email protected]>
2022-09-05 10:25:14 +02:00
Quinlan Jung 0131cdbf4b [ENG-6070][cli] generate tvOS credentials for adhoc and prod (#1325)
* [cli] generate tvOS credentials for adhoc and prod

* update CHANGELOG.md

* pr feedback: dsokal

* pr feedback: wojtek

* fix tsc

* tricky mocks
2022-09-02 00:45:09 +02:00
Will Schurman 114190c52e [eas-cli] Add noImplicitOverride to tsconfigs (#1327) 2022-09-01 09:23:08 -07:00
HBiede a06352d150 [eas-cli] Fix viewing branch list when a branch has no associated platforms (#1326) 2022-09-01 09:12:22 -07:00
Expo CI 1969eea8a7 update CHANGELOG.md 2022-09-01 11:40:30 +00:00
Dominik Sokal 8c758fa413 v2.0.0 2022-09-01 13:30:03 +02:00
Dominik Sokal 4b2fb24d1d Update CHANGELOG.md 2022-09-01 13:24:46 +02:00
Dominik Sokal 1637573d72 fix typo in promptToConfigurePushNotifications (#1332)
* fix typo in promptToConfigurePushNotifications

* address PR feedback
2022-09-01 13:20:18 +02:00
Will Schurman 367477f75b [eas-cli] Fix dynamic config update warning for update:configure (#1322) 2022-08-31 14:24:40 -07:00
Expo CI f5d44a08ec update CHANGELOG.md 2022-08-29 09:58:55 +00:00
Dominik Sokal d4ffb07402 v1.2.0 2022-08-29 11:39:02 +02:00
Evan Bacon 0b2b6ddc14 feat(eas-cli): add 2022 Apple capabilities (#1307)
* feat(eas-cli): add 2022 Apple capabilities

* updated tests

* updated tests and third-party

* update CHANGELOG.md

* updated names

* Update CHANGELOG.md
2022-08-29 11:37:00 +02:00
Szymon Dziedzic 0cff5ef83f [eas-cli] warn about EAS outages (#1312)
* [eas-cli] warn users about EAS Build outages

* [eas-cli] warn users about EAS Submit outages

* [eas-cli] show warnings about EAS outages as a first thing when running build and submit commands

* [eas-cli] don't return null as a result of StatuspageServiceQuery

* [eas-cli] remove unneccessary if check

* [eas-cli] rename warnAboutEasOutagesAsync function to maybeWarnAboutEasOutagesAsync and remove please word from output

* [eas-cli] warn about outages when running `eas update` command

* [eas-cli] improve outage warning communicate

* [eas-cli] move maybeWarnAboutEasOutagesAsync call after flags and project dir validation

* [eas-cli] minor refactor after code review

* [eas-cli] change warning message on EAS outage

* [eas-cli] put platform selection and validation logic into ensurePlatformSelectedAsync function

* update CHANGELOG.md

* [eas-cli] use ensurePlatformSelectedAsync in submit command
2022-08-29 11:29:51 +02:00
Dominik Sokal 396c45269e add eas.json option to skip push notifications setup (#1315)
* add eas.json option to skip push notifications setup

* address PR feedback

* update CHANGELOG.md
2022-08-29 10:19:43 +02:00
Brent Vatne 98ad9798c5 Copy changes to EAS Submit output (#1318)
* Change e-mail -> email

* Update link to build to point to TestFlight

* update CHANGELOG.md
2022-08-29 09:30:20 +02:00
Dominik Sokal b4741293ba improve instructions on setting eas.projectId in app.json (#1316)
* improve instructions on setting eas.projectId in app.json

* address PR feedback

* update CHANGELOG.md
2022-08-26 09:49:41 +02:00
Dominik SokalandBrent Vatne 43a6aa826b [ENG-6125] print warning when eas-cli is installed as dependency (#1310)
* print warning when eas-cli is installed as dependency

* address PR feedback

* Apply suggestions from code review

Co-authored-by: Brent Vatne <[email protected]>

* PR feedback

* Apply suggestions from code review

Co-authored-by: Brent Vatne <[email protected]>

* update CHANGELOG.md

Co-authored-by: Brent Vatne <[email protected]>
2022-08-26 09:24:47 +02:00
Wojciech Kozyra e0a9384e56 update CHANGELOG 2022-08-25 10:21:57 +02:00
Dominik Sokal b5ca8c63d9 add new common submit error 2022-08-25 10:18:10 +02:00
Will Schurman c98401131a [eas-cli] Fix EnvironmentSecretsQuery param name (#1305) 2022-08-24 08:18:16 -07:00
Will Schurman c1cd0f584f [eas-cli] Use AppQuery instead of ProjectQuery (#1309) 2022-08-24 08:16:04 -07:00
Will Schurman f82aae7fc9 [eas-cli] Replace promptToCreateProjectIfNotExistsAsync with getProjectIdAsync (#1303) 2022-08-24 08:14:06 -07:00
Dominik Sokal c349ad7a40 Update CHANGELOG.md 2022-08-24 09:07:03 +02:00
Expo CI 3500a4d5ec update CHANGELOG.md 2022-08-23 19:43:43 +00:00
229 changed files with 7717 additions and 3521 deletions

No files matched your search

+71 -3
View File
@@ -6,6 +6,76 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
- Prompt before configuring EAS project. ([#1356](https://github.com/expo/eas-cli/pull/1356) by [@wschurman](https://github.com/wschurman))
### 🎉 New features
- Add `-e` as a shortcut for `--profile` flag. ([#1342](https://github.com/expo/eas-cli/pull/1342) by [@szdziedzic](https://github.com/szdziedzic))
- Support JSON5 in **eas.json**. ([#1350](https://github.com/expo/eas-cli/pull/1350) by [@dsokal](https://github.com/dsokal))
- Add pagination and interactivity to update commands. ([#1323](https://github.com/expo/eas-cli/pull/1323) by [@kgc00](https://github.com/kgc00))
- Add support for uploading files to EAS Secret. ([#1354](https://github.com/expo/eas-cli/pull/1354) by [@dsokal](https://github.com/dsokal))
- Add pagination to channel commands. ([#1352](https://github.com/expo/eas-cli/pull/1352) by [@kgc00](https://github.com/kgc00))
- 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))
### 🐛 Bug fixes
### 🧹 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))
## [2.1.0](https://github.com/expo/eas-cli/releases/tag/v2.1.0) - 2022-09-05
### 🎉 New features
- Add tvOS credentials compatibility for Adhoc and App store builds. ([#1325](https://github.com/expo/eas-cli/pull/1325) by [@quinlanj](https://github.com/quinlanj))
- Add `eas open` command for opening project page in web browser. ([#1337](https://github.com/expo/eas-cli/pull/1337) by [@dsokal](https://github.com/dsokal))
- Add support for buildArtifactPaths. Rename artifactPath to applicationArchivePath. ([#1321](https://github.com/expo/eas-cli/pull/1321) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix viewing branch list when a branch has no associated platforms. ([#1326](https://github.com/expo/eas-cli/pull/1326) by [@hbiede](https://github.com/hbiede))
- Fix description of `help` command in help prompt. ([#1341](https://github.com/expo/eas-cli/pull/1341) by [@Simek](https://github.com/Simek))
- Resolve paths from the root when checking for gitignored values. ([#1336](https://github.com/expo/eas-cli/pull/1336) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Add noImplicitOverride to tsconfigs. ([#1327](https://github.com/expo/eas-cli/pull/1327) by [@wschurman](https://github.com/wschurman))
- Fix typos. ([#1340](https://github.com/expo/eas-cli/pull/1340) by [@hbiede](https://github.com/hbiede))
## [2.0.0](https://github.com/expo/eas-cli/releases/tag/v2.0.0) - 2022-09-01
### 🛠 Breaking changes
- Fix typo in `cli.promptToConfigurePushNotifications` field in **eas.json**. EAS CLI will throw an error if the old field name is in **eas.json**. ([#1332](https://github.com/expo/eas-cli/pull/1332) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix dynamic config update warning. ([#1322](https://github.com/expo/eas-cli/pull/1322) by [@wschurman](https://github.com/wschurman))
## [1.2.0](https://github.com/expo/eas-cli/releases/tag/v1.2.0) - 2022-08-29
### 🎉 New features
- Add 2022 Apple capabilities. ([#1307](https://github.com/expo/eas-cli/pull/1307) by [@EvanBacon](https://github.com/EvanBacon))
- Print warning when eas-cli is installed as project dependency. ([#1310](https://github.com/expo/eas-cli/pull/1310) by [@dsokal](https://github.com/dsokal))
- Add eas.json to skip push notifications credentials setup. ([#1315](https://github.com/expo/eas-cli/pull/1315) by [@dsokal](https://github.com/dsokal))
- Warn about EAS outages when running `build`, `submit`, and `update` commands. ([#1312](https://github.com/expo/eas-cli/pull/1312) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Replace promptToCreateProjectIfNotExistsAsync with getProjectIdAsync. ([#1303](https://github.com/expo/eas-cli/pull/1303) by [@wschurman](https://github.com/wschurman))
- Use AppQuery instead of ProjectQuery. ([#1304](https://github.com/expo/eas-cli/pull/1304) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Improve instructions on setting `extra.eas.projectId` in app configuration. ([#1316](https://github.com/expo/eas-cli/pull/1316) by [@dsokal](https://github.com/dsokal))
- Improve copy in EAS Submit - "e-mail" -> email, and make the link to app on App Store go directly to the TestFlight tab. ([#1318](https://github.com/expo/eas-cli/pull/1318) by [@brentvatne](https://github.com/brentvatne))
## [1.1.1](https://github.com/expo/eas-cli/releases/tag/v1.1.1) - 2022-08-23
### 🎉 New features
- Add support for `autoIncrement` option at the root of the build profile. ([#1298](https://github.com/expo/eas-cli/pull/1298) by [@szdziedzic](https://github.com/szdziedzic))
@@ -13,8 +83,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- Handle trailing backslash in `.gitignore`. ([#1306](https://github.com/expo/eas-cli/pull/1306) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Retry ASC Api Key downloads if it has not fully propagated on Apple's infrastructure. ([#1302](https://github.com/expo/eas-cli/pull/1302) by [@quinlanj](https://github.com/quinlanj))
## [1.1.0](https://github.com/expo/eas-cli/releases/tag/v1.1.0) - 2022-08-23
@@ -24,7 +93,6 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- Retry ASC Api Key downloads if it has not fully propagated on Apple's infrastructure. ([#1302](https://github.com/expo/eas-cli/pull/1302) by [@quinlanj](https://github.com/quinlanj))
- Prevent throwing dynamic app config write error when configuring project ID. ([#1301](https://github.com/expo/eas-cli/pull/1301) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "1.1.1"
"version": "2.1.0"
}
+86 -79
View File
@@ -19,6 +19,28 @@ npm install -g eas-cli
yarn global add eas-cli
```
## Enforcing eas-cli version for your project
If you want to enforce the `eas-cli` version for your project, use the `"cli.version"` field in **eas.json**. Installing `eas-cli` to your project dependencies is strongly discouraged because it may cause dependency conflicts that are difficult to debug.
An example of **eas.json** that enforces `eas-cli` in version `1.0.0` or newer:
```json
{
"cli": {
"version": ">=1.0.0"
},
"build": {
// build profiles
},
"submit": {
// submit profiles
}
}
```
Learn more: https://docs.expo.dev/build-reference/eas-json/
# Usage
```sh
@@ -58,19 +80,18 @@ eas --help COMMAND
* [`eas channel:view [NAME]`](#eas-channelview-name)
* [`eas config`](#eas-config)
* [`eas credentials`](#eas-credentials)
* [`eas database:create`](#eas-databasecreate)
* [`eas device:create`](#eas-devicecreate)
* [`eas device:delete`](#eas-devicedelete)
* [`eas device:list`](#eas-devicelist)
* [`eas device:view [UDID]`](#eas-deviceview-udid)
* [`eas diagnostics`](#eas-diagnostics)
* [`eas functions:start`](#eas-functionsstart)
* [`eas help [COMMAND]`](#eas-help-command)
* [`eas init`](#eas-init)
* [`eas login`](#eas-login)
* [`eas logout`](#eas-logout)
* [`eas metadata:pull`](#eas-metadatapull)
* [`eas metadata:push`](#eas-metadatapush)
* [`eas open`](#eas-open)
* [`eas project:info`](#eas-projectinfo)
* [`eas project:init`](#eas-projectinit)
* [`eas secret:create`](#eas-secretcreate)
@@ -104,7 +125,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -121,7 +142,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -138,7 +159,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v1.1.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]`
@@ -152,7 +173,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v1.1.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]`
@@ -201,7 +222,7 @@ DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v1.1.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]`
@@ -221,7 +242,7 @@ DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -242,7 +263,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -261,7 +282,7 @@ DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v1.1.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]`
@@ -285,7 +306,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v1.1.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,7 +336,7 @@ DESCRIPTION
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v1.1.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]`
@@ -329,7 +350,7 @@ DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -346,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/v1.1.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`
@@ -381,7 +402,7 @@ 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/v1.1.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,7 +435,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -461,7 +482,7 @@ DESCRIPTION
Update version of an app.
```
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -480,7 +501,7 @@ 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/v1.1.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]`
@@ -497,7 +518,7 @@ DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v1.1.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]`
@@ -517,7 +538,7 @@ DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v1.1.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]`
@@ -538,7 +559,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -555,7 +576,7 @@ DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v1.1.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]`
@@ -575,7 +596,7 @@ DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -593,7 +614,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -610,21 +631,7 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v1.1.1/packages/eas-cli/src/commands/credentials.ts)_
## `eas database:create`
create a database
```
USAGE
$ eas database:create
DESCRIPTION
create a database
```
_See code: [src/commands/database/create.ts](https://github.com/expo/eas-cli/blob/v1.1.1/packages/eas-cli/src/commands/database/create.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`
@@ -638,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/v1.1.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`
@@ -656,7 +663,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -673,7 +680,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v1.1.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]`
@@ -687,7 +694,7 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -701,25 +708,11 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v1.1.1/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas functions:start`
start a function
```
USAGE
$ eas functions:start
DESCRIPTION
start a function
```
_See code: [src/commands/functions/start.ts](https://github.com/expo/eas-cli/blob/v1.1.1/packages/eas-cli/src/commands/functions/start.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]`
Display help for eas.
display help for eas-cli
```
USAGE
@@ -732,10 +725,10 @@ FLAGS
-n, --nested-commands Include all nested commands in the output.
DESCRIPTION
Display help for eas.
display help for eas-cli
```
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.2.0/src/commands/help.ts)_
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.3.0/src/commands/help.ts)_
## `eas init`
@@ -797,7 +790,7 @@ DESCRIPTION
generate the local store configuration from the app stores
```
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -814,7 +807,21 @@ DESCRIPTION
sync the local store configuration to the app stores
```
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
open the project page in a web browser
```
USAGE
$ eas open
DESCRIPTION
open the project page in a web browser
```
_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`
@@ -828,7 +835,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -845,7 +852,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -865,7 +872,7 @@ 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/v1.1.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`
@@ -882,7 +889,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -896,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/v1.1.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`
@@ -926,7 +933,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -956,7 +963,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -973,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/v1.1.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`
@@ -993,7 +1000,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -1012,7 +1019,7 @@ DESCRIPTION
view the recent updates for a branch
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -1032,7 +1039,7 @@ DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -1052,7 +1059,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v1.1.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]`
@@ -1069,7 +1076,7 @@ DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -1086,7 +1093,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -1107,7 +1114,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
@@ -1124,7 +1131,7 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v1.1.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`
+2209 -52
View File
File diff suppressed because it is too large. Load diff
+6 -5
View File
@@ -1,27 +1,27 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "1.1.1",
"version": "2.1.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.33",
"@expo/apple-utils": "0.0.0-alpha.34",
"@expo/code-signing-certificates": "0.0.2",
"@expo/config": "7.0.1",
"@expo/config-plugins": "5.0.1",
"@expo/config-types": "46.0.2",
"@expo/eas-build-job": "0.2.85",
"@expo/eas-json": "1.1.1",
"@expo/eas-build-job": "0.2.94",
"@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",
"@expo/pkcs12": "0.0.8",
"@expo/plist": "0.0.18",
"@expo/plugin-autocomplete": "1.4.0",
"@expo/plugin-help": "5.2.0",
"@expo/plugin-help": "5.3.0",
"@expo/plugin-warn-if-update-available": "2.3.0",
"@expo/prebuild-config": "5.0.3",
"@expo/results": "1.0.0",
@@ -32,6 +32,7 @@
"@urql/core": "2.6.1",
"@urql/exchange-retry": "0.3.3",
"ajv": "^6.12.6",
"better-opn": "3.0.2",
"chalk": "4.1.2",
"cli-progress": "3.11.2",
"cli-table3": "0.6.2",
@@ -1,60 +0,0 @@
import { Actor } from '../user/User';
interface MockProject {
projectRoot: string;
projectTree: Record<string, string>;
appJSON: AppJSON;
packageJSON: PackageJSON;
}
interface PackageJSON {
name: string;
version: string;
description: string;
main: string;
}
interface AppJSON {
expo: {
name: string;
version: string;
slug: string;
sdkVersion: string;
owner?: string;
[key: string]: any;
};
}
export function createTestProject(
user: Actor,
appJSONExtraData?: Record<string, object | string>
): MockProject {
const projectRoot = '/test-project';
const packageJSON: PackageJSON = {
name: 'testing123',
version: '0.1.0',
description: 'fake description',
main: 'index.js',
};
const owner = appJSONExtraData?.owner as string | undefined;
const appJSON: AppJSON = {
expo: {
name: 'testing 123',
version: '0.1.0',
slug: 'testing-123',
sdkVersion: '33.0.0',
owner: owner ?? (user.__typename === 'User' ? user.username : undefined),
...appJSONExtraData,
},
};
return {
appJSON,
packageJSON,
projectRoot,
projectTree: {
[projectRoot + '/package.json']: JSON.stringify(packageJSON, null, 2),
[projectRoot + '/app.json']: JSON.stringify(appJSON, null, 2),
},
};
}
+1 -1
View File
@@ -4,7 +4,7 @@ import fetch, { RequestError, RequestInit } from './fetch';
import { getAccessToken, getSessionSecret } from './user/sessionStorage';
export class ApiV2Error extends Error {
readonly name = 'ApiV2Error';
override readonly name = 'ApiV2Error';
readonly expoApiV2ErrorCode: string;
readonly expoApiV2ErrorDetails?: JSONValue;
readonly expoApiV2ErrorServerStack?: string;
+52 -37
View File
@@ -5,7 +5,7 @@ import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { UpdateBranchFragment } from '../graphql/generated';
import { BranchQuery } from '../graphql/queries/BranchQuery';
import Log from '../log';
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../update/utils';
import { UPDATE_COLUMNS, formatUpdateMessage, getPlatformsForGroup } from '../update/utils';
import { printJsonOnlyOutput } from '../utils/json';
import {
paginatedQueryWithConfirmPromptAsync,
@@ -14,24 +14,29 @@ import {
export const BRANCHES_LIMIT = 50;
export async function selectBranchFromPaginatedQueryAsync(
projectId: string,
promptTitle: string,
options: PaginatedQueryOptions
): Promise<UpdateBranchFragment> {
if (options.nonInteractive) {
export async function selectBranchOnAppAsync({
projectId,
promptTitle,
displayTextForListItem,
paginatedQueryOptions,
}: {
projectId: string;
displayTextForListItem: (queryItem: UpdateBranchFragment) => string;
promptTitle: string;
paginatedQueryOptions: PaginatedQueryOptions;
}): Promise<UpdateBranchFragment> {
if (paginatedQueryOptions.nonInteractive) {
throw new Error('Unable to select a branch in non-interactive mode.');
}
const selectedBranch = await paginatedQueryWithSelectPromptAsync({
limit: options.limit ?? BRANCHES_LIMIT,
offset: options.offset,
queryToPerform: (limit, offset) => queryBranchesForProjectAsync(limit, offset, projectId),
limit: paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) => queryBranchesOnProjectAsync(limit, offset, projectId),
promptOptions: {
title: promptTitle,
getIdentifierForQueryItem: updateBranchFragment => updateBranchFragment.id,
createDisplayTextForSelectionPromptListItem: updateBranchFragment =>
updateBranchFragment.name,
createDisplayTextForSelectionPromptListItem: displayTextForListItem,
},
});
if (!selectedBranch) {
@@ -40,36 +45,39 @@ export async function selectBranchFromPaginatedQueryAsync(
return selectedBranch;
}
export async function listAndRenderPaginatedBranchesAsync(
projectId: string,
options: PaginatedQueryOptions
): Promise<void> {
if (options.nonInteractive) {
const branches = await queryBranchesForProjectAsync(
options.limit ?? BRANCHES_LIMIT,
options.offset,
export async function listAndRenderBranchesOnAppAsync({
projectId,
paginatedQueryOptions,
}: {
projectId: string;
paginatedQueryOptions: PaginatedQueryOptions;
}): Promise<void> {
if (paginatedQueryOptions.nonInteractive) {
const branches = await queryBranchesOnProjectAsync(
paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
paginatedQueryOptions.offset,
projectId
);
renderPageOfBranches(branches, options);
renderPageOfBranches(branches, paginatedQueryOptions);
} else {
await paginatedQueryWithConfirmPromptAsync({
limit: options.limit ?? BRANCHES_LIMIT,
offset: options.offset,
queryToPerform: (limit, offset) => queryBranchesForProjectAsync(limit, offset, projectId),
limit: paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) => queryBranchesOnProjectAsync(limit, offset, projectId),
promptOptions: {
title: 'Load more branches?',
renderListItems: branches => renderPageOfBranches(branches, options),
renderListItems: branches => renderPageOfBranches(branches, paginatedQueryOptions),
},
});
}
}
async function queryBranchesForProjectAsync(
async function queryBranchesOnProjectAsync(
limit: number,
offset: number,
projectId: string
): Promise<UpdateBranchFragment[]> {
return await BranchQuery.listBranchesAsync({
return await BranchQuery.listBranchesOnAppAsync({
appId: projectId,
limit,
offset,
@@ -89,16 +97,23 @@ function renderPageOfBranches(
});
table.push(
...currentPage.map(branch => [
branch.name,
formatUpdate(branch.updates[0]),
branch.updates[0]?.runtimeVersion ?? 'N/A',
branch.updates[0]?.group ?? 'N/A',
getPlatformsForGroup({
updates: branch.updates,
group: branch.updates[0]?.group,
}),
])
...currentPage.map(branch => {
if (branch.updates.length === 0) {
return [branch.name, 'N/A', 'N/A', 'N/A', 'N/A'];
}
const latestUpdateOnBranch = branch.updates[0];
return [
branch.name,
formatUpdateMessage(latestUpdateOnBranch),
latestUpdateOnBranch.runtimeVersion,
latestUpdateOnBranch.group,
getPlatformsForGroup({
group: latestUpdateOnBranch.group,
updates: branch.updates,
}),
];
})
);
Log.addNewLineIfNone();
+14
View File
@@ -0,0 +1,14 @@
import { getVcsClient } from '../vcs';
export async function getDefaultBranchNameAsync(): Promise<string> {
return (
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substring(2, 4)}`
);
}
export class BranchNotFoundError extends Error {
constructor(message?: string) {
super(message ?? 'Branch not found.');
}
}
@@ -0,0 +1,83 @@
import { v4 as uuid } from 'uuid';
import { selectBuildToCancelAsync } from '../../commands/build/cancel';
import { AppPlatform, BuildFragment, BuildPriority, BuildStatus } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { confirmAsync, selectAsync } from '../../prompts';
jest.mock('../../ora', () => ({
ora: jest.fn().mockImplementation(() => ({
start: jest.fn().mockImplementation(() => ({
stop: jest.fn(),
fail: jest.fn(),
})),
})),
}));
jest.mock('../../prompts', () => {
return {
selectAsync: jest.fn(),
confirmAsync: jest.fn(),
};
});
jest.mock('../../graphql/queries/BuildQuery', () => {
const actual = jest.requireActual('../../graphql/queries/BuildQuery');
return {
BuildQuery: {
...actual.BuildQuery,
viewBuildsOnAppAsync: jest.fn(),
},
};
});
describe(selectBuildToCancelAsync.name, () => {
const selectedBuildId = uuid();
const projectId = uuid();
beforeEach(() => {
jest
.mocked(BuildQuery.viewBuildsOnAppAsync)
.mockImplementation(async () => [
createMockBuildFragment({ projectId, buildId: selectedBuildId }),
]);
});
it('does not return build id when confirmation is rejected', async () => {
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
expect(selectBuildToCancelAsync(projectId, 'blah')).resolves.toEqual(null);
});
it('returns build id when confirmation is confirmed', async () => {
jest.mocked(selectAsync).mockResolvedValueOnce(selectedBuildId);
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
expect(selectBuildToCancelAsync(projectId, 'blah')).resolves.toEqual(selectedBuildId);
});
});
function createMockBuildFragment({
projectId,
buildId,
}: {
projectId: string;
buildId?: string;
}): BuildFragment {
return {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
platform: AppPlatform.Android,
id: buildId ?? uuid(),
priority: BuildPriority.Normal,
project: {
__typename: 'App',
slug: 'test-project',
id: projectId,
name: 'test-project',
ownerAccount: {
__typename: 'Account',
id: uuid(),
name: 'test-account',
},
},
status: BuildStatus.InQueue,
};
}
@@ -73,17 +73,17 @@ describe(bumpVersionAsync, () => {
await bumpVersionAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/repo',
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('3.0.0');
expect(fakeExp.android?.versionCode).toBe(124);
expect(appJSON.expo.version).toBe('3.0.0');
expect(appJSON.expo.android.versionCode).toBe(124);
const buildGradle = await getAppBuildGradleAsync('/repo');
const buildGradle = await getAppBuildGradleAsync('/app');
assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('124');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
@@ -95,17 +95,17 @@ describe(bumpVersionAsync, () => {
await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
projectDir: '/repo',
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('3.0.1');
expect(fakeExp.android?.versionCode).toBe(123);
expect(appJSON.expo.version).toBe('3.0.1');
expect(appJSON.expo.android.versionCode).toBe(123);
const buildGradle = await getAppBuildGradleAsync('/repo');
const buildGradle = await getAppBuildGradleAsync('/app');
assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.1');
@@ -116,17 +116,17 @@ describe(bumpVersionAsync, () => {
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
projectDir: '/repo',
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('3.0.0');
expect(fakeExp.android?.versionCode).toBe(123);
expect(appJSON.expo.version).toBe('3.0.0');
expect(appJSON.expo.android.versionCode).toBe(123);
const buildGradle = await getAppBuildGradleAsync('/repo');
const buildGradle = await getAppBuildGradleAsync('/app');
assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
@@ -140,11 +140,11 @@ describe(bumpVersionInAppJsonAsync, () => {
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/repo',
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('5.0.0');
expect(fakeExp.android?.versionCode).toBe(127);
expect(appJSON.expo.version).toBe('5.0.0');
@@ -156,11 +156,11 @@ describe(bumpVersionInAppJsonAsync, () => {
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
projectDir: '/repo',
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('5.0.1');
expect(fakeExp.android?.versionCode).toBe(126);
expect(appJSON.expo.version).toBe('5.0.1');
@@ -172,11 +172,11 @@ describe(bumpVersionInAppJsonAsync, () => {
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.NOOP,
projectDir: '/repo',
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('5.0.0');
expect(fakeExp.android?.versionCode).toBe(126);
expect(appJSON.expo.version).toBe('5.0.0');
@@ -189,7 +189,7 @@ describe(maybeResolveVersionsAsync, () => {
it('reads the versions from native code', async () => {
const exp = initBareWorkflowProject();
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/repo',
'/app',
exp,
{} as BuildProfile<Platform.ANDROID>
);
@@ -201,7 +201,7 @@ describe(maybeResolveVersionsAsync, () => {
it('reads the versions from expo config', async () => {
const exp = initManagedProject();
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/repo',
'/app',
exp,
{} as BuildProfile<Platform.ANDROID>
);
@@ -240,7 +240,7 @@ function initBareWorkflowProject({
}`,
'./android/app/src/main/AndroidManifest.xml': 'fake',
},
'/repo'
'/app'
);
return fakeExp;
@@ -261,7 +261,7 @@ function initManagedProject(): ExpoConfig {
expo: fakeExp,
}),
},
'/repo'
'/app'
);
return fakeExp;
@@ -92,6 +92,7 @@ export async function prepareAndroidBuildAsync(
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? false
: ctx.buildProfile.autoIncrement,
nonInteractive: ctx.nonInteractive,
});
},
prepareJobAsync: async (
@@ -15,7 +15,8 @@ export function transformJob(job: Android.Job): AndroidJobInput {
cache: job.cache,
version: job.version?.versionCode ? { versionCode: job.version.versionCode } : undefined,
gradleCommand: job.gradleCommand,
artifactPath: job.artifactPath,
applicationArchivePath: job.applicationArchivePath,
buildArtifactPaths: job.buildArtifactPaths,
username: job.username,
buildType: job.buildType && transformBuildType(job.buildType),
developmentClient: job.developmentClient,
@@ -24,7 +24,10 @@ export async function prepareJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Job> {
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const username = getUsername(
ctx.exp,
await ensureLoggedInAsync({ nonInteractive: ctx.nonInteractive })
);
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
@@ -72,7 +75,8 @@ export async function prepareJobAsync(
updates: { channel: ctx.buildProfile.channel },
developmentClient: buildProfile.developmentClient,
gradleCommand: buildProfile.gradleCommand,
artifactPath: buildProfile.artifactPath,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths,
buildType,
username,
...(ctx.android.versionCodeOverride && {
@@ -16,10 +16,12 @@ export async function syncProjectConfigurationAsync({
projectDir,
exp,
localAutoIncrement,
nonInteractive,
}: {
projectDir: string;
exp: ExpoConfig;
localAutoIncrement?: AndroidVersionAutoIncrement;
nonInteractive: boolean;
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
@@ -27,7 +29,7 @@ export async function syncProjectConfigurationAsync({
if (workflow === Workflow.GENERIC) {
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
await syncUpdatesConfigurationAsync(projectDir, exp, { nonInteractive });
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
} else {
+11
View File
@@ -177,6 +177,17 @@ async function uploadProjectAsync<TPlatform extends Platform>(
)}`
);
const projectTarball = await makeProjectTarballAsync();
if (projectTarball.size > 1024 * 1024 * 100) {
Log.warn(
`Your project archive is ${formatBytes(
projectTarball.size
)}. You can reduce its size and the time it takes to upload by excluding files that are unnecessary for the build process in ${chalk.bold(
'.easignore'
)} file. ${learnMore('https://expo.fyi/eas-build-archive')}`
);
}
projectTarballPath = projectTarball.path;
const { bucketKey } = await uploadFileAtPathToS3Async(
+3 -3
View File
@@ -1,5 +1,5 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJson, EasJsonReader } from '@expo/eas-json';
import { EasJson, EasJsonAccessor } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
@@ -24,7 +24,7 @@ interface ConfigureParams {
export async function ensureProjectConfiguredAsync(
configureParams: ConfigureParams
): Promise<boolean> {
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(configureParams.projectDir))) {
if (await fs.pathExists(EasJsonAccessor.formatEasJsonPath(configureParams.projectDir))) {
return false;
}
@@ -89,7 +89,7 @@ const EAS_JSON_BARE_DEFAULT: EasJson = {
};
async function createEasJsonAsync(projectDir: string): Promise<void> {
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
const easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir);
const hasAndroidNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
+10 -7
View File
@@ -10,9 +10,8 @@ import { Analytics, BuildEvent } from '../analytics/events';
import { CredentialsContext } from '../credentials/context';
import { BuildResourceClass } from '../graphql/generated';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
import { getOwnerAccountForProjectIdAsync, getProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { findAccountByName } from '../user/Account';
import { ensureLoggedInAsync } from '../user/actions';
import { createAndroidContextAsync } from './android/build';
import { BuildContext, CommonContext } from './context';
@@ -46,12 +45,15 @@ export async function createBuildContextAsync<T extends Platform>({
}): Promise<BuildContext<T>> {
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
const user = await ensureLoggedInAsync();
const accountName = getProjectAccountName(exp, user);
const user = await ensureLoggedInAsync({ nonInteractive });
const projectName = exp.slug;
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
const projectId = await getProjectIdAsync(exp, {
env: buildProfile.env,
nonInteractive,
});
const account = await getOwnerAccountForProjectIdAsync(projectId);
const workflow = await resolveWorkflowAsync(projectDir, platform);
const accountId = findAccountByName(user.accounts, accountName)?.id;
const accountId = account.id;
const runFromCI = getenv.boolish('CI', false);
const credentialsCtx = new CredentialsContext({
@@ -60,6 +62,7 @@ export async function createBuildContextAsync<T extends Platform>({
projectDir,
user,
env: buildProfile.env,
easJsonCliConfig,
});
const devClientProperties = getDevClientEventProperties({
@@ -80,7 +83,7 @@ export async function createBuildContextAsync<T extends Platform>({
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
const commonContext: CommonContext<T> = {
accountName,
accountName: account.name,
buildProfile,
buildProfileName,
resourceClass,
+1
View File
@@ -80,6 +80,7 @@ export async function prepareIosBuildAsync(
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? false
: ctx.buildProfile.autoIncrement,
nonInteractive: ctx.nonInteractive,
});
},
prepareJobAsync: async (
@@ -1,7 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
import { getAppFromContextAsync } from '../../credentials/ios/actions/BuildCredentialsUtils';
import { IosCredentials, Target } from '../../credentials/ios/types';
import { CredentialsResult } from '../build';
import { BuildContext } from '../context';
@@ -16,7 +16,7 @@ export async function ensureIosCredentialsAsync(
}
const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, {
app: getAppFromContext(buildCtx.credentialsCtx),
app: await getAppFromContextAsync(buildCtx.credentialsCtx),
targets,
distribution: buildCtx.buildProfile.distribution ?? 'store',
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
+3 -4
View File
@@ -1,4 +1,4 @@
import { Env, Ios } from '@expo/eas-build-job';
import { Ios } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
@@ -17,7 +17,8 @@ export function transformJob(job: Ios.Job): IosJobInput {
version: job.version?.buildNumber ? { buildNumber: job.version.buildNumber } : undefined,
scheme: job.scheme,
buildConfiguration: job.buildConfiguration,
artifactPath: job.artifactPath,
applicationArchivePath: job.applicationArchivePath,
buildArtifactPaths: job.buildArtifactPaths,
username: job.username,
developmentClient: job.developmentClient,
simulator: job.simulator,
@@ -27,7 +28,6 @@ export function transformJob(job: Ios.Job): IosJobInput {
function transformIosSecrets(secrets: {
buildCredentials?: Ios.BuildCredentials;
env?: Env;
}): IosJobSecretsInput {
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
@@ -43,6 +43,5 @@ function transformIosSecrets(secrets: {
return {
buildCredentials,
environmentSecrets: secrets.env,
};
}
+7 -2
View File
@@ -26,7 +26,10 @@ export async function prepareJobAsync(
): Promise<Job> {
const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const username = getUsername(
ctx.exp,
await ensureLoggedInAsync({ nonInteractive: ctx.nonInteractive })
);
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
if (jobData.credentials) {
const targetNames = Object.keys(jobData.credentials);
@@ -64,7 +67,9 @@ export async function prepareJobAsync(
simulator: ctx.buildProfile.simulator,
scheme: jobData.buildScheme,
buildConfiguration: ctx.buildProfile.buildConfiguration,
artifactPath: ctx.buildProfile.artifactPath,
applicationArchivePath:
ctx.buildProfile.applicationArchivePath ?? ctx.buildProfile.artifactPath,
buildArtifactPaths: ctx.buildProfile.buildArtifactPaths,
username,
...(ctx.ios.buildNumberOverride && {
version: {
@@ -13,18 +13,20 @@ export async function syncProjectConfigurationAsync({
exp,
targets,
localAutoIncrement,
nonInteractive,
}: {
projectDir: string;
exp: ExpoConfig;
targets: Target[];
localAutoIncrement?: IosVersionAutoIncrement;
nonInteractive: boolean;
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
await syncUpdatesConfigurationAsync(projectDir, exp, { nonInteractive });
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, targets });
} else {
+4 -1
View File
@@ -49,7 +49,10 @@ export async function collectMetadataAsync<T extends Platform>(
(await vcsClient.getLastCommitMessageAsync()) ?? undefined
),
isGitWorkingTreeDirty: await vcsClient.hasUncommittedChangesAsync(),
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
username: getUsername(
ctx.exp,
await ensureLoggedInAsync({ nonInteractive: ctx.nonInteractive })
),
message: ctx.message,
...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
+70
View File
@@ -0,0 +1,70 @@
import chalk from 'chalk';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { BuildFilter, BuildFragment } from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log from '../log';
import { printJsonOnlyOutput } from '../utils/json';
import { paginatedQueryWithConfirmPromptAsync } from '../utils/queries';
import { formatGraphQLBuild } from './utils/formatBuild';
export const BUILDS_LIMIT = 50;
export async function listAndRenderBuildsOnAppAsync({
projectId,
projectDisplayName,
filter,
paginatedQueryOptions,
}: {
projectId: string;
projectDisplayName: string;
filter?: BuildFilter;
paginatedQueryOptions: PaginatedQueryOptions;
}): Promise<void> {
if (paginatedQueryOptions.nonInteractive) {
const builds = await BuildQuery.viewBuildsOnAppAsync({
appId: projectId,
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
offset: paginatedQueryOptions.offset,
filter,
});
renderPageOfBuilds({ builds, projectDisplayName, paginatedQueryOptions });
} else {
await paginatedQueryWithConfirmPromptAsync({
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
BuildQuery.viewBuildsOnAppAsync({
appId: projectId,
limit,
offset,
filter,
}),
promptOptions: {
title: 'Load more builds?',
renderListItems: builds =>
renderPageOfBuilds({ builds, projectDisplayName, paginatedQueryOptions }),
},
});
}
}
function renderPageOfBuilds({
builds,
projectDisplayName,
paginatedQueryOptions,
}: {
builds: BuildFragment[];
projectDisplayName: string;
paginatedQueryOptions: PaginatedQueryOptions;
}): void {
if (paginatedQueryOptions.json) {
printJsonOnlyOutput(builds);
} else {
const list = builds.map(build => formatGraphQLBuild(build)).join(`\n\n${chalk.dim('———')}\n\n`);
Log.addNewLineIfNone();
Log.log(chalk.bold(`Builds for ${projectDisplayName}:`));
Log.log(`\n${list}`);
}
}
@@ -3,7 +3,8 @@ import {
AppVersionSource,
BuildProfile,
EasJson,
EasJsonReader,
EasJsonAccessor,
EasJsonUtils,
SubmitProfile,
} from '@expo/eas-json';
import chalk from 'chalk';
@@ -94,13 +95,14 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
projectDir,
nonInteractive: flags.nonInteractive,
});
const easJsonReader = new EasJsonReader(projectDir);
const easJsonCliConfig: EasJson['cli'] = (await easJsonReader.getCliConfigAsync()) ?? {};
const easJsonAccessor = new EasJsonAccessor(projectDir);
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
const platforms = toPlatforms(flags.requestedPlatform);
const buildProfiles = await getProfilesAsync({
type: 'build',
easJsonReader,
easJsonAccessor,
platforms,
profileName: flags.profile ?? undefined,
});
@@ -150,7 +152,7 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) {
const submitProfiles = await getProfilesAsync({
easJsonReader,
easJsonAccessor,
platforms,
profileName: flags.submitProfile,
type: 'submit',
@@ -70,7 +70,11 @@ export async function commitPromptAsync({
initial: initialCommitMessage,
validate: (input: string) => input !== '',
});
await getVcsClient().commitAsync({ commitAllFiles, commitMessage: message });
await getVcsClient().commitAsync({
commitAllFiles,
commitMessage: message,
nonInteractive: false,
});
}
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
+194
View File
@@ -0,0 +1,194 @@
import chalk from 'chalk';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import {
ViewBranchesOnUpdateChannelQueryVariables,
ViewUpdateChannelsOnAppQueryVariables,
} from '../graphql/generated';
import { BranchQuery, UpdateBranchOnChannelObject } from '../graphql/queries/BranchQuery';
import { ChannelQuery, UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import Log from '../log';
import formatFields from '../utils/formatFields';
import { printJsonOnlyOutput } from '../utils/json';
import {
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
import { logChannelDetails } from './utils';
export const CHANNELS_LIMIT = 25;
export async function selectChannelOnAppAsync({
projectId,
selectionPromptTitle,
paginatedQueryOptions,
}: {
projectId: string;
selectionPromptTitle: string;
paginatedQueryOptions: PaginatedQueryOptions;
}): Promise<UpdateChannelObject> {
if (paginatedQueryOptions.nonInteractive) {
throw new Error('Unable to select a channel in non-interactive mode.');
}
const updateChannel = await paginatedQueryWithSelectPromptAsync({
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) => queryChannelsOnAppAsync({ appId: projectId, limit, offset }),
promptOptions: {
title: selectionPromptTitle,
createDisplayTextForSelectionPromptListItem: updateChannel => updateChannel.name,
getIdentifierForQueryItem: updateChannel => updateChannel.id,
},
});
if (!updateChannel) {
throw new Error(`Could not find any channels for app "${projectId}"`);
}
return updateChannel;
}
export async function listAndRenderChannelsOnAppAsync({
projectId,
paginatedQueryOptions,
}: {
projectId: string;
paginatedQueryOptions: PaginatedQueryOptions;
}): Promise<void> {
if (paginatedQueryOptions.nonInteractive) {
const channels = await queryChannelsOnAppAsync({
appId: projectId,
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
offset: paginatedQueryOptions.offset,
});
renderPageOfChannels(channels, paginatedQueryOptions);
} else {
await paginatedQueryWithConfirmPromptAsync({
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
queryChannelsOnAppAsync({ limit, offset, appId: projectId }),
promptOptions: {
title: 'Load more channels?',
renderListItems: channels => renderPageOfChannels(channels, paginatedQueryOptions),
},
});
}
}
export async function listAndRenderBranchesAndUpdatesOnChannelAsync({
projectId: appId,
channelName,
paginatedQueryOptions,
}: {
projectId: string;
channelName: string;
paginatedQueryOptions: PaginatedQueryOptions;
}): Promise<void> {
const channel = await ChannelQuery.viewUpdateChannelAsync({ appId, channelName });
renderChannelHeaderContent({ channelName: channel.name, channelId: channel.id });
if (paginatedQueryOptions.nonInteractive) {
const branches = await queryBranchesAndUpdateGroupsOnChannelAsync({
appId,
channelName,
offset: paginatedQueryOptions.offset,
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
});
renderPageOfBranchesOnChannel(channel, branches, paginatedQueryOptions);
} else {
await paginatedQueryWithConfirmPromptAsync({
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
queryBranchesAndUpdateGroupsOnChannelAsync({
channelName,
appId,
offset,
limit,
}),
promptOptions: {
title: 'Load more channels?',
renderListItems: branches =>
renderPageOfBranchesOnChannel(channel, branches, paginatedQueryOptions),
},
});
}
}
async function queryChannelsOnAppAsync({
appId,
offset,
limit,
}: ViewUpdateChannelsOnAppQueryVariables): Promise<UpdateChannelObject[]> {
return await ChannelQuery.viewUpdateChannelsOnAppAsync({
appId,
offset,
limit,
});
}
async function queryBranchesAndUpdateGroupsOnChannelAsync(
args: ViewBranchesOnUpdateChannelQueryVariables
): Promise<UpdateBranchOnChannelObject[]> {
return await BranchQuery.listBranchesOnChannelAsync(args);
}
function renderPageOfChannels(
currentPage: UpdateChannelObject[],
{ json }: PaginatedQueryOptions
): void {
if (json) {
printJsonOnlyOutput({ currentPage });
} else {
for (const channel of currentPage) {
Log.addNewLineIfNone();
Log.log(
formatFields([
{ label: 'Name', value: channel.name },
{ label: 'ID', value: channel.id },
])
);
logChannelDetails(channel);
}
}
}
function renderPageOfBranchesOnChannel(
channel: UpdateChannelObject,
currentPage: UpdateBranchOnChannelObject[],
{ json }: PaginatedQueryOptions
): void {
const channelWithNewBranches = { ...channel, updateBranches: currentPage };
if (json) {
printJsonOnlyOutput({ currentPage: channelWithNewBranches });
} else {
Log.addNewLineIfNone();
Log.log(
formatFields([
{ label: 'Name', value: channel.name },
{ label: 'ID', value: channel.id },
])
);
logChannelDetails(channelWithNewBranches);
}
}
function renderChannelHeaderContent({
channelName,
channelId,
}: {
channelName: string;
channelId: string;
}): void {
Log.addNewLineIfNone();
Log.log(chalk.bold('Channel:'));
Log.log(
formatFields([
{ label: 'Name', value: channelName },
{ label: 'ID', value: channelId },
])
);
Log.addNewLineIfNone();
Log.log(chalk`{bold Branches pointed at this channel and their most recent update group:}`);
}
+112
View File
@@ -0,0 +1,112 @@
import assert from 'assert';
import CliTable from 'cli-table3';
import { UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import Log from '../log';
import { UPDATE_COLUMNS, getUpdateGroupDescriptionsWithBranch } from '../update/utils';
export type BranchMapping = {
version: number;
data: {
branchId: string;
branchMappingLogic: {
operand: number;
clientKey: string;
branchMappingOperator: string;
} & string;
}[];
};
/**
* Get the branch mapping and determine whether it is a rollout.
* Ensure that the branch mapping is properly formatted.
*/
export function getBranchMapping(branchMappingString?: string): {
branchMapping: BranchMapping;
isRollout: boolean;
rolloutPercent?: number;
} {
if (!branchMappingString) {
throw new Error('Missing branch mapping.');
}
let branchMapping: BranchMapping;
try {
branchMapping = JSON.parse(branchMappingString);
} catch {
throw new Error(`Could not parse branchMapping string into a JSON: "${branchMappingString}"`);
}
assert(branchMapping, 'Branch Mapping must be defined.');
if (branchMapping.version !== 0) {
throw new Error('Branch mapping must be version 0.');
}
const isRollout = branchMapping.data.length === 2;
const rolloutPercent = branchMapping.data[0].branchMappingLogic.operand;
switch (branchMapping.data.length) {
case 0:
break;
case 1:
if (branchMapping.data[0].branchMappingLogic !== 'true') {
throw new Error('Branch mapping logic for a single branch must be "true"');
}
break;
case 2:
if (branchMapping.data[0].branchMappingLogic.clientKey !== 'rolloutToken') {
throw new Error('Client key of initial branch mapping must be "rolloutToken"');
}
if (branchMapping.data[0].branchMappingLogic.branchMappingOperator !== 'hash_lt') {
throw new Error('Branch mapping operator of initial branch mapping must be "hash_lt"');
}
if (rolloutPercent == null) {
throw new Error('Branch mapping is missing a "rolloutPercent"');
}
if (branchMapping.data[1].branchMappingLogic !== 'true') {
throw new Error('Branch mapping logic for a the second branch of a rollout must be "true"');
}
break;
default:
throw new Error('Branch mapping data must have length less than or equal to 2.');
}
return { branchMapping, isRollout, rolloutPercent };
}
export function logChannelDetails(channel: UpdateChannelObject): void {
const { branchMapping, isRollout, rolloutPercent } = getBranchMapping(channel.branchMapping);
const updateChannelsTable = new CliTable({
head: ['branch', ...(isRollout ? ['rollout percent'] : []), ...UPDATE_COLUMNS],
wordWrap: true,
});
if (branchMapping.data.length > 2) {
throw new Error('Branch Mapping data must have length less than or equal to 2.');
}
const rolloutBranchIds = branchMapping.data.map(data => data.branchId);
for (const currentBranch of channel.updateBranches) {
const updateGroupDescriptions = getUpdateGroupDescriptionsWithBranch(
currentBranch.updateGroups
);
const isRolloutBranch = isRollout && rolloutBranchIds.includes(currentBranch.id);
const isBaseBranch = rolloutBranchIds.length > 0 && rolloutBranchIds[0] === currentBranch.id;
updateGroupDescriptions.forEach(({ branch, message, runtimeVersion, group, platforms }) => {
updateChannelsTable.push([
branch,
...(isRolloutBranch
? [isBaseBranch ? `${rolloutPercent! * 100}%` : `${(1 - rolloutPercent!) * 100}%`]
: []),
message,
runtimeVersion,
group,
platforms,
]);
});
}
Log.log(updateChannelsTable.toString());
}
@@ -1,5 +1,9 @@
import { EasJsonReader } from '@expo/eas-json';
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 {
@@ -8,6 +12,7 @@ import {
initAsync as initAnalyticsAsync,
logEvent,
} from '../analytics/rudderstackClient';
import { learnMore } from '../log';
import { findProjectRootAsync } from '../project/projectUtils';
import { getUserAsync } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
@@ -28,8 +33,11 @@ export default abstract class EasCommand extends Command {
// eslint-disable-next-line async-protect/async-suffix
async run(): Promise<any> {
await initAnalyticsAsync();
if (this.mustBeRunInsideProject) {
await this.applyCliConfigAsync();
const projectDir = await findProjectRootAsync();
await this.applyCliConfigAsync(projectDir);
await this.ensureEasCliIsNotInDependenciesAsync(projectDir);
}
if (this.requiresAuthentication) {
@@ -48,15 +56,14 @@ export default abstract class EasCommand extends Command {
}
// eslint-disable-next-line async-protect/async-suffix
async finally(err: Error): Promise<any> {
override async finally(err: Error): Promise<any> {
await flushAnalyticsAsync();
return super.finally(err);
}
private async applyCliConfigAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const easJsonReader = new EasJsonReader(projectDir);
const config = await easJsonReader.getCliConfigAsync();
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})`
@@ -66,4 +73,50 @@ export default abstract class EasCommand extends Command {
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
);
}
}
@@ -43,7 +43,7 @@ beforeEach(() => {
const createTestEasCommand = (authValue: boolean): typeof EasCommand => {
class TestEasCommand extends EasCommand {
requiresAuthentication = authValue;
override requiresAuthentication = authValue;
async runAsync(): Promise<void> {}
}
@@ -0,0 +1,23 @@
import { Flags } from '@oclif/core';
export const EasNonInteractiveAndJsonFlags = {
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
dependsOn: ['non-interactive'],
}),
'non-interactive': Flags.boolean({
description: 'Run the command in non-interactive mode.',
}),
};
export const EASNonInteractiveFlag = {
'non-interactive': Flags.boolean({
description: 'Run the command in non-interactive mode.',
}),
};
export const EasJsonOnlyFlag = {
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
}),
};
+21 -15
View File
@@ -1,13 +1,17 @@
import { Flags } from '@oclif/core';
import { EasNonInteractiveAndJsonFlags } from './flags';
export const getPaginatedQueryOptions = (
flags: Record<keyof typeof EasPaginatedQueryFlags, any>
flags: Partial<
Record<keyof typeof EasPaginatedQueryFlags | keyof typeof EasNonInteractiveAndJsonFlags, any>
>
): PaginatedQueryOptions => {
return {
json: flags.json ?? false,
...('limit' in flags && { limit: flags.limit }),
offset: flags.offset ?? 0,
nonInteractive: flags['non-interactive'] ?? false,
...('limit' in flags && { limit: flags.limit }),
json: flags.json ?? false,
};
};
@@ -27,6 +31,19 @@ const parseFlagInputStringAsInteger = (
return inputAsNumber;
};
export const getLimitFlagWithCustomValues = ({
defaultTo,
limit,
}: {
defaultTo: number;
limit: number;
}): any =>
Flags.integer({
description: `The number of items to fetch each query. Defaults to ${defaultTo} and is capped at ${limit}.`,
// eslint-disable-next-line async-protect/async-suffix
parse: async input => parseFlagInputStringAsInteger(input, 'limit', 1, limit),
});
export const EasPaginatedQueryFlags = {
offset: Flags.integer({
description: 'Start queries from specified index. Use for paginating results. Defaults to 0.',
@@ -34,18 +51,7 @@ export const EasPaginatedQueryFlags = {
parse: async input =>
parseFlagInputStringAsInteger(input, 'offset', 0, Number.MAX_SAFE_INTEGER),
}),
limit: Flags.integer({
description:
'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.',
// eslint-disable-next-line async-protect/async-suffix
parse: async input => parseFlagInputStringAsInteger(input, 'limit', 1, 100),
}),
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
}),
'non-interactive': Flags.boolean({
description: 'Run the command in non-interactive mode.',
}),
limit: getLimitFlagWithCustomValues({ defaultTo: 50, limit: 100 }),
};
// options required to control a paginated query
@@ -3,11 +3,11 @@ import Log from '../../log';
import { showLoginPromptAsync } from '../../user/actions';
export default class AccountLogin extends EasCommand {
static description = 'log in with your Expo account';
static aliases = ['login'];
static override description = 'log in with your Expo account';
static override aliases = ['login'];
protected mustBeRunInsideProject = false;
protected requiresAuthentication = false;
protected override mustBeRunInsideProject = false;
protected override requiresAuthentication = false;
async runAsync(): Promise<void> {
await showLoginPromptAsync();
@@ -3,11 +3,11 @@ import Log from '../../log';
import { logoutAsync } from '../../user/User';
export default class AccountLogout extends EasCommand {
static description = 'log out';
static aliases = ['logout'];
static override description = 'log out';
static override aliases = ['logout'];
protected mustBeRunInsideProject = false;
protected requiresAuthentication = false;
protected override mustBeRunInsideProject = false;
protected override requiresAuthentication = false;
async runAsync(): Promise<void> {
await logoutAsync();
@@ -5,11 +5,11 @@ import Log from '../../log';
import { getActorDisplayName, getUserAsync } from '../../user/User';
export default class AccountView extends EasCommand {
static description = 'show the username you are logged in as';
static aliases = ['whoami'];
static override description = 'show the username you are logged in as';
static override aliases = ['whoami'];
protected mustBeRunInsideProject = false;
protected requiresAuthentication = false;
protected override mustBeRunInsideProject = false;
protected override requiresAuthentication = false;
async runAsync(): Promise<void> {
const user = await getUserAsync();
+3 -3
View File
@@ -3,11 +3,11 @@ import Log from '../log';
import UserSettings from '../user/UserSettings';
export default class AnalyticsView extends EasCommand {
static description = 'display or change analytics settings';
static override description = 'display or change analytics settings';
static args = [{ name: 'STATUS', options: ['on', 'off'] }];
static override args = [{ name: 'STATUS', options: ['on', 'off'] }];
protected requiresAuthentication = false;
protected override requiresAuthentication = false;
async runAsync(): Promise<void> {
const { STATUS: status } = (await this.parse(AnalyticsView)).args;
+17 -20
View File
@@ -1,8 +1,9 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
import { getDefaultBranchNameAsync } from '../../branch/utils';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
CreateUpdateBranchForAppMutation,
@@ -13,12 +14,11 @@ import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { getVcsClient } from '../../vcs';
export async function createUpdateBranchOnAppAsync({
appId,
@@ -52,9 +52,9 @@ export async function createUpdateBranchOnAppAsync({
}
export default class BranchCreate extends EasCommand {
static description = 'create a branch';
static override description = 'create a branch';
static args = [
static override args = [
{
name: 'name',
required: false,
@@ -62,50 +62,47 @@ export default class BranchCreate extends EasCommand {
},
];
static flags = {
json: Flags.boolean({
description: 'return a json with the new branch ID and name.',
default: false,
}),
static override flags = {
...EasNonInteractiveAndJsonFlags,
};
async runAsync(): Promise<void> {
let {
args: { name },
flags,
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
} = await this.parse(BranchCreate);
if (flags.json) {
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
if (!name) {
const validationMessage = 'Branch name may not be empty.';
if (flags.json) {
if (nonInteractive) {
throw new Error(validationMessage);
}
({ name } = await promptAsync({
type: 'text',
name: 'name',
message: 'Provide a branch name:',
initial:
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substr(2, 4)}`,
initial: await getDefaultBranchNameAsync(),
validate: value => (value ? true : validationMessage),
}));
}
const newBranch = await createUpdateBranchOnAppAsync({ appId: projectId, name });
if (flags.json) {
if (jsonFlag) {
printJsonOnlyOutput(newBranch);
} else {
Log.withTick(
`️Created a new branch: ${chalk.bold(newBranch.name)} on project ${chalk.bold(fullName)}.`
`️Created a new branch: ${chalk.bold(newBranch.name)} on project ${chalk.bold(
projectDisplayName
)}.`
);
}
}
+31 -27
View File
@@ -1,8 +1,10 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
import { selectBranchOnAppAsync } from '../../branch/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
DeleteUpdateBranchMutation,
@@ -15,10 +17,10 @@ import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { toggleConfirmAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
async function getBranchInfoAsync({
@@ -77,65 +79,65 @@ async function deleteBranchOnAppAsync({
}
export default class BranchDelete extends EasCommand {
static description = 'delete a branch';
static override description = 'delete a branch';
static args = [
static override args = [
{
name: 'name',
required: false,
description: 'Name of the branch to delete',
},
];
static flags = {
json: Flags.boolean({
description: `return JSON with the edited branch's ID and name.`,
default: false,
}),
static override flags = {
...EasNonInteractiveAndJsonFlags,
};
async runAsync(): Promise<void> {
let {
args: { name },
flags: { json: jsonFlag },
args: { name: branchName },
flags,
} = await this.parse(BranchDelete);
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
if (!name) {
if (!branchName) {
const validationMessage = 'branch name may not be empty.';
if (jsonFlag) {
if (nonInteractive) {
throw new Error(validationMessage);
}
({ name } = await promptAsync({
type: 'text',
name: 'name',
message: 'Provide the name of the branch to delete:',
validate: value => (value ? true : validationMessage),
({ name: branchName } = await selectBranchOnAppAsync({
projectId,
displayTextForListItem: updateBranch => updateBranch.name,
promptTitle: 'Which branch would you like to delete?',
paginatedQueryOptions,
}));
}
const data = await getBranchInfoAsync({ appId: projectId, name });
const data = await getBranchInfoAsync({ appId: projectId, name: branchName });
const branchId = data.app?.byId.updateBranchByName?.id;
if (!branchId) {
throw new Error(`Could not find branch ${name} on ${fullName}`);
throw new Error(`Could not find branch ${branchName} on ${projectDisplayName}`);
}
if (!jsonFlag) {
if (!nonInteractive) {
Log.addNewLineIfNone();
Log.warn(
`You are about to permamently delete branch: "${name}" and all of the updates published on it.` +
`You are about to permanently delete branch: "${branchName}" and all of the updates published on it.` +
`\nThis action is irreversible.`
);
Log.newLine();
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
if (!confirmed) {
Log.error(`Cancelled deletion of branch: "${name}".`);
Log.error(`Cancelled deletion of branch: "${branchName}".`);
process.exit(1);
}
}
@@ -148,7 +150,9 @@ export default class BranchDelete extends EasCommand {
printJsonOnlyOutput(deletionResult);
} else {
Log.withTick(
`️Deleted branch "${name}" and all of its updates on project ${chalk.bold(fullName)}.`
`️Deleted branch "${branchName}" and all of its updates on project ${chalk.bold(
projectDisplayName
)}.`
);
}
}
+11 -7
View File
@@ -1,28 +1,32 @@
import { listAndRenderPaginatedBranchesAsync } from '../../branch/queries';
import { listAndRenderBranchesOnAppAsync } from '../../branch/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { enableJsonOutput } from '../../utils/json';
export default class BranchList extends EasCommand {
static description = 'list all branches';
static override description = 'list all branches';
static flags = {
static override flags = {
...EasPaginatedQueryFlags,
...EasNonInteractiveAndJsonFlags,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(BranchList);
const options = getPaginatedQueryOptions(flags);
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
if (options.json) {
if (paginatedQueryOptions.json) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
await listAndRenderPaginatedBranchesAsync(projectId, options);
const projectId = await getProjectIdAsync(exp, {
nonInteractive: flags['non-interactive'],
});
await listAndRenderBranchesOnAppAsync({ projectId, paginatedQueryOptions });
}
}
@@ -1,8 +1,8 @@
import EasCommand from '../../commandUtils/EasCommand';
export default class BranchPublish extends EasCommand {
static description = 'deprecated, use "eas update"';
static hidden = true;
static override description = 'deprecated, use "eas update"';
static override hidden = true;
async runAsync(): Promise<void> {
throw new Error(BranchPublish.description);
+11 -13
View File
@@ -3,6 +3,7 @@ import chalk from 'chalk';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
EditUpdateBranchInput,
@@ -14,7 +15,7 @@ import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
@@ -52,9 +53,9 @@ async function renameUpdateBranchOnAppAsync({
}
export default class BranchRename extends EasCommand {
static description = 'rename a branch';
static override description = 'rename a branch';
static flags = {
static override flags = {
from: Flags.string({
description: 'current name of the branch.',
required: false,
@@ -63,15 +64,12 @@ export default class BranchRename extends EasCommand {
description: 'new name of the branch.',
required: false,
}),
json: Flags.boolean({
description: `return a json with the edited branch's ID and name.`,
default: false,
}),
...EasNonInteractiveAndJsonFlags,
};
async runAsync(): Promise<void> {
let {
flags: { json: jsonFlag, from: currentName, to: newName },
flags: { json: jsonFlag, from: currentName, to: newName, 'non-interactive': nonInteractive },
} = await this.parse(BranchRename);
if (jsonFlag) {
enableJsonOutput();
@@ -79,12 +77,12 @@ export default class BranchRename extends EasCommand {
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
if (!currentName) {
const validationMessage = 'current name may not be empty.';
if (jsonFlag) {
if (nonInteractive) {
throw new Error(validationMessage);
}
({ currentName } = await promptAsync({
@@ -97,7 +95,7 @@ export default class BranchRename extends EasCommand {
if (!newName) {
const validationMessage = 'new name may not be empty.';
if (jsonFlag) {
if (nonInteractive) {
throw new Error(validationMessage);
}
({ newName } = await promptAsync({
@@ -120,7 +118,7 @@ export default class BranchRename extends EasCommand {
Log.withTick(
`️Renamed branch from ${currentName} to ${chalk.bold(
editedBranch.name
)} on project ${chalk.bold(fullName)}.`
)} on project ${chalk.bold(projectDisplayName)}.`
);
}
}
+33 -17
View File
@@ -1,15 +1,20 @@
import { selectBranchFromPaginatedQueryAsync } from '../../branch/queries';
import { selectBranchOnAppAsync } from '../../branch/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import {
EasPaginatedQueryFlags,
getLimitFlagWithCustomValues,
getPaginatedQueryOptions,
} from '../../commandUtils/pagination';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { listAndRenderUpdatesOnBranchByNameAsync } from '../../update/queries';
import { listAndRenderUpdateGroupsOnBranchAsync } from '../../update/queries';
import { enableJsonOutput } from '../../utils/json';
export default class BranchView extends EasCommand {
static description = 'view a branch';
static override description = 'view a branch';
static args = [
static override args = [
{
name: 'name',
required: false,
@@ -17,8 +22,10 @@ export default class BranchView extends EasCommand {
},
];
static flags = {
static override flags = {
...EasPaginatedQueryFlags,
...EasNonInteractiveAndJsonFlags,
limit: getLimitFlagWithCustomValues({ defaultTo: 25, limit: 50 }),
};
async runAsync(): Promise<void> {
@@ -26,30 +33,39 @@ export default class BranchView extends EasCommand {
args: { name: branchName },
flags,
} = await this.parse(BranchView);
const options = getPaginatedQueryOptions(flags);
const { 'non-interactive': nonInteractive } = flags;
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
if (options.json) {
if (paginatedQueryOptions.json) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
// provide help to a user if they ran the command with missing args
if (!branchName) {
if (options.nonInteractive) {
if (nonInteractive) {
throw new Error('Branch name may not be empty.');
}
({ name: branchName } = await selectBranchFromPaginatedQueryAsync(
({ name: branchName } = await selectBranchOnAppAsync({
projectId,
'Which branch would you like to view?',
// discard limit and offset because this query is not those flag's intended target
{ json: options.json, nonInteractive: options.nonInteractive, offset: 0 }
));
promptTitle: 'Which branch would you like to view?',
displayTextForListItem: updateBranch => updateBranch.name,
// discard limit and offset because this query is not their intended target
paginatedQueryOptions: {
json: paginatedQueryOptions.json,
nonInteractive,
offset: 0,
},
}));
}
await listAndRenderUpdatesOnBranchByNameAsync(projectId, branchName, options);
await listAndRenderUpdateGroupsOnBranchAsync({
projectId,
branchName,
paginatedQueryOptions,
});
}
}
+52 -23
View File
@@ -2,6 +2,7 @@ import chalk from 'chalk';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
Build,
@@ -16,7 +17,7 @@ import { appPlatformEmojis } from '../../platform';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { confirmAsync, selectAsync } from '../../prompts';
@@ -59,36 +60,45 @@ function formatUnfinishedBuild(
return `${platform} Started at: ${startTime}, Status: ${status}, Id: ${build.id}`;
}
async function selectBuildToCancelAsync(
export async function selectBuildToCancelAsync(
projectId: string,
projectFullName: string
projectDisplayName: string
): Promise<string | null> {
const spinner = ora().start('Fetching the uncompleted builds…');
let builds;
try {
const [newBuilds, inQueueBuilds, inProgressBuilds] = await Promise.all([
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.New } }),
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.InQueue } }),
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.InProgress } }),
BuildQuery.viewBuildsOnAppAsync({
appId: projectId,
offset: 0,
limit: 10,
filter: { status: BuildStatus.New },
}),
BuildQuery.viewBuildsOnAppAsync({
appId: projectId,
offset: 0,
limit: 10,
filter: { status: BuildStatus.InQueue },
}),
BuildQuery.viewBuildsOnAppAsync({
appId: projectId,
offset: 0,
limit: 10,
filter: { status: BuildStatus.InProgress },
}),
]);
spinner.stop();
builds = [...newBuilds, ...inQueueBuilds, ...inProgressBuilds];
} catch (error) {
spinner.fail(
`Something went wrong and we couldn't fetch the builds for the project ${projectFullName}.`
`Something went wrong and we couldn't fetch the builds for the project ${projectDisplayName}.`
);
throw error;
}
if (builds.length === 0) {
Log.warn(`There aren't any uncompleted builds for the project ${projectFullName}.`);
Log.warn(`There aren't any uncompleted builds for the project ${projectDisplayName}.`);
return null;
} else if (builds.length === 1) {
Log.log('Found one build');
Log.log(formatUnfinishedBuild(builds[0]));
await confirmAsync({
message: 'Do you want to cancel it?',
});
return builds[0].id;
} else {
const buildId = await selectAsync<string>(
'Which build do you want to cancel?',
@@ -97,7 +107,12 @@ async function selectBuildToCancelAsync(
value: build.id,
}))
);
return buildId;
return (await confirmAsync({
message: 'Are you sure you want to cancel it?',
}))
? buildId
: null;
}
}
@@ -110,25 +125,39 @@ async function ensureBuildExistsAsync(buildId: string): Promise<void> {
}
export default class BuildCancel extends EasCommand {
static description = 'cancel a build';
static override description = 'cancel a build';
static args = [{ name: 'BUILD_ID' }];
static override args = [{ name: 'BUILD_ID' }];
static override flags = {
...EASNonInteractiveFlag,
};
async runAsync(): Promise<void> {
const { BUILD_ID: buildIdFromArg } = (await this.parse(BuildCancel)).args;
const {
args: { BUILD_ID: buildIdFromArg },
flags: { 'non-interactive': nonInteractive },
} = await this.parse(BuildCancel);
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectFullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const displayName = await getDisplayNameForProjectIdAsync(projectId);
if (buildIdFromArg) {
await ensureBuildExistsAsync(buildIdFromArg);
}
const buildId = buildIdFromArg || (await selectBuildToCancelAsync(projectId, projectFullName));
let buildId: string | null = buildIdFromArg;
if (!buildId) {
return;
if (nonInteractive) {
throw new Error('BUILD_ID must not be empty in non-interactive mode');
}
buildId = await selectBuildToCancelAsync(projectId, displayName);
if (!buildId) {
return;
}
}
const spinner = ora().start('Canceling the build…');
@@ -16,9 +16,9 @@ import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } fro
import { getVcsClient } from '../../vcs';
export default class BuildConfigure extends EasCommand {
static description = 'configure the project to support EAS Build';
static override description = 'configure the project to support EAS Build';
static flags = {
static override flags = {
platform: Flags.enum({
description: 'Platform to configure',
char: 'p',
@@ -60,14 +60,14 @@ export default class BuildConfigure extends EasCommand {
if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
await syncAndroidUpdatesConfigurationAsync(projectDir, exp);
await syncAndroidUpdatesConfigurationAsync(projectDir, exp, { nonInteractive: false });
}
}
if ([RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
await syncIosUpdatesConfigurationAsync(projectDir, exp);
await syncIosUpdatesConfigurationAsync(projectDir, exp, { nonInteractive: false });
}
}
}
+138 -29
View File
@@ -1,13 +1,22 @@
import { Platform } from '@expo/eas-build-job';
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import figures from 'figures';
import path from 'path';
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import { UserInputResourceClass } from '../../build/types';
import EasCommand from '../../commandUtils/EasCommand';
import Log from '../../log';
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';
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
interface RawBuildFlags {
platform?: string;
@@ -27,9 +36,9 @@ interface RawBuildFlags {
}
export default class Build extends EasCommand {
static description = 'start a build';
static override description = 'start a build';
static flags = {
static override flags = {
platform: Flags.enum({
char: 'p',
options: ['android', 'ios', 'all'],
@@ -38,23 +47,16 @@ export default class Build extends EasCommand {
default: false,
hidden: true,
}),
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
default: false,
}),
'skip-project-configuration': Flags.boolean({
default: false,
hidden: true,
}),
profile: Flags.string({
char: 'e',
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
}),
'non-interactive': Flags.boolean({
default: false,
description: 'Run command in non-interactive mode',
}),
local: Flags.boolean({
default: false,
description: 'Run build locally [experimental]',
@@ -91,21 +93,37 @@ export default class Build extends EasCommand {
char: 'm',
description: 'A short message describing the build',
}),
...EasNonInteractiveAndJsonFlags,
};
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(Build);
if (rawFlags.json) {
enableJsonOutput();
}
const flags = await this.sanitizeFlagsAsync(rawFlags);
const flags = this.sanitizeFlags(rawFlags);
const projectDir = await findProjectRootAsync();
await runBuildAndSubmitAsync(projectDir, flags);
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
if (!flags.localBuildOptions.enable) {
await maybeWarnAboutEasOutagesAsync(
flags.autoSubmit
? [StatuspageServiceName.EasBuild, StatuspageServiceName.EasSubmit]
: [StatuspageServiceName.EasBuild]
);
}
const flagsWithPlatform = await this.ensurePlatformSelectedAsync(flags);
await runBuildAndSubmitAsync(projectDir, flagsWithPlatform);
}
private async sanitizeFlagsAsync(flags: RawBuildFlags): Promise<BuildFlags> {
private sanitizeFlags(
flags: RawBuildFlags
): Omit<BuildFlags, 'requestedPlatform'> & { requestedPlatform?: RequestedPlatform } {
const nonInteractive = flags['non-interactive'];
if (!flags.local && flags.output) {
Errors.error('--output is allowed only for local builds', { exit: 1 });
@@ -117,21 +135,11 @@ export default class Build extends EasCommand {
Errors.error('--json is allowed only when building in non-interactive mode', { exit: 1 });
}
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
if (flags.local) {
if (flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined) {
// TODO: implement this
Errors.error('Auto-submits are not yet supported when building locally', { exit: 1 });
}
if (requestedPlatform === RequestedPlatform.All) {
Errors.error('Builds for multiple platforms are not supported with flag --local', {
exit: 1,
});
} else if (process.platform !== 'darwin' && requestedPlatform === RequestedPlatform.Ios) {
Errors.error('Unsupported platform, macOS is required to build apps for iOS', { exit: 1 });
}
}
const requestedPlatform =
flags.platform &&
Object.values(RequestedPlatform).includes(flags.platform.toLowerCase() as RequestedPlatform)
? (flags.platform.toLowerCase() as RequestedPlatform)
: undefined;
if (flags['skip-credentials-check']) {
Log.warnDeprecatedFlag(
@@ -176,4 +184,105 @@ export default class Build extends EasCommand {
message,
};
}
private async ensurePlatformSelectedAsync(
flags: Omit<BuildFlags, 'requestedPlatform'> & { requestedPlatform?: RequestedPlatform }
): Promise<BuildFlags> {
const requestedPlatform = await selectRequestedPlatformAsync(flags.requestedPlatform);
if (flags.localBuildOptions.enable) {
if (flags.autoSubmit) {
// TODO: implement this
Errors.error('Auto-submits are not yet supported when building locally', { exit: 1 });
}
if (requestedPlatform === RequestedPlatform.All) {
Errors.error('Builds for multiple platforms are not supported with flag --local', {
exit: 1,
});
} else if (process.platform !== 'darwin' && requestedPlatform === RequestedPlatform.Ios) {
Errors.error('Unsupported platform, macOS is required to build apps for iOS', { exit: 1 });
}
}
return {
...flags,
requestedPlatform,
};
}
}
async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
const easJsonAccessor = new EasJsonAccessor(projectDir);
const profileNames = await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor);
const platformAndProfileNames: [Platform, string][] = profileNames.flatMap(profileName => [
[Platform.ANDROID, profileName],
[Platform.IOS, profileName],
]);
const deprecatedProfiles: [Platform, string][] = [];
for (const [platform, profileName] of platformAndProfileNames) {
const buildProfile = await EasJsonUtils.getBuildProfileAsync(
easJsonAccessor,
platform,
profileName
);
if (buildProfile.artifactPath) {
deprecatedProfiles.push([platform, profileName]);
}
}
if (deprecatedProfiles.length === 0) {
return;
}
const deprecatedProfileNames = uniq(deprecatedProfiles.map(([, profileName]) => profileName));
Log.warn(`Some of your build profiles use deprecated field ${chalk.bold('artifactPath')}:`);
for (const profileName of deprecatedProfileNames) {
Log.warn(`- ${profileName}`);
}
Log.newLine();
if (nonInteractive) {
Log.warn(
`${figures.warning} Action required: rename ${chalk.bold('artifactPath')} to ${chalk.bold(
'applicationArchivePath'
)} in all of the build profiles listed above.`
);
Log.warn(
`See ${link('https://docs.expo.dev/build-reference/eas-json/')} for more information.`
);
Log.warn(
`This warning will become an error in a future EAS CLI release. This build will continue to use the ${chalk.bold(
'artifactPath'
)} setting.`
);
return;
}
const rename = await selectAsync('Do you want us to handle renaming the field for you?', [
{ title: 'Yes', value: true },
{ title: 'No, I will edit eas.json manually (EAS CLI exits)', value: false },
]);
if (!rename) {
Errors.exit(1);
}
await easJsonAccessor.readRawJsonAsync();
for (const [platform, profileName] of deprecatedProfiles) {
easJsonAccessor.patch(easJsonRawObject => {
easJsonRawObject.build[profileName][platform].applicationArchivePath =
easJsonRawObject.build[profileName][platform].artifactPath;
delete easJsonRawObject.build[profileName][platform].artifactPath;
return easJsonRawObject;
});
}
await easJsonAccessor.writeAsync();
Log.withTick('Updated eas.json');
}
@@ -25,16 +25,17 @@ const STAGE_DESCRIPTION = `Stage of the build you want to inspect.
post-build - builds the native project and leaves the output directory for inspection`;
export default class BuildInspect extends EasCommand {
static description =
static override description =
'inspect the state of the project at specific build stages, useful for troubleshooting';
static flags = {
static override flags = {
platform: Flags.enum({
char: 'p',
options: [RequestedPlatform.Android, RequestedPlatform.Ios],
required: true,
}),
profile: Flags.string({
char: 'e',
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
+38 -63
View File
@@ -1,36 +1,35 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { BUILDS_LIMIT, listAndRenderBuildsOnAppAsync } from '../../build/queries';
import { BuildDistributionType, BuildStatus } from '../../build/types';
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import {
EasPaginatedQueryFlags,
getLimitFlagWithCustomValues,
getPaginatedQueryOptions,
} from '../../commandUtils/pagination';
import {
AppPlatform,
DistributionType,
BuildStatus as GraphQLBuildStatus,
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { enableJsonOutput } from '../../utils/json';
export default class BuildList extends EasCommand {
static description = 'list all builds for your project';
static override description = 'list all builds for your project';
static flags = {
static override flags = {
platform: Flags.enum({
options: [RequestedPlatform.All, RequestedPlatform.Android, RequestedPlatform.Ios],
}),
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
}),
status: Flags.enum({
options: [
BuildStatus.NEW,
@@ -56,19 +55,22 @@ export default class BuildList extends EasCommand {
appIdentifier: Flags.string(),
buildProfile: Flags.string(),
gitCommitHash: Flags.string(),
limit: Flags.integer(),
...EasPaginatedQueryFlags,
limit: getLimitFlagWithCustomValues({ defaultTo: 10, limit: BUILDS_LIMIT }),
...EasNonInteractiveAndJsonFlags,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildList);
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
const {
json,
json: jsonFlag,
platform: requestedPlatform,
status: buildStatus,
distribution: buildDistribution,
limit = 10,
'non-interactive': nonInteractive,
} = flags;
if (json) {
if (jsonFlag) {
enableJsonOutput();
}
@@ -78,54 +80,27 @@ export default class BuildList extends EasCommand {
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const displayName = await getDisplayNameForProjectIdAsync(projectId);
const spinner = ora().start('Fetching the build list for the project…');
try {
const builds = await BuildQuery.allForAppAsync(projectId, {
limit,
filter: {
platform,
status: graphqlBuildStatus,
distribution: graphqlBuildDistribution,
channel: flags.channel,
appVersion: flags.appVersion,
appBuildVersion: flags.appBuildVersion,
sdkVersion: flags.sdkVersion,
runtimeVersion: flags.runtimeVersion,
appIdentifier: flags.appIdentifier,
buildProfile: flags.buildProfile,
gitCommitHash: flags.gitCommitHash,
},
});
if (builds.length) {
if (platform || graphqlBuildStatus) {
spinner.succeed(
`Showing ${builds.length} matching builds for the project ${projectName}`
);
} else {
spinner.succeed(`Showing last ${builds.length} builds for the project ${projectName}`);
}
if (json) {
printJsonOnlyOutput(builds);
} else {
const list = builds
.map(build => formatGraphQLBuild(build))
.join(`\n\n${chalk.dim('———')}\n\n`);
Log.log(`\n${list}`);
}
} else {
spinner.fail(`Couldn't find any builds for the project ${projectName}`);
}
} catch (e) {
spinner.fail(`Something went wrong and we couldn't fetch the build list ${projectName}`);
throw e;
}
await listAndRenderBuildsOnAppAsync({
projectId,
projectDisplayName: displayName,
filter: {
platform,
status: graphqlBuildStatus,
distribution: graphqlBuildDistribution,
channel: flags.channel,
appVersion: flags.appVersion,
appBuildVersion: flags.appBuildVersion,
sdkVersion: flags.sdkVersion,
runtimeVersion: flags.runtimeVersion,
appIdentifier: flags.appIdentifier,
buildProfile: flags.buildProfile,
gitCommitHash: flags.gitCommitHash,
},
paginatedQueryOptions,
});
}
}
@@ -1,6 +1,6 @@
import { getRuntimeVersionNullable } from '@expo/config-plugins/build/utils/Updates';
import { Platform } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -16,7 +16,7 @@ import { getExpoConfig } from '../../../project/expoConfig';
import { BUILD_NUMBER_REQUIREMENTS, isValidBuildNumber } from '../../../project/ios/versions';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../../project/projectUtils';
import {
@@ -27,14 +27,15 @@ import {
import { promptAsync } from '../../../prompts';
export default class BuildVersionSetView extends EasCommand {
static description = 'Update version of an app.';
static override description = 'Update version of an app.';
static flags = {
static override flags = {
platform: Flags.enum({
char: 'p',
options: ['android', 'ios'],
}),
profile: Flags.string({
char: 'e',
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
@@ -47,13 +48,20 @@ export default class BuildVersionSetView extends EasCommand {
const projectDir = await findProjectRootAsync();
const platform = await selectPlatformAsync(flags.platform);
const easJsonReader = new EasJsonReader(projectDir);
await ensureVersionSourceIsRemoteAsync(projectDir, easJsonReader);
const profile = await easJsonReader.getBuildProfileAsync(platform, flags.profile ?? undefined);
const easJsonAccessor = new EasJsonAccessor(projectDir);
await ensureVersionSourceIsRemoteAsync(easJsonAccessor);
const profile = await EasJsonUtils.getBuildProfileAsync(
easJsonAccessor,
platform,
flags.profile ?? undefined
);
const exp = getExpoConfig(projectDir, { env: profile.env });
const projectId = await getProjectIdAsync(exp);
const projectFullName = await getProjectFullNameAsync(exp);
// this command is always interactive (see prompt below)
const projectId = await getProjectIdAsync(exp, { nonInteractive: false });
const displayName = await getDisplayNameForProjectIdAsync(projectId);
validateAppConfigForRemoteVersionSource(exp, platform);
const applicationIdentifier = await getApplicationIdentifierAsync(
@@ -68,12 +76,12 @@ export default class BuildVersionSetView extends EasCommand {
applicationIdentifier
);
const currentStateMessage = remoteVersions?.buildVersion
? `Project ${chalk.bold(projectFullName)} with ${getApplicationIdentifierName(
? `Project ${chalk.bold(displayName)} with ${getApplicationIdentifierName(
platform
)} "${applicationIdentifier}" is configured with ${getBuildVersionName(platform)} ${
remoteVersions.buildVersion
}.`
: `Project ${chalk.bold(projectFullName)} with ${getApplicationIdentifierName(
: `Project ${chalk.bold(displayName)} with ${getApplicationIdentifierName(
platform
)} "${applicationIdentifier}" does not have any ${getBuildVersionName(
platform
@@ -1,6 +1,6 @@
import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, EasJsonReader } from '@expo/eas-json';
import { BuildProfile, EasJsonAccessor } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -40,14 +40,16 @@ interface SyncContext<T extends Platform> {
}
export default class BuildVersionSyncView extends EasCommand {
public static description = 'Update a version in native code with a value stored on EAS servers';
public static override description =
'Update a version in native code with a value stored on EAS servers';
public static flags = {
public static override flags = {
platform: Flags.enum({
char: 'p',
options: ['android', 'ios', 'all'],
}),
profile: Flags.string({
char: 'e',
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
@@ -59,19 +61,22 @@ export default class BuildVersionSyncView extends EasCommand {
const projectDir = await findProjectRootAsync();
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
const easJsonReader = new EasJsonReader(projectDir);
await ensureVersionSourceIsRemoteAsync(projectDir, easJsonReader);
const easJsonAccessor = new EasJsonAccessor(projectDir);
await ensureVersionSourceIsRemoteAsync(easJsonAccessor);
const platforms = toPlatforms(requestedPlatform);
const buildProfiles = await getProfilesAsync({
type: 'build',
easJsonReader,
easJsonAccessor,
platforms,
profileName: flags.profile ?? undefined,
});
for (const profileInfo of buildProfiles) {
const exp = getExpoConfig(projectDir, { env: profileInfo.profile.env });
const projectId = await getProjectIdAsync(exp);
// this command is always non-interactive
const projectId = await getProjectIdAsync(exp, { nonInteractive: true });
validateAppConfigForRemoteVersionSource(exp, profileInfo.platform);
const platformDisplayName = appPlatformDisplayNames[toAppPlatform(profileInfo.platform)];
+19 -16
View File
@@ -1,7 +1,6 @@
import { Flags } from '@oclif/core';
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
import EasCommand from '../../commandUtils/EasCommand';
import { EasJsonOnlyFlag } from '../../commandUtils/flags';
import { BuildFragment } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
@@ -9,20 +8,18 @@ import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class BuildView extends EasCommand {
static description = 'view a build for your project';
static override description = 'view a build for your project';
static args = [{ name: 'BUILD_ID' }];
static override args = [{ name: 'BUILD_ID' }];
static flags = {
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
}),
static override flags = {
...EasJsonOnlyFlag,
};
async runAsync(): Promise<void> {
@@ -36,8 +33,10 @@ export default class BuildView extends EasCommand {
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectName = await getProjectFullNameAsync(exp);
// this command is always non-interactive
const projectId = await getProjectIdAsync(exp, { nonInteractive: true });
const displayName = await getDisplayNameForProjectIdAsync(projectId);
const spinner = ora().start('Fetching the build…');
@@ -47,18 +46,22 @@ export default class BuildView extends EasCommand {
if (buildId) {
build = await BuildQuery.byIdAsync(buildId);
} else {
const builds = await BuildQuery.allForAppAsync(projectId, { limit: 1 });
const builds = await BuildQuery.viewBuildsOnAppAsync({
appId: projectId,
offset: 0,
limit: 1,
});
if (builds.length === 0) {
spinner.fail(`Couldn't find any builds for the project ${projectName}`);
spinner.fail(`Couldn't find any builds for the project ${displayName}`);
return;
}
build = builds[0];
}
if (buildId) {
spinner.succeed(`Found a matching build for the project ${projectName}`);
spinner.succeed(`Found a matching build for the project ${displayName}`);
} else {
spinner.succeed(`Showing the last build for the project ${projectName}`);
spinner.succeed(`Showing the last build for the project ${displayName}`);
}
if (flags.json) {
@@ -71,7 +74,7 @@ export default class BuildView extends EasCommand {
spinner.fail(`Something went wrong and we couldn't fetch the build with id ${buildId}`);
} else {
spinner.fail(
`Something went wrong and we couldn't fetch the last build for the project ${projectName}`
`Something went wrong and we couldn't fetch the last build for the project ${displayName}`
);
}
+29 -26
View File
@@ -1,8 +1,9 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
import { BranchNotFoundError } from '../../branch/utils';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
CreateUpdateChannelOnAppMutation,
@@ -13,7 +14,7 @@ import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
@@ -60,9 +61,9 @@ export async function createUpdateChannelOnAppAsync({
}
export default class ChannelCreate extends EasCommand {
static description = 'create a channel';
static override description = 'create a channel';
static args = [
static override args = [
{
name: 'name',
required: false,
@@ -70,18 +71,14 @@ export default class ChannelCreate extends EasCommand {
},
];
static flags = {
json: Flags.boolean({
description:
'print output as a JSON object with the new channel ID, name and branch mapping.',
default: false,
}),
static override flags = {
...EasNonInteractiveAndJsonFlags,
};
async runAsync(): Promise<void> {
let {
args: { name: channelName },
flags: { json: jsonFlag },
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
} = await this.parse(ChannelCreate);
if (jsonFlag) {
enableJsonOutput();
@@ -89,11 +86,11 @@ export default class ChannelCreate extends EasCommand {
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
if (!channelName) {
const validationMessage = 'Channel name may not be empty.';
if (jsonFlag) {
if (nonInteractive) {
throw new Error(validationMessage);
}
({ name: channelName } = await promptAsync({
@@ -106,21 +103,25 @@ export default class ChannelCreate extends EasCommand {
let branchId: string;
let branchMessage: string;
const existingBranch = await BranchQuery.getBranchByNameAsync({
appId: projectId,
name: channelName,
});
if (existingBranch) {
branchId = existingBranch.id;
branchMessage = `We found a branch with the same name`;
} else {
const newBranch = await createUpdateBranchOnAppAsync({
try {
const branch = await BranchQuery.getBranchByNameAsync({
appId: projectId,
name: channelName,
});
branchId = newBranch.id;
branchMessage = `We also went ahead and made a branch with the same name`;
branchId = branch.id;
branchMessage = `We found a branch with the same name`;
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newBranch = await createUpdateBranchOnAppAsync({
appId: projectId,
name: channelName,
});
branchId = newBranch.id;
branchMessage = `We also went ahead and made a branch with the same name`;
} else {
throw error;
}
}
const {
@@ -133,7 +134,7 @@ export default class ChannelCreate extends EasCommand {
if (!newChannel) {
throw new Error(
`Could not create channel with name ${channelName} on app with id ${projectId}`
`Could not create channel with name ${channelName} on project with id ${projectId}`
);
}
@@ -142,7 +143,9 @@ export default class ChannelCreate extends EasCommand {
} else {
Log.addNewLineIfNone();
Log.withTick(
`Created a new channel on project ${chalk.bold(await getProjectFullNameAsync(exp))}`
`Created a new channel on project ${chalk.bold(
await getDisplayNameForProjectIdAsync(projectId)
)}`
);
Log.log(
formatFields([
+35 -79
View File
@@ -1,96 +1,83 @@
import { Flags } from '@oclif/core';
import gql from 'graphql-tag';
import { selectChannelOnAppAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
DeleteUpdateChannelMutation,
DeleteUpdateChannelMutationVariables,
DeleteUpdateChannelResult,
GetChannelInfoQuery,
GetChannelInfoQueryVariables,
} from '../../graphql/generated';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { toggleConfirmAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class ChannelDelete extends EasCommand {
static hidden = true;
static description = 'Delete a channel';
static override hidden = true;
static override description = 'Delete a channel';
static args = [
static override args = [
{
name: 'name',
required: false,
description: 'Name of the channel to delete',
},
];
static flags = {
json: Flags.boolean({
description: 'print output as a JSON object',
default: false,
}),
'non-interactive': Flags.boolean({
default: false,
description: 'Run command in non-interactive mode',
}),
static override flags = {
...EasNonInteractiveAndJsonFlags,
};
async runAsync(): Promise<void> {
const {
args: { name: nameArg },
flags: { json: jsonFlag, 'non-interactive': nonInteractiveFlag },
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
} = await this.parse(ChannelDelete);
if (jsonFlag && !nonInteractiveFlag) {
throw new Error('--json is allowed only in non-interactive mode');
}
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
let name;
let channelId, channelName;
if (nameArg) {
name = nameArg;
const { id, name } = await ChannelQuery.viewUpdateChannelAsync({
appId: projectId,
channelName: nameArg,
});
channelId = id;
channelName = name;
} else {
if (nonInteractiveFlag) {
if (nonInteractive) {
throw new Error('Channel name must be set when running in non-interactive mode');
}
name = (
await promptAsync({
type: 'text',
name: 'name',
message: 'Provide the name of the channel to delete:',
validate: (value: any) => (value ? true : 'Channel name may not be empty.'),
})
).name;
const { id, name } = await selectChannelOnAppAsync({
projectId,
selectionPromptTitle: 'Select a channel to delete',
paginatedQueryOptions: {
json: jsonFlag,
nonInteractive,
offset: 0,
},
});
channelId = id;
channelName = name;
}
const data = await getChannelInfoAsync({ appId: projectId, name });
const channelId = data.app?.byId.updateChannelByName?.id;
if (!channelId) {
throw new Error(`Could not find channel ${name} on ${fullName}`);
}
if (!nonInteractiveFlag) {
if (!nonInteractive) {
Log.addNewLineIfNone();
Log.warn(
`You are about to permamently delete channel: "${name}".\nThis action is irreversible.`
`You are about to permanently delete channel: "${channelName}".\nThis action is irreversible.`
);
Log.newLine();
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
if (!confirmed) {
Log.error(`Canceled deletion of channel: "${name}".`);
Log.error(`Canceled deletion of channel: "${channelName}".`);
process.exit(1);
}
}
@@ -102,42 +89,11 @@ export default class ChannelDelete extends EasCommand {
if (jsonFlag) {
printJsonOnlyOutput(deletionResult);
} else {
Log.withTick(`️Deleted channel "${name}".`);
Log.withTick(`️Deleted channel "${channelName}".`);
}
}
}
async function getChannelInfoAsync({
appId,
name,
}: GetChannelInfoQueryVariables): Promise<GetChannelInfoQuery> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<GetChannelInfoQuery, GetChannelInfoQueryVariables>(
gql`
query GetChannelInfo($appId: String!, $name: String!) {
app {
byId(appId: $appId) {
id
updateChannelByName(name: $name) {
id
name
}
}
}
}
`,
{
appId,
name,
},
{ additionalTypenames: ['UpdateChannel'] }
)
.toPromise()
);
return data;
}
async function deleteChannelOnAppAsync({
channelId,
}: DeleteUpdateChannelMutationVariables): Promise<DeleteUpdateChannelResult> {
+41 -98
View File
@@ -2,71 +2,28 @@ import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
import { selectBranchOnAppAsync } from '../../branch/queries';
import { selectChannelOnAppAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
GetChannelByNameToEditQuery,
GetChannelByNameToEditQueryVariables,
UpdateBranch,
UpdateChannel,
UpdateChannelBranchMappingMutation,
UpdateChannelBranchMappingMutationVariables,
} from '../../graphql/generated';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
async function getChannelByNameForAppAsync({
appId,
channelName,
}: GetChannelByNameToEditQueryVariables): Promise<
Pick<UpdateChannel, 'id' | 'name'> & {
updateBranches: Pick<UpdateBranch, 'id' | 'name'>[];
}
> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<GetChannelByNameToEditQuery, GetChannelByNameToEditQueryVariables>(
gql`
query GetChannelByNameToEdit($appId: String!, $channelName: String!) {
app {
byId(appId: $appId) {
id
updateChannelByName(name: $channelName) {
id
name
updateBranches(offset: 0, limit: 1) {
id
name
}
}
}
}
}
`,
{ appId, channelName },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch'] }
)
.toPromise()
);
const updateChannelByNameResult = data.app?.byId.updateChannelByName;
if (!updateChannelByNameResult) {
throw new Error(`Could not find a channel named ${channelName} on app with id ${appId}`);
}
return updateChannelByNameResult;
}
export async function updateChannelBranchMappingAsync({
channelId,
branchMapping,
}: UpdateChannelBranchMappingMutationVariables): Promise<{
id: string;
name: string;
branchMapping: string;
}> {
}: UpdateChannelBranchMappingMutationVariables): Promise<
UpdateChannelBranchMappingMutation['updateChannel']['editUpdateChannel']
> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<UpdateChannelBranchMappingMutation, UpdateChannelBranchMappingMutationVariables>(
@@ -89,13 +46,13 @@ export async function updateChannelBranchMappingAsync({
if (!channel) {
throw new Error(`Could not find a channel with id: ${channelId}`);
}
return data.updateChannel.editUpdateChannel!;
return channel;
}
export default class ChannelEdit extends EasCommand {
static description = 'point a channel at a new branch';
static override description = 'point a channel at a new branch';
static args = [
static override args = [
{
name: 'name',
required: false,
@@ -103,46 +60,54 @@ export default class ChannelEdit extends EasCommand {
},
];
static flags = {
static override flags = {
branch: Flags.string({
description: 'Name of the branch to point to',
}),
json: Flags.boolean({
description: 'Print output as a JSON object with the channel ID, name and branch mapping',
default: false,
}),
...EasNonInteractiveAndJsonFlags,
};
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(ChannelEdit);
if (flags.json) {
const {
args,
flags: { branch: branchFlag, json, 'non-interactive': nonInteractive },
} = await this.parse(ChannelEdit);
if (json) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const channelName = args.name ?? (await promptForChannelAsync());
const existingChannel = args.name
? await ChannelQuery.viewUpdateChannelAsync({ appId: projectId, channelName: args.name })
: await selectChannelOnAppAsync({
projectId,
selectionPromptTitle: 'Select a channel to edit',
paginatedQueryOptions: { json, nonInteractive, offset: 0 },
});
const existingChannel = await getChannelByNameForAppAsync({ appId: projectId, channelName });
if (existingChannel.updateBranches.length > 1) {
throw new Error('There is a rollout in progress. Manage it with "channel:rollout" instead.');
}
const branchName = flags.branch ?? (await promptForBranchAsync());
const branch = branchFlag
? await BranchQuery.getBranchByNameAsync({
appId: projectId,
name: branchFlag,
})
: await selectBranchOnAppAsync({
projectId,
promptTitle: `Which branch would you like ${existingChannel.name} to point at?`,
displayTextForListItem: updateBranch => updateBranch.name,
paginatedQueryOptions: {
json,
nonInteractive,
offset: 0,
},
});
const branch = await BranchQuery.getBranchByNameAsync({
appId: projectId,
name: branchName,
});
if (!branch) {
throw new Error(
`Could not find a branch named "${branchName}". Check what branches exist on this project with ${chalk.bold(
'eas branch:list'
)}.`
);
}
const channel = await updateChannelBranchMappingAsync({
channelId: existingChannel.id,
// todo: move branch mapping logic to utility
@@ -152,7 +117,7 @@ export default class ChannelEdit extends EasCommand {
}),
});
if (flags.json) {
if (json) {
printJsonOnlyOutput(channel);
} else {
Log.withTick(
@@ -165,25 +130,3 @@ export default class ChannelEdit extends EasCommand {
}
}
}
async function promptForChannelAsync(): Promise<string> {
Log.addNewLineIfNone();
const { name } = await promptAsync({
type: 'text',
name: 'name',
message: 'Provide the name of the channel to edit:',
validate: value => (value ? true : 'The channel name may not be empty.'),
});
return name;
}
async function promptForBranchAsync(): Promise<string> {
Log.addNewLineIfNone();
const { name } = await promptAsync({
type: 'text',
name: 'name',
message: `To which branch should the channel link?`,
validate: value => (value ? true : 'The branch name may not be empty.'),
});
return name;
}
+19 -94
View File
@@ -1,114 +1,39 @@
import { Flags } from '@oclif/core';
import gql from 'graphql-tag';
import { CHANNELS_LIMIT, listAndRenderChannelsOnAppAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import {
GetAllChannelsForAppQuery,
GetAllChannelsForAppQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
EasPaginatedQueryFlags,
getLimitFlagWithCustomValues,
getPaginatedQueryOptions,
} from '../../commandUtils/pagination';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { logChannelDetails } from './view';
const CHANNEL_LIMIT = 10_000;
async function getAllUpdateChannelForAppAsync({
appId,
}: {
appId: string;
}): Promise<GetAllChannelsForAppQuery> {
return await withErrorHandlingAsync(
graphqlClient
.query<GetAllChannelsForAppQuery, GetAllChannelsForAppQueryVariables>(
gql`
query GetAllChannelsForApp($appId: String!, $offset: Int!, $limit: Int!) {
app {
byId(appId: $appId) {
id
updateChannels(offset: $offset, limit: $limit) {
id
name
branchMapping
updateBranches(offset: 0, limit: $limit) {
id
name
updates(offset: 0, limit: 10) {
id
group
message
runtimeVersion
createdAt
platform
actor {
id
... on User {
username
}
... on Robot {
firstName
}
}
}
}
}
}
}
}
`,
{ appId, offset: 0, limit: CHANNEL_LIMIT },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
)
.toPromise()
);
}
import { enableJsonOutput } from '../../utils/json';
export default class ChannelList extends EasCommand {
static description = 'list all channels';
static override description = 'list all channels';
static flags = {
json: Flags.boolean({
description: 'print output as a JSON object with the channel ID, name and branch mapping.',
default: false,
}),
static override flags = {
...EasPaginatedQueryFlags,
limit: getLimitFlagWithCustomValues({ defaultTo: 10, limit: CHANNELS_LIMIT }),
...EasNonInteractiveAndJsonFlags,
};
async runAsync(): Promise<void> {
const {
flags: { json: jsonFlag },
} = await this.parse(ChannelList);
const { flags } = await this.parse(ChannelList);
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const getAllUpdateChannelForAppResult = await getAllUpdateChannelForAppAsync({
appId: projectId,
await listAndRenderChannelsOnAppAsync({
projectId,
paginatedQueryOptions,
});
const channels = getAllUpdateChannelForAppResult.app?.byId.updateChannels;
if (!channels) {
throw new Error(`Could not find channels on project with id ${projectId}`);
}
if (jsonFlag) {
printJsonOnlyOutput(channels);
} else {
for (const channel of channels) {
Log.addNewLineIfNone();
Log.log(
formatFields([
{ label: 'Name', value: channel.name },
{ label: 'ID', value: channel.id },
])
);
logChannelDetails(channel);
}
}
}
}
@@ -1,21 +1,22 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { BranchMapping, getBranchMapping } from '../../channel/utils';
import EasCommand from '../../commandUtils/EasCommand';
import { GetChannelByNameForAppQuery, UpdateBranch } from '../../graphql/generated';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { UpdateBranch } from '../../graphql/generated';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import { ChannelQuery, UpdateChannelByNameObject } from '../../graphql/queries/ChannelQuery';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync, selectAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { updateChannelBranchMappingAsync } from './edit';
import { BranchMapping, getBranchMapping } from './view';
async function promptForRolloutPercentAsync({
promptMessage,
@@ -40,9 +41,7 @@ async function promptForRolloutPercentAsync({
return rolloutPercent;
}
function getRolloutInfo(
channel: NonNullable<GetChannelByNameForAppQuery['app']['byId']['updateChannelByName']>
): {
function getRolloutInfo(channel: UpdateChannelByNameObject): {
newBranch: Pick<UpdateBranch, 'name' | 'id'>;
oldBranch: Pick<UpdateBranch, 'name' | 'id'>;
currentPercent: number;
@@ -64,20 +63,20 @@ async function startRolloutAsync({
channelName,
branchName,
percent,
jsonFlag,
projectId,
fullName,
displayName,
currentBranchMapping,
channel,
nonInteractive,
}: {
channelName?: string;
branchName: string;
percent?: number;
jsonFlag: boolean;
projectId: string;
fullName: string;
displayName: string;
currentBranchMapping: BranchMapping;
channel: NonNullable<GetChannelByNameForAppQuery['app']['byId']['updateChannelByName']>;
channel: UpdateChannelByNameObject;
nonInteractive: boolean;
}): Promise<{
newChannelInfo: {
id: string;
@@ -90,13 +89,6 @@ async function startRolloutAsync({
appId: projectId,
name: branchName,
});
if (!branch) {
throw new Error(
`Could not find a branch named "${branchName}". Check which branches exist on this project with ${chalk.bold(
'eas branch:list'
)}.`
);
}
const oldBranchId = currentBranchMapping.data[0].branchId;
if (branch.id === oldBranchId) {
@@ -106,9 +98,9 @@ async function startRolloutAsync({
}
if (percent == null) {
if (jsonFlag) {
if (nonInteractive) {
throw new Error(
'You must specify a percent with the --percent flag when initiating a rollout with the --json flag.'
'You must specify a percent with the --percent flag when initiating a rollout with the --non-interactive flag.'
);
}
const promptMessage = `What percent of users should be directed to the branch "${branchName}"?`;
@@ -137,7 +129,7 @@ async function startRolloutAsync({
const oldBranch = channel.updateBranches.filter(branch => branch.id === oldBranchId)[0];
if (!oldBranch) {
throw new Error(
`Branch mapping is missing its only branch for channel "${channelName}" on app "${fullName}"`
`Branch mapping is missing its only branch for channel "${channelName}" on app "${displayName}"`
);
}
@@ -155,15 +147,15 @@ async function startRolloutAsync({
async function editRolloutAsync({
channelName,
percent,
jsonFlag,
nonInteractive,
currentBranchMapping,
channel,
}: {
channelName?: string;
percent?: number;
jsonFlag: boolean;
nonInteractive: boolean;
currentBranchMapping: BranchMapping;
channel: NonNullable<GetChannelByNameForAppQuery['app']['byId']['updateChannelByName']>;
channel: UpdateChannelByNameObject;
}): Promise<{
newChannelInfo: {
id: string;
@@ -175,7 +167,7 @@ async function editRolloutAsync({
const { newBranch, oldBranch, currentPercent } = getRolloutInfo(channel);
if (percent == null) {
if (jsonFlag) {
if (nonInteractive) {
throw new Error(
'A rollout is already in progress. If you wish to modify it you must use specify the new rollout percentage with the --percent flag.'
);
@@ -210,15 +202,15 @@ async function editRolloutAsync({
async function endRolloutAsync({
channelName,
branchName,
jsonFlag,
nonInteractive,
projectId,
channel,
}: {
channelName?: string;
branchName?: string;
jsonFlag: boolean;
nonInteractive: boolean;
projectId: string;
channel: NonNullable<GetChannelByNameForAppQuery['app']['byId']['updateChannelByName']>;
channel: UpdateChannelByNameObject;
}): Promise<{
newChannelInfo: {
id: string;
@@ -236,13 +228,6 @@ async function endRolloutAsync({
appId: projectId,
name: branchName,
});
if (!branch) {
throw new Error(
`Could not find a branch named "${branchName}". Check which branches exist on this project with ${chalk.bold(
'eas branch:list'
)}.`
);
}
switch (branch.id) {
case newBranch.id:
@@ -257,9 +242,9 @@ async function endRolloutAsync({
);
}
} else {
if (jsonFlag) {
if (nonInteractive) {
throw new Error(
'Branch name must be specified with the --branch flag when both the --end and --json flag are true.'
'Branch name must be specified with the --branch flag when both the --end and --non-interactive flag are true.'
);
}
endOnNewBranch = await selectAsync<boolean>(
@@ -304,10 +289,10 @@ async function endRolloutAsync({
}
export default class ChannelRollout extends EasCommand {
static hidden = true;
static description = 'Rollout a new branch out to a channel incrementally.';
static override hidden = true;
static override description = 'Rollout a new branch out to a channel incrementally.';
static args = [
static override args = [
{
name: 'channel',
required: true,
@@ -315,7 +300,7 @@ export default class ChannelRollout extends EasCommand {
},
];
static flags = {
static override flags = {
branch: Flags.string({
description: 'branch to rollout',
required: false,
@@ -328,16 +313,19 @@ export default class ChannelRollout extends EasCommand {
description: 'end the rollout',
default: false,
}),
json: Flags.boolean({
description: 'print output as a JSON object with the new channel ID, name and branch mapping',
default: false,
}),
...EasNonInteractiveAndJsonFlags,
};
async runAsync(): Promise<void> {
const {
args: { channel: channelName },
flags: { json: jsonFlag, end: endFlag, branch: branchName, percent },
flags: {
json: jsonFlag,
end: endFlag,
branch: branchName,
percent,
'non-interactive': nonInteractive,
},
} = await this.parse(ChannelRollout);
if (jsonFlag) {
enableJsonOutput();
@@ -345,10 +333,10 @@ export default class ChannelRollout extends EasCommand {
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
const channel = await ChannelQuery.getUpdateChannelByNameForAppAsync({
const channel = await ChannelQuery.viewUpdateChannelAsync({
appId: projectId,
channelName: channelName!,
});
@@ -395,11 +383,11 @@ export default class ChannelRollout extends EasCommand {
if (!isRollout) {
rolloutMutationResult = await startRolloutAsync({
channelName,
branchName: branchName ?? (await promptForBranchNameAsync(channelName)),
branchName: branchName ?? (await promptForBranchNameAsync(channelName, nonInteractive)),
percent,
jsonFlag,
nonInteractive,
projectId,
fullName,
displayName: projectDisplayName,
currentBranchMapping,
channel,
});
@@ -407,7 +395,7 @@ export default class ChannelRollout extends EasCommand {
rolloutMutationResult = await endRolloutAsync({
channelName,
branchName,
jsonFlag,
nonInteractive,
projectId,
channel,
});
@@ -415,7 +403,7 @@ export default class ChannelRollout extends EasCommand {
rolloutMutationResult = await editRolloutAsync({
channelName,
percent,
jsonFlag,
nonInteractive,
currentBranchMapping,
channel,
});
@@ -432,7 +420,13 @@ export default class ChannelRollout extends EasCommand {
}
}
async function promptForBranchNameAsync(channelName: string): Promise<string> {
async function promptForBranchNameAsync(
channelName: string,
nonInteractive: boolean
): Promise<string> {
if (nonInteractive) {
throw new Error('Must supply branch flag in non-interactive mode');
}
const { name } = await promptAsync({
type: 'text',
name: 'name',
+31 -170
View File
@@ -1,146 +1,20 @@
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import Table from 'cli-table3';
import {
listAndRenderBranchesAndUpdatesOnChannelAsync,
selectChannelOnAppAsync,
} from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import Log from '../../log';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import {
FormatUpdateParameter,
UPDATE_COLUMNS,
formatUpdate,
getPlatformsForGroup,
} from '../../update/utils';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export type BranchMapping = {
version: number;
data: {
branchId: string;
branchMappingLogic: {
operand: number;
clientKey: string;
branchMappingOperator: string;
} & string;
}[];
};
/**
* Get the branch mapping and determine whether it is a rollout.
* Ensure that the branch mapping is properly formatted.
*/
export function getBranchMapping(branchMappingString?: string): {
branchMapping: BranchMapping;
isRollout: boolean;
rolloutPercent?: number;
} {
if (!branchMappingString) {
throw new Error('Missing branch mapping.');
}
let branchMapping: BranchMapping;
try {
branchMapping = JSON.parse(branchMappingString);
} catch {
throw new Error(`Could not parse branchMapping string into a JSON: "${branchMappingString}"`);
}
assert(branchMapping, 'Branch Mapping must be defined.');
if (branchMapping.version !== 0) {
throw new Error('Branch mapping must be version 0.');
}
const isRollout = branchMapping.data.length === 2;
const rolloutPercent = branchMapping.data[0].branchMappingLogic.operand;
switch (branchMapping.data.length) {
case 0:
break;
case 1:
if (branchMapping.data[0].branchMappingLogic !== 'true') {
throw new Error('Branch mapping logic for a single branch must be "true"');
}
break;
case 2:
if (branchMapping.data[0].branchMappingLogic.clientKey !== 'rolloutToken') {
throw new Error('Client key of initial branch mapping must be "rolloutToken"');
}
if (branchMapping.data[0].branchMappingLogic.branchMappingOperator !== 'hash_lt') {
throw new Error('Branch mapping operator of initial branch mapping must be "hash_lt"');
}
if (rolloutPercent == null) {
throw new Error('Branch mapping is missing a "rolloutPercent"');
}
if (branchMapping.data[1].branchMappingLogic !== 'true') {
throw new Error('Branch mapping logic for a the second branch of a rollout must be "true"');
}
break;
default:
throw new Error('Branch mapping data must have length less than or equal to 2.');
}
return { branchMapping, isRollout, rolloutPercent };
}
export function logChannelDetails(channel: {
branchMapping: string;
updateBranches: {
updates: (FormatUpdateParameter & {
runtimeVersion: string;
group: string;
platform: string;
})[];
name: string;
id: string;
}[];
}): void {
const { branchMapping, isRollout, rolloutPercent } = getBranchMapping(channel.branchMapping);
const table = new Table({
head: ['branch', ...(isRollout ? ['rollout percent'] : []), ...UPDATE_COLUMNS],
wordWrap: true,
});
for (const index in branchMapping.data) {
if (parseInt(index, 10) > 1) {
throw new Error('Branch Mapping data must have length less than or equal to 2.');
}
const { branchId } = branchMapping.data[index];
const branch = channel.updateBranches.filter(branch => branch.id === branchId)[0];
if (!branch) {
throw new Error('Branch mapping is pointing at a missing branch.');
}
const update = branch.updates[0];
table.push([
branch.name,
...(isRollout
? [
parseInt(index, 10) === 0
? `${rolloutPercent! * 100}%`
: `${(1 - rolloutPercent!) * 100}%`,
]
: []),
formatUpdate(update),
update?.runtimeVersion ?? 'N/A',
update?.group ?? 'N/A',
getPlatformsForGroup({
updates: branch.updates,
group: branch.updates[0]?.group,
}),
]);
}
Log.log(table.toString());
}
import { enableJsonOutput } from '../../utils/json';
export default class ChannelView extends EasCommand {
static description = 'view a channel';
static override description = 'view a channel';
static args = [
static override args = [
{
name: 'name',
required: false,
@@ -148,61 +22,48 @@ export default class ChannelView extends EasCommand {
},
];
static flags = {
json: Flags.boolean({
description: 'print output as a JSON object with the channel ID, name and branch mapping.',
default: false,
}),
static override flags = {
...EasNonInteractiveAndJsonFlags,
...EasPaginatedQueryFlags,
};
async runAsync(): Promise<void> {
let {
args: { name: channelName },
flags: { json: jsonFlag },
flags,
} = await this.parse(ChannelView);
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
if (!channelName) {
const validationMessage = 'A channel name is required to view a specific channel.';
if (jsonFlag) {
if (nonInteractive) {
throw new Error(validationMessage);
}
({ name: channelName } = await promptAsync({
type: 'text',
name: 'name',
message: 'Provide a channel name:',
validate: value => (value ? true : validationMessage),
}));
const selectedUpdateChannel = await selectChannelOnAppAsync({
projectId,
selectionPromptTitle: 'Select a channel to view',
paginatedQueryOptions: {
json: jsonFlag,
nonInteractive,
offset: 0,
},
});
channelName = selectedUpdateChannel.name;
assert(channelName, `A channel must be selected.`);
}
const channel = await ChannelQuery.getUpdateChannelByNameForAppAsync({
appId: projectId,
await listAndRenderBranchesAndUpdatesOnChannelAsync({
projectId,
channelName,
paginatedQueryOptions,
});
if (!channel) {
throw new Error(`Could not find a channel with name: ${channelName}`);
}
if (jsonFlag) {
printJsonOnlyOutput(channel);
} else {
Log.addNewLineIfNone();
Log.log(chalk.bold('Channel:'));
Log.log(
formatFields([
{ label: 'Name', value: channel.name },
{ label: 'ID', value: channel.id },
])
);
Log.addNewLineIfNone();
Log.log(chalk`{bold Branches pointed at this channel and their most recent update group:}`);
logChannelDetails(channel);
}
}
}
+13 -8
View File
@@ -1,6 +1,6 @@
import { getProjectConfigDescription } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -13,14 +13,19 @@ import { findProjectRootAsync } from '../project/projectUtils';
import { selectAsync } from '../prompts';
export default class Config extends EasCommand {
static description = 'display project configuration (app.json + eas.json)';
static override description = 'display project configuration (app.json + eas.json)';
static flags = {
static override flags = {
platform: Flags.enum({ char: 'p', options: ['android', 'ios'] }),
profile: Flags.string(),
profile: Flags.string({
char: 'e',
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
}),
};
protected requiresAuthentication = false;
protected override requiresAuthentication = false;
async runAsync(): Promise<void> {
const { flags } = await this.parse(Config);
@@ -31,13 +36,13 @@ export default class Config extends EasCommand {
const projectDir = await findProjectRootAsync();
const reader = new EasJsonReader(projectDir);
const accessor = new EasJsonAccessor(projectDir);
const profileName =
maybeProfile ??
(await selectAsync(
'Select build profile',
(
await reader.getBuildProfileNamesAsync()
await EasJsonUtils.getBuildProfileNamesAsync(accessor)
).map(profileName => ({
title: profileName,
value: profileName,
@@ -56,7 +61,7 @@ export default class Config extends EasCommand {
},
]));
const profile = await reader.getBuildProfileAsync(platform, profileName);
const profile = await EasJsonUtils.getBuildProfileAsync(accessor, platform, profileName);
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
Log.addNewLineIfNone();
+2 -2
View File
@@ -4,9 +4,9 @@ import EasCommand from '../commandUtils/EasCommand';
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
export default class Credentials extends EasCommand {
static description = 'manage credentials';
static override description = 'manage credentials';
static flags = {
static override flags = {
platform: Flags.enum({ char: 'p', options: ['android', 'ios'] }),
};
@@ -5,10 +5,11 @@ import DeviceManager from '../../devices/manager';
import { ensureLoggedInAsync } from '../../user/actions';
export default class DeviceCreate extends EasCommand {
static description = 'register new Apple Devices to use for internal distribution';
static override description = 'register new Apple Devices to use for internal distribution';
async runAsync(): Promise<void> {
const user = await ensureLoggedInAsync();
// this command is interactive by design
const user = await ensureLoggedInAsync({ nonInteractive: false });
const ctx = await createContextAsync({ appStore: new AppStoreApi(), user });
const manager = new DeviceManager(ctx);
+16 -9
View File
@@ -17,13 +17,17 @@ import { AppleDevice, Maybe } from '../../graphql/generated';
import Log from '../../log';
import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getOwnerAccountForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
export default class DeviceDelete extends EasCommand {
static description = 'remove a registered device from your account';
static override description = 'remove a registered device from your account';
static flags = {
static override flags = {
'apple-team-id': Flags.string(),
udid: Flags.string({ multiple: true }),
};
@@ -35,15 +39,17 @@ export default class DeviceDelete extends EasCommand {
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const accountName = await getProjectAccountNameAsync(exp);
// this command is interactive by design
const projectId = await getProjectIdAsync(exp, { nonInteractive: false });
const account = await getOwnerAccountForProjectIdAsync(projectId);
if (!appleTeamIdentifier) {
appleTeamIdentifier = await this.askForAppleTeamAsync(accountName);
appleTeamIdentifier = await this.askForAppleTeamAsync(account.name);
}
assert(appleTeamIdentifier, 'No team identifier is specified');
const appleDevicesResult = await this.getDevicesForTeamAsync(accountName, appleTeamIdentifier);
const appleDevicesResult = await this.getDevicesForTeamAsync(account.name, appleTeamIdentifier);
if (!appleDevicesResult) {
return;
@@ -89,9 +95,10 @@ export default class DeviceDelete extends EasCommand {
Log.addNewLineIfNone();
const removeAppleSpinner = ora('Disabling devices on Apple').start();
try {
let realDevices = await Device.getAllIOSProfileDevicesAsync(context);
realDevices = realDevices.filter(d =>
chosenDevices.map(cd => cd.identifier).includes(d.attributes.udid)
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) {
+14 -7
View File
@@ -9,13 +9,17 @@ import formatDevice from '../../devices/utils/formatDevice';
import Log from '../../log';
import { Ora, ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getOwnerAccountForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
export default class BuildList extends EasCommand {
static description = 'list all registered devices for your account';
static override description = 'list all registered devices for your account';
static flags = {
static override flags = {
'apple-team-id': Flags.string(),
};
@@ -24,7 +28,10 @@ export default class BuildList extends EasCommand {
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const accountName = await getProjectAccountNameAsync(exp);
// this command is interactive by design
const projectId = await getProjectIdAsync(exp, { nonInteractive: false });
const account = await getOwnerAccountForProjectIdAsync(projectId);
let spinner: Ora;
@@ -32,7 +39,7 @@ export default class BuildList extends EasCommand {
spinner = ora().start('Fetching the list of teams for the project…');
try {
const teams = await AppleTeamQuery.getAllForAccountAsync(accountName);
const teams = await AppleTeamQuery.getAllForAccountAsync(account.name);
if (teams.length > 0) {
spinner.succeed();
@@ -55,7 +62,7 @@ export default class BuildList extends EasCommand {
appleTeamIdentifier = result.appleTeamIdentifier;
}
} else {
spinner.fail(`Couldn't find any teams for the account ${accountName}`);
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`);
@@ -69,7 +76,7 @@ export default class BuildList extends EasCommand {
try {
const result = await AppleDeviceQuery.getAllForAppleTeamAsync(
accountName,
account.name,
appleTeamIdentifier
);
+12 -5
View File
@@ -4,12 +4,16 @@ import formatDevice from '../../devices/utils/formatDevice';
import Log from '../../log';
import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getOwnerAccountForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
export default class DeviceView extends EasCommand {
static description = 'view a device for your project';
static override description = 'view a device for your project';
static args = [{ name: 'UDID' }];
static override args = [{ name: 'UDID' }];
async runAsync(): Promise<void> {
const { UDID } = (await this.parse(DeviceView)).args;
@@ -30,12 +34,15 @@ If you are not sure what is the UDID of the device you are looking for, run:
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const accountName = await getProjectAccountNameAsync(exp);
// this command is non-interactive by design
const projectId = await getProjectIdAsync(exp, { nonInteractive: true });
const account = await getOwnerAccountForProjectIdAsync(projectId);
const spinner = ora().start(`Fetching device details for ${UDID}…`);
try {
const device = await AppleDeviceQuery.getByDeviceIdentifierAsync(accountName, UDID);
const device = await AppleDeviceQuery.getByDeviceIdentifierAsync(account.name, UDID);
if (device) {
spinner.succeed('Fetched device details');
+2 -2
View File
@@ -8,9 +8,9 @@ import { resolveWorkflowAsync } from '../project/workflow';
import { easCliVersion } from '../utils/easCli';
export default class Diagnostics extends EasCommand {
static description = 'display environment info';
static override description = 'display environment info';
protected requiresAuthentication = false;
protected override requiresAuthentication = false;
async runAsync(): Promise<void> {
const info = await envinfo.run(
@@ -14,10 +14,11 @@ import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUt
import { ensureLoggedInAsync } from '../../user/actions';
export default class MetadataPull extends EasCommand {
static description = 'generate the local store configuration from the app stores';
static override description = 'generate the local store configuration from the app stores';
static flags = {
static override flags = {
profile: Flags.string({
char: 'e',
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
@@ -29,13 +30,15 @@ export default class MetadataPull extends EasCommand {
const { flags } = await this.parse(MetadataPull);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
await getProjectIdAsync(exp);
// this command is interactive (all nonInteractive flags passed to utility functions are false)
await getProjectIdAsync(exp, { nonInteractive: false });
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
const credentialsCtx = new CredentialsContext({
exp,
projectDir,
user: await ensureLoggedInAsync(),
user: await ensureLoggedInAsync({ nonInteractive: false }),
nonInteractive: false,
});
@@ -12,10 +12,11 @@ import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUt
import { ensureLoggedInAsync } from '../../user/actions';
export default class MetadataPush extends EasCommand {
static description = 'sync the local store configuration to the app stores';
static override description = 'sync the local store configuration to the app stores';
static flags = {
static override flags = {
profile: Flags.string({
char: 'e',
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
@@ -27,13 +28,15 @@ export default class MetadataPush extends EasCommand {
const { flags } = await this.parse(MetadataPush);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
await getProjectIdAsync(exp);
// this command is interactive (all nonInteractive flags passed to utility functions are false)
await getProjectIdAsync(exp, { nonInteractive: false });
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
const credentialsCtx = new CredentialsContext({
exp,
projectDir,
user: await ensureLoggedInAsync(),
user: await ensureLoggedInAsync({ nonInteractive: false }),
nonInteractive: false,
});
+44
View File
@@ -0,0 +1,44 @@
import openBrowserAsync from 'better-opn';
import { getProjectDashboardUrl } from '../build/utils/url';
import EasCommand from '../commandUtils/EasCommand';
import { ora } from '../ora';
import { getExpoConfig } from '../project/expoConfig';
import {
findProjectRootAsync,
getOwnerAccountForProjectIdAsync,
getProjectIdAsync,
} from '../project/projectUtils';
export default class Open extends EasCommand {
static override description = 'open the project page in a web browser';
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
// this command is interactive by nature (only really run by humans in a terminal)
// this ensures the project exists before opening the browser
const projectId = await getProjectIdAsync(exp, { nonInteractive: false });
const account = await getOwnerAccountForProjectIdAsync(projectId);
const projectName = exp.slug;
const projectDashboardUrl = getProjectDashboardUrl(account.name, projectName);
const failedMessage = `Unable to open a web browser. Project page is available at: ${projectDashboardUrl}`;
const spinner = ora(`Opening ${projectDashboardUrl}`).start();
try {
const opened = await openBrowserAsync(projectDashboardUrl);
if (opened) {
spinner.succeed(`Opened ${projectDashboardUrl}`);
} else {
spinner.fail(failedMessage);
}
return;
} catch (error) {
spinner.fail(failedMessage);
throw error;
}
}
}
@@ -32,15 +32,18 @@ async function projectInfoByIdAsync(appId: string): Promise<AppInfoQuery> {
}
export default class ProjectInfo extends EasCommand {
static description = 'information about the current project';
static override description = 'information about the current project';
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
// this command is non-interactive by design
const projectId = await getProjectIdAsync(exp, { nonInteractive: true });
const { app } = await projectInfoByIdAsync(projectId);
if (!app) {
throw new Error(`Could not find project with ID: ${projectId}`);
throw new Error(`Could not find project with id: ${projectId}`);
}
Log.addNewLineIfNone();
+7 -12
View File
@@ -3,15 +3,11 @@ import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
fetchProjectIdFromServerAsync,
findProjectRootAsync,
saveProjectIdToAppConfigAsync,
} from '../../project/projectUtils';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
export default class ProjectInit extends EasCommand {
static description = 'create or link an EAS project';
static aliases = ['init'];
static override description = 'create or link an EAS project';
static override aliases = ['init'];
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
@@ -19,14 +15,13 @@ export default class ProjectInit extends EasCommand {
if (exp.extra?.eas?.projectId) {
Log.error(
`app.json is already linked to project with ID: ${chalk.bold(exp.extra?.eas?.projectId)}`
`app.json is already linked to project with id: ${chalk.bold(exp.extra?.eas?.projectId)}`
);
return;
}
const projectId = await fetchProjectIdFromServerAsync(exp);
await saveProjectIdToAppConfigAsync(projectDir, projectId);
Log.withTick(`Linked app.json to project with ID ${chalk.bold(projectId)}`);
// this command is interactive by nature
const projectId = await getProjectIdAsync(exp, { nonInteractive: false });
Log.withTick(`Linked app.json to project with id ${chalk.bold(projectId)}`);
}
}
+117 -43
View File
@@ -1,28 +1,35 @@
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
import {
EnvironmentSecretScope,
EnvironmentSecretsQuery,
} from '../../graphql/queries/EnvironmentSecretsQuery';
import {
SecretType,
SecretTypeToEnvironmentSecretType,
} from '../../graphql/types/EnvironmentSecret';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
getDisplayNameForProjectIdAsync,
getOwnerAccountForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { findAccountByName } from '../../user/Account';
import { getActorDisplayName } from '../../user/User';
import { ensureLoggedInAsync } from '../../user/actions';
import { promptAsync, selectAsync } from '../../prompts';
export default class EnvironmentSecretCreate extends EasCommand {
static description = 'create an environment secret on the current project or owner account';
static override description =
'create an environment secret on the current project or owner account';
static flags = {
static override flags = {
scope: Flags.enum({
description: 'Scope for the secret',
options: [EnvironmentSecretScope.ACCOUNT, EnvironmentSecretScope.PROJECT],
@@ -32,29 +39,42 @@ export default class EnvironmentSecretCreate extends EasCommand {
description: 'Name of the secret',
}),
value: Flags.string({
description: 'Value of the secret',
description: 'Text value or path to a file to store in the secret',
}),
type: Flags.enum({
description: 'The type of secret',
options: [SecretType.STRING, SecretType.FILE],
}),
force: Flags.boolean({
description: 'Delete and recreate existing secrets',
default: false,
}),
...EASNonInteractiveFlag,
};
async runAsync(): Promise<void> {
const actor = await ensureLoggedInAsync();
let {
flags: { name, value: secretValue, scope, force },
flags: {
name,
value: secretValue,
scope,
force,
type: secretType,
'non-interactive': nonInteractive,
},
} = await this.parse(EnvironmentSecretCreate);
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const accountName = await getProjectAccountNameAsync(exp);
const { slug } = exp;
const projectId = await getProjectIdAsync(exp);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
const ownerAccount = await getOwnerAccountForProjectIdAsync(projectId);
if (!scope) {
const validationMessage = 'Secret scope may not be empty.';
if (nonInteractive) {
throw new Error(validationMessage);
}
({ scope } = await promptAsync({
type: 'select',
@@ -69,13 +89,18 @@ export default class EnvironmentSecretCreate extends EasCommand {
}
if (!name) {
const validationMessage = 'Secret name may not be empty.';
if (nonInteractive) {
throw new Error(validationMessage);
}
({ name } = await promptAsync({
type: 'text',
name: 'name',
message: `Secret name:`,
validate: value => {
if (!value) {
return 'Secret name may not be empty.';
return validationMessage;
}
// this validation regex here is just to shorten the feedback loop
@@ -89,42 +114,82 @@ export default class EnvironmentSecretCreate extends EasCommand {
}));
if (!name) {
throw new Error('Secret name may not be empty.');
throw new Error(validationMessage);
}
}
if (!secretType) {
if (nonInteractive) {
throw new Error('Secret type may not be empty in non-interactive mode');
}
secretType = await selectAsync('Select secret type', [
{
title: 'string',
value: SecretType.STRING,
},
{
title: 'file',
value: SecretType.FILE,
},
]);
}
if (!secretValue) {
const validationMessage = 'Secret value may not be empty.';
if (nonInteractive) {
throw new Error(validationMessage);
}
({ secretValue } = await promptAsync({
type: 'text',
name: 'secretValue',
message: 'Secret value:',
validate: value => (value ? true : validationMessage),
message: secretType === SecretType.STRING ? 'Secret value:' : 'Local file path:',
// eslint-disable-next-line async-protect/async-suffix
validate: async secretValue => {
if (!secretValue) {
return 'Secret value may not be empty.';
}
if (secretType === SecretType.FILE) {
const secretFilePath = path.resolve(secretValue);
if (!(await fs.pathExists(secretFilePath))) {
return `File "${secretValue}" does not exist.`;
}
}
return true;
},
}));
}
if (!secretValue) {
throw new Error(validationMessage);
assert(secretValue);
let secretFilePath: string | undefined;
if (secretType === SecretType.FILE) {
secretFilePath = path.resolve(secretValue);
if (!(await fs.pathExists(secretFilePath))) {
throw new Error(`File "${secretValue}" does not exist`);
}
}
if (scope === EnvironmentSecretScope.PROJECT) {
if (force) {
const existingSecrets = await EnvironmentSecretsQuery.byAppIdAsync(projectId);
const { appSecrets: existingSecrets } = await EnvironmentSecretsQuery.byAppIdAsync(
projectId
);
const existingSecret = existingSecrets.find(secret => secret.name === name);
if (existingSecret) {
await EnvironmentSecretMutation.deleteAsync(existingSecret.id);
Log.withTick(
`Deleting existing secret ${chalk.bold(name)} on project ${chalk.bold(
`@${accountName}/${slug}`
projectDisplayName
)}.`
);
}
}
const secret = await EnvironmentSecretMutation.createForAppAsync(
{ name, value: secretValue },
{ name, value: secretValue, type: SecretTypeToEnvironmentSecretType[secretType] },
projectId
);
if (!secret) {
@@ -133,25 +198,24 @@ export default class EnvironmentSecretCreate extends EasCommand {
);
}
Log.withTick(
`️Created a new secret ${chalk.bold(name)} on project ${chalk.bold(
`@${accountName}/${slug}`
)}.`
);
} else if (scope === EnvironmentSecretScope.ACCOUNT) {
const ownerAccount = findAccountByName(actor.accounts, accountName);
if (!ownerAccount) {
Log.warn(
`Your account (${getActorDisplayName(actor)}) doesn't have access to the ${chalk.bold(
accountName
)} account`
if (secretType === SecretType.STRING) {
Log.withTick(
`️Created a new secret ${chalk.bold(name)} with value ${chalk.bold(
secretValue
)} on project ${chalk.bold(projectDisplayName)}.`
);
} else {
Log.withTick(
`️Created a new secret ${chalk.bold(name)} from file ${chalk.bold(
secretFilePath
)} on project ${chalk.bold(projectDisplayName)}.`
);
return;
}
} else if (scope === EnvironmentSecretScope.ACCOUNT) {
if (force) {
const existingSecrets = await EnvironmentSecretsQuery.byAccountNameAsync(ownerAccount.name);
const { accountSecrets: existingSecrets } = await EnvironmentSecretsQuery.byAppIdAsync(
projectId
);
const existingSecret = existingSecrets.find(secret => secret.name === name);
if (existingSecret) {
@@ -166,7 +230,7 @@ export default class EnvironmentSecretCreate extends EasCommand {
}
const secret = await EnvironmentSecretMutation.createForAccountAsync(
{ name, value: secretValue },
{ name, value: secretValue, type: SecretTypeToEnvironmentSecretType[secretType] },
ownerAccount.id
);
@@ -176,9 +240,19 @@ export default class EnvironmentSecretCreate extends EasCommand {
);
}
Log.withTick(
`️Created a new secret ${chalk.bold(name)} on account ${chalk.bold(ownerAccount.name)}.`
);
if (secretType === SecretType.STRING) {
Log.withTick(
`️Created a new secret ${chalk.bold(name)} with value ${chalk.bold(
secretValue
)} on account ${chalk.bold(ownerAccount.name)}.`
);
} else {
Log.withTick(
`️Created a new secret ${chalk.bold(name)} from file ${chalk.bold(
secretFilePath
)} on account ${chalk.bold(ownerAccount.name)}.`
);
}
}
}
}
+34 -33
View File
@@ -1,6 +1,7 @@
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
import {
EnvironmentSecretScope,
@@ -9,38 +10,36 @@ import {
} from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
export default class EnvironmentSecretDelete extends EasCommand {
static description = 'delete an environment secret by ID';
static override description = 'delete an environment secret by ID';
static flags = {
static override flags = {
id: Flags.string({
description: 'ID of the secret to delete',
}),
...EASNonInteractiveFlag,
};
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectAccountName = await getProjectAccountNameAsync(exp);
let {
flags: { id },
flags: { id, 'non-interactive': nonInteractive },
} = await this.parse(EnvironmentSecretDelete);
let secret: EnvironmentSecretWithScope | undefined;
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
if (!id) {
const validationMessage = 'You must select which secret to delete.';
if (nonInteractive) {
throw new Error(validationMessage);
}
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectId);
const secrets = await EnvironmentSecretsQuery.allAsync(projectId);
({ secret } = await promptAsync({
type: 'autocomplete',
name: 'secret',
@@ -58,25 +57,27 @@ export default class EnvironmentSecretDelete extends EasCommand {
}
}
Log.addNewLineIfNone();
Log.warn(
`You are about to permamently delete secret${
secret?.name ? ` "${secret?.name}"` : ''
} with id: "${id}".\nThis action is irreversible.`
);
Log.newLine();
const confirmed = await toggleConfirmAsync({
message: `Are you sure you wish to proceed?${
secret?.scope === EnvironmentSecretScope.ACCOUNT
? ' This secret is applied across your whole account and may affect multiple apps.'
: ''
}`,
});
if (!confirmed) {
Log.error(
`Canceled deletion of secret${secret?.name ? ` "${secret?.name}"` : ''} with id: "${id}".`
if (!nonInteractive) {
Log.addNewLineIfNone();
Log.warn(
`You are about to permanently delete secret${
secret?.name ? ` "${secret?.name}"` : ''
} with id: "${id}".\nThis action is irreversible.`
);
process.exit(1);
Log.newLine();
const confirmed = await toggleConfirmAsync({
message: `Are you sure you wish to proceed?${
secret?.scope === EnvironmentSecretScope.ACCOUNT
? ' This secret is applied across your whole account and may affect multiple apps.'
: ''
}`,
});
if (!confirmed) {
Log.error(
`Canceled deletion of secret${secret?.name ? ` "${secret?.name}"` : ''} with id: "${id}".`
);
process.exit(1);
}
}
await EnvironmentSecretMutation.deleteAsync(id);
+17 -14
View File
@@ -4,37 +4,40 @@ import dateFormat from 'dateformat';
import EasCommand from '../../commandUtils/EasCommand';
import { EnvironmentSecretsQuery } from '../../graphql/queries/EnvironmentSecretsQuery';
import { EnvironmentSecretTypeToSecretType } from '../../graphql/types/EnvironmentSecret';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
export default class EnvironmentSecretList extends EasCommand {
static description = 'list environment secrets available for your current app';
static override description = 'list environment secrets available for your current app';
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectAccountName = await getProjectAccountNameAsync(exp);
if (!projectDir) {
throw new Error("Run this command inside your project's directory");
}
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectId);
const exp = getExpoConfig(projectDir);
// this command is non-interacive by design
const projectId = await getProjectIdAsync(exp, { nonInteractive: true });
const secrets = await EnvironmentSecretsQuery.allAsync(projectId);
const table = new Table({
head: ['Name', 'Scope', 'ID', 'Updated at'],
head: ['Name', 'Type', 'Scope', 'ID', 'Updated at'],
wordWrap: true,
});
for (const secret of secrets) {
const { name, createdAt: updatedAt, scope, id } = secret;
table.push([name, scope, id, dateFormat(updatedAt, 'mmm dd HH:MM:ss')]);
const { name, createdAt: updatedAt, scope, id, type } = secret;
table.push([
name,
EnvironmentSecretTypeToSecretType[type],
scope,
id,
dateFormat(updatedAt, 'mmm dd HH:MM:ss'),
]);
}
Log.log(chalk`{bold Secrets for this account and project:}`);
+49 -24
View File
@@ -1,9 +1,9 @@
import { EasJsonReader } from '@expo/eas-json';
import { EasJsonAccessor } from '@expo/eas-json';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../commandUtils/EasCommand';
import { SubmissionFragment } from '../graphql/generated';
import { StatuspageServiceName, SubmissionFragment } from '../graphql/generated';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log from '../log';
import {
@@ -23,6 +23,7 @@ import {
} from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { getProfilesAsync } from '../utils/profiles';
import { maybeWarnAboutEasOutagesAsync } from '../utils/statuspageService';
interface RawCommandFlags {
platform?: string;
@@ -46,15 +47,16 @@ interface CommandFlags {
}
export default class Submit extends EasCommand {
static description = 'submit app binary to App Store and/or Play Store';
static aliases = ['build:submit'];
static override description = 'submit app binary to App Store and/or Play Store';
static override aliases = ['build:submit'];
static flags = {
static override flags = {
platform: Flags.enum({
char: 'p',
options: ['android', 'ios', 'all'],
}),
profile: Flags.string({
char: 'e',
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
@@ -91,18 +93,23 @@ export default class Submit extends EasCommand {
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(Submit);
const flags = await this.sanitizeFlagsAsync(rawFlags);
const flags = this.sanitizeFlags(rawFlags);
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectId = await getProjectIdAsync(exp, { nonInteractive: flags.nonInteractive });
const platforms = toPlatforms(flags.requestedPlatform);
await maybeWarnAboutEasOutagesAsync([StatuspageServiceName.EasSubmit]);
const flagsWithPlatform = await this.ensurePlatformSelectedAsync(flags);
const platforms = toPlatforms(flagsWithPlatform.requestedPlatform);
const submissionProfiles = await getProfilesAsync({
type: 'submit',
easJsonReader: new EasJsonReader(projectDir),
easJsonAccessor: new EasJsonAccessor(projectDir),
platforms,
profileName: flags.profile,
profileName: flagsWithPlatform.profile,
});
const submissions: SubmissionFragment[] = [];
@@ -112,8 +119,8 @@ export default class Submit extends EasCommand {
projectDir,
projectId,
profile: submissionProfile.profile,
archiveFlags: flags.archiveFlags,
nonInteractive: flags.nonInteractive,
archiveFlags: flagsWithPlatform.archiveFlags,
nonInteractive: flagsWithPlatform.nonInteractive,
});
if (submissionProfiles.length > 1) {
@@ -133,15 +140,17 @@ export default class Submit extends EasCommand {
Log.newLine();
printSubmissionDetailsUrls(submissions);
if (flags.wait) {
if (flagsWithPlatform.wait) {
const completedSubmissions = await waitToCompleteAsync(submissions, {
verbose: flags.verbose,
verbose: flagsWithPlatform.verbose,
});
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
}
}
private async sanitizeFlagsAsync(flags: RawCommandFlags): Promise<CommandFlags> {
private sanitizeFlags(
flags: RawCommandFlags
): Omit<CommandFlags, 'requestedPlatform'> & { requestedPlatform?: RequestedPlatform } {
const {
platform,
verbose,
@@ -155,15 +164,11 @@ export default class Submit extends EasCommand {
Errors.error('--platform is required when building in non-interactive mode', { exit: 1 });
}
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
if (requestedPlatform === RequestedPlatform.All) {
if (archiveFlags.id || archiveFlags.path || archiveFlags.url) {
Errors.error(
'--id, --path, and --url params are only supported when performing a single-platform submit',
{ exit: 1 }
);
}
}
const requestedPlatform =
flags.platform &&
Object.values(RequestedPlatform).includes(flags.platform.toLowerCase() as RequestedPlatform)
? (flags.platform.toLowerCase() as RequestedPlatform)
: undefined;
return {
archiveFlags,
@@ -174,4 +179,24 @@ export default class Submit extends EasCommand {
nonInteractive,
};
}
private async ensurePlatformSelectedAsync(
flags: Omit<CommandFlags, 'requestedPlatform'> & { requestedPlatform?: RequestedPlatform }
): Promise<CommandFlags> {
const requestedPlatform = await selectRequestedPlatformAsync(flags.requestedPlatform);
if (requestedPlatform === RequestedPlatform.All) {
if (flags.archiveFlags.id || flags.archiveFlags.path || flags.archiveFlags.url) {
Errors.error(
'--id, --path, and --url params are only supported when performing a single-platform submit',
{ exit: 1 }
);
}
}
return {
...flags,
requestedPlatform,
};
}
}
@@ -24,9 +24,9 @@ const DEFAULT_MANAGED_RUNTIME_VERSION = { policy: 'sdkVersion' } as const;
const DEFAULT_BARE_RUNTIME_VERSION = '1.0.0' as const;
export default class UpdateConfigure extends EasCommand {
static description = 'configure the project to support EAS Update';
static override description = 'configure the project to support EAS Update';
static flags = {
static override flags = {
platform: Flags.enum({
description: 'Platform to configure',
char: 'p',
@@ -69,14 +69,14 @@ export default class UpdateConfigure extends EasCommand {
[RequestedPlatform.Android, RequestedPlatform.All].includes(platform) &&
androidWorkflow === Workflow.GENERIC
) {
await syncAndroidUpdatesConfigurationAsync(projectDir, updatedExp);
await syncAndroidUpdatesConfigurationAsync(projectDir, updatedExp, { nonInteractive: true });
Log.withTick(`Configured ${chalk.bold('AndroidManifest.xml')} for EAS Update`);
}
if (
[RequestedPlatform.Ios, RequestedPlatform.All].includes(platform) &&
iosWorkflow === Workflow.GENERIC
) {
await syncIosUpdatesConfigurationAsync(projectDir, updatedExp);
await syncIosUpdatesConfigurationAsync(projectDir, updatedExp, { nonInteractive: true });
Log.withTick(`Configured ${chalk.bold('Expo.plist')} for EAS Update`);
}
@@ -98,7 +98,8 @@ async function configureAppJSONForEASUpdateAsync({
[key in RequestedPlatform.Android | RequestedPlatform.Ios]: Workflow;
};
}): Promise<ExpoConfig> {
const projectId = await getProjectIdAsync(exp);
// this command is non-interactive in the way it was designed
const projectId = await getProjectIdAsync(exp, { nonInteractive: true });
const easUpdateURL = getEASUpdateURL(projectId);
const updates = { ...exp.updates, url: easUpdateURL };
@@ -117,6 +118,7 @@ async function configureAppJSONForEASUpdateAsync({
exp.ios?.runtimeVersion ?? exp.runtimeVersion ?? iosDefaultRuntimeVersion;
let newConfig: Partial<ExpoConfig>;
let newConfigOnlyAddedValues: Partial<ExpoConfig>;
switch (platform) {
case RequestedPlatform.All: {
if (isRuntimeEqual(newAndroidRuntimeVersion, newIosRuntimeVersion)) {
@@ -129,6 +131,26 @@ async function configureAppJSONForEASUpdateAsync({
ios: { ...exp.ios, runtimeVersion: undefined },
updates,
};
newConfigOnlyAddedValues = {
runtimeVersion: newAndroidRuntimeVersion,
...(exp.android && 'runtimeVersion' in exp.android
? {
android: {
runtimeVersion: '<remove this key>',
},
}
: {}),
...(exp.ios && 'runtimeVersion' in exp.ios
? {
ios: {
runtimeVersion: '<remove this key>',
},
}
: {}),
updates: {
url: easUpdateURL,
},
};
} else {
newConfig = {
runtimeVersion: undefined, // top level runtime is redundant if it is specified in both android and ios
@@ -142,6 +164,22 @@ async function configureAppJSONForEASUpdateAsync({
},
updates,
};
newConfigOnlyAddedValues = {
...('runtimeVersion' in exp
? {
runtimeVersion: '<remove this key>', // top level runtime is redundant if it is specified in both android and ios
}
: {}),
android: {
runtimeVersion: newAndroidRuntimeVersion,
},
ios: {
runtimeVersion: newIosRuntimeVersion,
},
updates: {
url: easUpdateURL,
},
};
}
break;
}
@@ -153,6 +191,14 @@ async function configureAppJSONForEASUpdateAsync({
},
updates,
};
newConfigOnlyAddedValues = {
android: {
runtimeVersion: newAndroidRuntimeVersion,
},
updates: {
url: easUpdateURL,
},
};
break;
}
case RequestedPlatform.Ios: {
@@ -163,6 +209,14 @@ async function configureAppJSONForEASUpdateAsync({
},
updates,
};
newConfigOnlyAddedValues = {
ios: {
runtimeVersion: newIosRuntimeVersion,
},
updates: {
url: easUpdateURL,
},
};
break;
}
default: {
@@ -172,8 +226,8 @@ async function configureAppJSONForEASUpdateAsync({
const result = await modifyConfigAsync(projectDir, newConfig);
const prexistingAndroidRuntimeVersion = exp.android?.runtimeVersion ?? exp.runtimeVersion;
const prexistingIosRuntimeVersion = exp.ios?.runtimeVersion ?? exp.runtimeVersion;
const preexistingAndroidRuntimeVersion = exp.android?.runtimeVersion ?? exp.runtimeVersion;
const preexistingIosRuntimeVersion = exp.ios?.runtimeVersion ?? exp.runtimeVersion;
switch (result.type) {
case 'success':
if (exp.updates?.url) {
@@ -187,7 +241,7 @@ async function configureAppJSONForEASUpdateAsync({
}
if (
!prexistingAndroidRuntimeVersion &&
!preexistingAndroidRuntimeVersion &&
[RequestedPlatform.Android, RequestedPlatform.All].includes(platform)
) {
Log.withTick(
@@ -197,7 +251,7 @@ async function configureAppJSONForEASUpdateAsync({
);
}
if (
!prexistingIosRuntimeVersion &&
!preexistingIosRuntimeVersion &&
[RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)
) {
Log.withTick(
@@ -219,7 +273,7 @@ async function configureAppJSONForEASUpdateAsync({
'https://expo.fyi/eas-update-config.md'
)}\n`
);
Log.log(chalk.bold(JSON.stringify(newConfig, null, 2)));
Log.log(chalk.bold(JSON.stringify(newConfigOnlyAddedValues, null, 2)));
Log.addNewLineIfNone();
if (workflows['android'] === Workflow.GENERIC || workflows['ios'] === Workflow.GENERIC) {
Log.warn(
+10 -11
View File
@@ -1,8 +1,8 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
DeleteUpdateGroupMutation,
@@ -36,9 +36,9 @@ async function deleteUpdateGroupAsync({
}
export default class UpdateDelete extends EasCommand {
static description = 'delete all the updates in an update group';
static override description = 'delete all the updates in an update group';
static args = [
static override args = [
{
name: 'groupId',
required: true,
@@ -46,22 +46,21 @@ export default class UpdateDelete extends EasCommand {
},
];
static flags = {
json: Flags.boolean({
description: `Return a json with the group ID of the deleted updates.`,
default: false,
}),
static override flags = {
...EasNonInteractiveAndJsonFlags,
};
async runAsync(): Promise<void> {
const {
args: { groupId: group },
flags: { json: jsonFlag },
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
} = await this.parse(UpdateDelete);
if (jsonFlag) {
enableJsonOutput();
} else {
}
if (!nonInteractive) {
const shouldAbort = await confirmAsync({
message:
`🚨${chalk.red('CAUTION')}🚨\n\n` +
@@ -69,7 +68,7 @@ export default class UpdateDelete extends EasCommand {
'This is a permanent operation.'
)}\n\n` +
`If you want to revert to a previous publish, you should use 'update --republish' targeted at the last working update group instead.\n\n` +
`An update group should only be deleted in an emergency like an accidental publish of a secret. In this case user 'update --republish' to revert to the last working update group first and then proceed with the deletion. Deleting an update group when it is the latest publish can lead to inconsistent cacheing behavior by clients.\n\n` +
`An update group should only be deleted in an emergency like an accidental publish of a secret. In this case user 'update --republish' to revert to the last working update group first and then proceed with the deletion. Deleting an update group when it is the latest publish can lead to inconsistent caching behavior by clients.\n\n` +
`Would you like to abort?`,
});
+94 -250
View File
@@ -4,26 +4,23 @@ import { Platform, Workflow } from '@expo/eas-build-job';
import { Errors, Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import dateFormat from 'dateformat';
import gql from 'graphql-tag';
import nullthrows from 'nullthrows';
import { getEASUpdateURL } from '../../api';
import { BRANCHES_LIMIT } from '../../branch/queries';
import { selectBranchOnAppAsync } from '../../branch/queries';
import { BranchNotFoundError, getDefaultBranchNameAsync } from '../../branch/utils';
import { getUpdateGroupUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import fetch from '../../fetch';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
GetUpdateGroupAsyncQuery,
PublishUpdateGroupInput,
Robot,
RootQueryUpdatesByGroupArgs,
StatuspageServiceName,
Update,
UpdateInfoGroup,
UpdatePublishMutation,
User,
ViewBranchUpdatesQuery,
ViewBranchQueryVariables,
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
@@ -33,7 +30,7 @@ import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectAccountName,
getOwnerAccountForProjectIdAsync,
getProjectIdAsync,
installExpoUpdatesAsync,
isExpoUpdatesInstalledOrAvailable,
@@ -47,18 +44,18 @@ import {
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
import { formatUpdate } from '../../update/utils';
import { ensureLoggedInAsync } from '../../user/actions';
import { confirmAsync, promptAsync } from '../../prompts';
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
import { formatUpdateMessage } from '../../update/utils';
import {
checkManifestBodyAgainstUpdateInfoGroup,
getCodeSigningInfoAsync,
getManifestBodyAsync,
signManifestBody,
} from '../../utils/code-signing';
import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
import { getVcsClient } from '../../vcs';
import { createUpdateBranchOnAppAsync } from '../branch/create';
import { createUpdateChannelOnAppAsync } from '../channel/create';
@@ -66,34 +63,6 @@ import { createUpdateChannelOnAppAsync } from '../channel/create';
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
export type PublishPlatformFlag = PublishPlatform | 'all';
async function getUpdateGroupAsync({
group,
}: RootQueryUpdatesByGroupArgs): Promise<GetUpdateGroupAsyncQuery['updatesByGroup']> {
const { updatesByGroup } = await withErrorHandlingAsync(
graphqlClient
.query<GetUpdateGroupAsyncQuery, RootQueryUpdatesByGroupArgs>(
gql`
query getUpdateGroupAsync($group: ID!) {
updatesByGroup(group: $group) {
id
group
runtimeVersion
manifestFragment
platform
message
}
}
`,
{
group,
},
{ additionalTypenames: ['Update'] }
)
.toPromise()
);
return updatesByGroup;
}
async function ensureChannelExistsAsync({
appId,
branchId,
@@ -125,43 +94,38 @@ async function ensureChannelExistsAsync({
export async function ensureBranchExistsAsync({
appId,
name: branchName,
limit,
offset,
}: {
appId: string;
name: string;
limit: number;
offset: number;
}): Promise<{
id: string;
updates: Exclude<
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
null | undefined
>['updates'];
}: ViewBranchQueryVariables): Promise<{
branchId: string;
}> {
const { app } = await UpdateQuery.viewBranchAsync({
appId,
name: branchName,
limit,
offset,
});
const updateBranch = app?.byId.updateBranchByName;
if (updateBranch) {
const { id, updates } = updateBranch;
await ensureChannelExistsAsync({ appId, branchId: id, channelName: branchName });
return { id, updates };
}
try {
const updateBranch = await BranchQuery.getBranchByNameAsync({
appId,
name: branchName,
});
const newUpdateBranch = await createUpdateBranchOnAppAsync({ appId, name: branchName });
Log.withTick(`Created branch: ${chalk.bold(branchName)}`);
await ensureChannelExistsAsync({ appId, branchId: newUpdateBranch.id, channelName: branchName });
return { id: newUpdateBranch.id, updates: [] };
const { id } = updateBranch;
await ensureChannelExistsAsync({ appId, branchId: id, channelName: branchName });
return { branchId: id };
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newUpdateBranch = await createUpdateBranchOnAppAsync({ appId, name: branchName });
Log.withTick(`Created branch: ${chalk.bold(branchName)}`);
await ensureChannelExistsAsync({
appId,
branchId: newUpdateBranch.id,
channelName: branchName,
});
return { branchId: newUpdateBranch.id };
} else {
throw error;
}
}
}
export default class UpdatePublish extends EasCommand {
static description = 'publish an update group';
static override description = 'publish an update group';
static flags = {
static override flags = {
branch: Flags.string({
description: 'Branch to publish the update group on',
required: false,
@@ -193,10 +157,6 @@ export default class UpdatePublish extends EasCommand {
default: 'all',
required: false,
}),
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
default: false,
}),
auto: Flags.boolean({
description:
'Use the current git branch and commit message for the EAS branch and update message',
@@ -206,35 +166,33 @@ export default class UpdatePublish extends EasCommand {
description: `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.`,
required: false,
}),
'non-interactive': Flags.boolean({
default: false,
description: 'Run command in non-interactive mode',
}),
...EasNonInteractiveAndJsonFlags,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(UpdatePublish);
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
let {
flags: {
branch: branchName,
json: jsonFlag,
auto: autoFlag,
message,
republish,
group,
'input-dir': inputDir,
'skip-bundler': skipBundler,
platform,
'private-key-path': privateKeyPath,
'non-interactive': nonInteractive,
},
} = await this.parse(UpdatePublish);
branch: branchName,
auto: autoFlag,
message,
republish,
group,
'input-dir': inputDir,
'skip-bundler': skipBundler,
platform,
'private-key-path': privateKeyPath,
'non-interactive': nonInteractive,
json: jsonFlag,
} = flags;
if (jsonFlag) {
enableJsonOutput();
}
const platformFlag = platform as PublishPlatformFlag;
// If a group was specified, that means we are republishing it.
republish = group ? true : republish;
republish = republish || !!group;
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir, {
@@ -245,6 +203,8 @@ export default class UpdatePublish extends EasCommand {
isPublicConfig: false,
});
await maybeWarnAboutEasOutagesAsync([StatuspageServiceName.EasUpdate]);
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion);
@@ -274,54 +234,41 @@ export default class UpdatePublish extends EasCommand {
}
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
const projectId = await getProjectIdAsync(exp);
await checkEASUpdateURLIsSetAsync(exp);
if (!branchName && autoFlag) {
branchName =
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substr(2, 4)}`;
}
const projectId = await getProjectIdAsync(exp, { nonInteractive });
await checkEASUpdateURLIsSetAsync(exp, projectId);
if (!branchName) {
if (nonInteractive) {
if (autoFlag) {
branchName = await getDefaultBranchNameAsync();
} else if (nonInteractive) {
throw new Error('Must supply --branch or use --auto when in non-interactive mode');
}
const validationMessage = 'Branch name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
const branches = await BranchQuery.listBranchesAsync({
appId: projectId,
limit: BRANCHES_LIMIT,
offset: 0,
});
if (branches.length === 0) {
({ name: branchName } = await promptAsync({
type: 'text',
name: 'name',
message: 'No branches found. Provide a branch name:',
initial:
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substr(2, 4)}`,
validate: value => (value ? true : validationMessage),
}));
} else {
branchName = await selectAsync<string>(
'Which branch would you like to publish on?',
branches.map(branch => {
return {
title: `${branch.name} ${chalk.grey(
`- current update: ${formatUpdate(branch.updates[0])}`
try {
const branch = await selectBranchOnAppAsync({
projectId,
promptTitle: `Which branch would you like to ${
republish ? 'republish' : 'publish'
} on?`,
displayTextForListItem: updateBranch =>
`${updateBranch.name} ${chalk.grey(
`- current update: ${formatUpdateMessage(updateBranch.updates[0])}`
)}`,
value: branch.name,
};
})
);
paginatedQueryOptions,
});
branchName = branch.name;
} catch {
// unable to select a branch (network error or no branches for project)
({ name: branchName } = await promptAsync({
type: 'text',
name: 'name',
message: 'No branches found. Provide a branch name:',
initial: await getDefaultBranchNameAsync(),
validate: value => (value ? true : 'Branch name may not be empty.'),
}));
}
assert(branchName, 'Branch name must be specified.');
}
assert(branchName, 'Branch name must be specified.');
}
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
@@ -337,23 +284,18 @@ export default class UpdatePublish extends EasCommand {
'group' | 'message' | 'runtimeVersion' | 'manifestFragment' | 'platform'
>[];
if (group) {
updatesToRepublish = await getUpdateGroupAsync({ group });
const updatesByGroup = await UpdateQuery.viewUpdateGroupAsync({ groupId: group });
updatesToRepublish = updatesByGroup;
} else {
if (nonInteractive) {
throw new Error('Must supply --group when in non-interactive mode');
}
// Drop into interactive mode if the user has not specified an update group to republish.
if (jsonFlag) {
throw new Error('You must specify the update group to republish.');
}
updatesToRepublish = await getUpdatesToRepublishInteractiveAsync(
updatesToRepublish = await selectUpdateGroupOnBranchAsync({
projectId,
branchName,
platformFlag,
50
);
paginatedQueryOptions,
});
}
const updatesToRepublishFilteredByPlatform = updatesToRepublish.filter(
// Only republish to the specified platforms
@@ -436,7 +378,7 @@ export default class UpdatePublish extends EasCommand {
if (!skipBundler) {
const bundleSpinner = ora().start('Building bundle...');
try {
await buildBundlesAsync({ projectDir, inputDir });
await buildBundlesAsync({ projectDir, inputDir, platform: platformFlag });
bundleSpinner.succeed('Built bundle!');
} catch (e) {
bundleSpinner.fail('Failed to build bundle!');
@@ -479,11 +421,9 @@ export default class UpdatePublish extends EasCommand {
.map(pair => pair[0]);
}
const { id: branchId } = await ensureBranchExistsAsync({
const { branchId } = await ensureBranchExistsAsync({
appId: projectId,
name: branchName,
limit: 0,
offset: 0,
});
// Sort the updates into different groups based on their platform specific runtime versions
@@ -589,7 +529,7 @@ export default class UpdatePublish extends EasCommand {
const updateGroupId = newUpdatesForRuntimeVersion[0].group;
const projectName = exp.slug;
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const accountName = (await getOwnerAccountForProjectIdAsync(projectId)).name;
const updateGroupUrl = getUpdateGroupUrl(accountName, projectName, updateGroupId);
const updateGroupLink = link(updateGroupUrl, { dim: false });
@@ -625,73 +565,6 @@ export default class UpdatePublish extends EasCommand {
}
}
export async function getUpdatesToRepublishInteractiveAsync(
projectId: string,
branchName: string,
platformFlag: string,
pageSize: number,
offset: number = 0,
cumulativeUpdates: Exclude<
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
null | undefined
>['updates'] = []
): Promise<
Exclude<
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
null | undefined
>['updates']
> {
const fetchMoreValue = '_fetchMore';
const { updates } = await ensureBranchExistsAsync({
appId: projectId,
name: branchName,
limit: pageSize + 1, // fetch an extra item so we know if there are additional pages
offset,
});
cumulativeUpdates = [
...cumulativeUpdates,
// drop that extra item used for pagination from our render logic
...updates.slice(0, updates.length - 1),
];
const cumulativeUpdatesForTargetPlatforms =
platformFlag === 'all'
? cumulativeUpdates
: cumulativeUpdates.filter(update => {
// Only show groups that have updates on the specified platform(s).
return update.platform === platformFlag;
});
const updateGroups = uniqBy(cumulativeUpdatesForTargetPlatforms, u => u.group).map(update => ({
title: formatUpdateTitle(update),
value: update.group,
}));
if (updateGroups.length === 0) {
throw new Error(
`There are no updates on branch "${branchName}" published for the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
);
}
if (updates.length > pageSize) {
updateGroups.push({ title: 'Next page...', value: fetchMoreValue });
}
const selectedUpdateGroup = await selectAsync<string>(
'Which update would you like to republish?',
updateGroups
);
if (selectedUpdateGroup === fetchMoreValue) {
return await getUpdatesToRepublishInteractiveAsync(
projectId,
branchName,
platformFlag,
pageSize,
offset + pageSize,
cumulativeUpdates
);
}
return cumulativeUpdates.filter(update => update.group === selectedUpdateGroup);
}
async function getRuntimeVersionObjectAsync(
exp: ExpoConfig,
platformFlag: PublishPlatformFlag,
@@ -722,37 +595,8 @@ async function getRuntimeVersionObjectAsync(
);
}
function formatUpdateTitle(
update: Exclude<
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
null | undefined
>['updates'][number]
): string {
const { message, createdAt, actor, runtimeVersion } = update;
let actorName: string;
switch (actor?.__typename) {
case 'User': {
actorName = (actor as Pick<User, 'username' | 'id'>).username;
break;
}
case 'Robot': {
const { firstName, id } = actor as Pick<Robot, 'firstName' | 'id'>;
actorName = firstName ?? `robot: ${id.slice(0, 4)}...`;
break;
}
default:
actorName = 'unknown';
}
return `[${dateFormat(
createdAt,
'mmm dd HH:MM'
)} by ${actorName}, runtimeVersion: ${runtimeVersion}] ${message}`;
}
async function checkEASUpdateURLIsSetAsync(exp: ExpoConfig): Promise<void> {
async function checkEASUpdateURLIsSetAsync(exp: ExpoConfig, projectId: string): Promise<void> {
const configuredURL = exp.updates?.url;
const projectId = await getProjectIdAsync(exp);
const expectedURL = getEASUpdateURL(projectId);
if (configuredURL !== expectedURL) {
+50 -89
View File
@@ -1,124 +1,85 @@
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import CliTable from 'cli-table3';
import { selectBranchOnAppAsync } from '../../branch/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log from '../../log';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import {
EasPaginatedQueryFlags,
getLimitFlagWithCustomValues,
getPaginatedQueryOptions,
} from '../../commandUtils/pagination';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { UPDATES_LIMIT } from '../../update/queries';
import {
UPDATE_COLUMNS,
UpdateGroupDescription,
formatUpdate,
getUpdateGroupsWithPlatforms,
} from '../../update/utils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { getVcsClient } from '../../vcs';
listAndRenderUpdateGroupsOnAppAsync,
listAndRenderUpdateGroupsOnBranchAsync,
} from '../../update/queries';
import { enableJsonOutput } from '../../utils/json';
export default class BranchView extends EasCommand {
static description = 'view the recent updates for a branch';
export default class UpdateList extends EasCommand {
static override description = 'view the recent updates';
static flags = {
static override flags = {
branch: Flags.string({
description: 'List all updates on this branch',
description: 'List updates only on this branch',
exclusive: ['all'],
}),
all: Flags.boolean({
description: 'List all updates associated with this project',
description: 'List updates on all branches',
exclusive: ['branch'],
default: false,
}),
json: Flags.boolean({
description: `Return a json with all of the recent update groups.`,
default: false,
}),
...EasPaginatedQueryFlags,
limit: getLimitFlagWithCustomValues({ defaultTo: 25, limit: 50 }),
...EasNonInteractiveAndJsonFlags,
};
async runAsync(): Promise<void> {
const {
flags: { branch: branchFlag, all, json: jsonFlag },
} = await this.parse(BranchView);
const { flags } = await this.parse(UpdateList);
const { branch: branchFlag, all, json: jsonFlag, 'non-interactive': nonInteractive } = flags;
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
let updateGroupDescriptions: UpdateGroupDescription[];
if (all) {
const branchesAndUpdates = await UpdateQuery.viewAllAsync({
appId: projectId,
limit: UPDATES_LIMIT,
offset: 0,
});
updateGroupDescriptions = getUpdateGroupsWithPlatforms(
branchesAndUpdates.app.byId.updates.map(update => ({
...update,
branch: update.branch.name,
}))
).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
listAndRenderUpdateGroupsOnAppAsync({ projectId, paginatedQueryOptions });
} else {
let branchInteractive: string | undefined;
if (!branchFlag) {
if (branchFlag) {
listAndRenderUpdateGroupsOnBranchAsync({
projectId,
branchName: branchFlag,
paginatedQueryOptions,
});
} else {
const validationMessage = 'Branch name may not be empty.';
if (jsonFlag) {
if (nonInteractive) {
throw new Error(validationMessage);
}
({ name: branchInteractive } = await promptAsync({
type: 'text',
name: 'name',
message: 'Provide the name of the branch whose updates you wish to view:',
initial: (await getVcsClient().getBranchNameAsync()) ?? undefined,
validate: (value: any) => (value ? true : validationMessage),
}));
}
const branch = branchFlag ?? branchInteractive;
assert(branch, 'Branch name may not be empty.');
const branchesAndUpdates = await UpdateQuery.viewBranchAsync({
appId: projectId,
name: branch,
limit: UPDATES_LIMIT,
offset: 0,
});
const UpdateBranch = branchesAndUpdates.app?.byId.updateBranchByName;
if (!UpdateBranch) {
throw new Error(`Could not find branch "${branch}"`);
const selectedBranch = await selectBranchOnAppAsync({
projectId,
promptTitle: 'Which branch would you like to view?',
displayTextForListItem: updateBranch => updateBranch.name,
paginatedQueryOptions:
// discard limit and offset because this query is not those flag's intended target
{
json: paginatedQueryOptions.json,
nonInteractive: paginatedQueryOptions.nonInteractive,
offset: 0,
},
});
listAndRenderUpdateGroupsOnBranchAsync({
projectId,
branchName: selectedBranch.name,
paginatedQueryOptions,
});
}
updateGroupDescriptions = getUpdateGroupsWithPlatforms(
UpdateBranch.updates.map(update => ({ ...update, branch }))
).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
}
if (jsonFlag) {
printJsonOnlyOutput(updateGroupDescriptions);
} else {
logAsTable(updateGroupDescriptions);
}
}
}
function logAsTable(updateGroupDescriptions: UpdateGroupDescription[]): void {
const table = new CliTable({
head: ['Branch', ...UPDATE_COLUMNS],
wordWrap: true,
});
table.push(
...updateGroupDescriptions.map(updateGroupDescription => [
updateGroupDescription.branch,
formatUpdate(updateGroupDescription),
updateGroupDescription.runtimeVersion,
updateGroupDescription.group,
updateGroupDescription.platforms,
])
);
Log.addNewLineIfNone();
Log.log(chalk.bold('Recently published update groups:'));
Log.log(table.toString());
}
+18 -64
View File
@@ -1,59 +1,18 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import Table from 'cli-table3';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { UpdatesByGroupQuery, UpdatesByGroupQueryVariables } from '../../graphql/generated';
import { EasJsonOnlyFlag } from '../../commandUtils/flags';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log from '../../log';
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
import { UPDATE_COLUMNS, getUpdateGroupDescriptions } from '../../update/utils';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export async function viewUpdateAsync({
groupId,
}: {
groupId: string;
}): Promise<UpdatesByGroupQuery> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<UpdatesByGroupQuery, UpdatesByGroupQueryVariables>(
gql`
query UpdatesByGroup($groupId: ID!) {
updatesByGroup(group: $groupId) {
id
group
runtimeVersion
platform
message
actor {
id
... on User {
username
}
... on Robot {
firstName
}
}
createdAt
}
}
`,
{
groupId,
},
{ additionalTypenames: ['Update'] }
)
.toPromise()
);
if (data.updatesByGroup.length === 0) {
throw new Error(`Could not find any updates with group ID: "${groupId}"`);
}
return data;
}
export default class UpdateView extends EasCommand {
static description = 'update group details';
static override description = 'update group details';
static args = [
static override args = [
{
name: 'groupId',
required: true,
@@ -61,11 +20,8 @@ export default class UpdateView extends EasCommand {
},
];
static flags = {
json: Flags.boolean({
description: `Return a json with the updates belonging to the group.`,
default: false,
}),
static override flags = {
...EasJsonOnlyFlag,
};
async runAsync(): Promise<void> {
@@ -77,27 +33,25 @@ export default class UpdateView extends EasCommand {
enableJsonOutput();
}
const { updatesByGroup } = await viewUpdateAsync({ groupId });
const updatesByGroup = await UpdateQuery.viewUpdateGroupAsync({ groupId });
const [updateGroupDescription] = getUpdateGroupDescriptions([updatesByGroup]);
if (jsonFlag) {
printJsonOnlyOutput(updatesByGroup);
printJsonOnlyOutput(updateGroupDescription);
} else {
const groupTable = new Table({
head: [...UPDATE_COLUMNS],
wordWrap: true,
});
const representativeUpdate = updatesByGroup[0];
Log.log(chalk.bold('Update group:'));
Log.log(formatFields([{ label: 'ID', value: updateGroupDescription.group }]));
groupTable.push([
formatUpdate(representativeUpdate),
representativeUpdate.runtimeVersion,
representativeUpdate.group,
getPlatformsForGroup({
updates: updatesByGroup,
group: updatesByGroup[0]?.group,
}),
updateGroupDescription.message,
updateGroupDescription.runtimeVersion,
updateGroupDescription.group,
updateGroupDescription.platforms,
]);
Log.log(groupTable.toString());
}
}
@@ -1,6 +1,7 @@
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { WebhookType } from '../../graphql/generated';
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
import { ora } from '../../ora';
@@ -9,9 +10,9 @@ import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUt
import { prepareInputParamsAsync } from '../../webhooks/input';
export default class WebhookCreate extends EasCommand {
static description = 'create a webhook';
static override description = 'create a webhook';
static flags = {
static override flags = {
event: Flags.enum({
description: 'Event type that triggers the webhook',
options: [WebhookType.Build, WebhookType.Submit],
@@ -23,6 +24,7 @@ export default class WebhookCreate extends EasCommand {
description:
"Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature' header.",
}),
...EASNonInteractiveFlag,
};
async runAsync(): Promise<void> {
@@ -31,7 +33,8 @@ export default class WebhookCreate extends EasCommand {
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectId = await getProjectIdAsync(exp, { nonInteractive: flags['non-interactive'] });
const spinner = ora('Creating a webhook').start();
try {
+24 -11
View File
@@ -3,6 +3,7 @@ import chalk from 'chalk';
import nullthrows from 'nullthrows';
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { WebhookFragment } from '../../graphql/generated';
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
@@ -14,9 +15,9 @@ import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { formatWebhook } from '../../webhooks/formatWebhook';
export default class WebhookDelete extends EasCommand {
static description = 'delete a webhook';
static override description = 'delete a webhook';
static args = [
static override args = [
{
name: 'ID',
required: false,
@@ -24,14 +25,19 @@ export default class WebhookDelete extends EasCommand {
},
];
static override flags = {
...EASNonInteractiveFlag,
};
async runAsync(): Promise<void> {
let {
args: { ID: webhookId },
flags: { 'non-interactive': nonInteractive },
} = await this.parse(WebhookDelete);
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
let webhook: WebhookFragment | undefined =
webhookId && (await WebhookQuery.byIdAsync(webhookId));
@@ -40,6 +46,11 @@ export default class WebhookDelete extends EasCommand {
if (webhooks.length === 0) {
process.exit(1);
}
if (nonInteractive) {
throw new Error('Must supply ID argument in non-interactive mode');
}
({ webhook } = await promptAsync({
type: 'autocomplete',
name: 'webhook',
@@ -57,15 +68,17 @@ export default class WebhookDelete extends EasCommand {
Log.addNewLineIfNone();
Log.log(formatWebhook(webhook));
Log.newLine();
Log.warn(`You are about to permamently delete this webhook.\nThis action is irreversible.`);
Log.newLine();
const confirmed = await toggleConfirmAsync({
message: 'Are you sure you wish to proceed?',
});
if (!confirmed) {
Log.error(`Canceled deletion of the webhook`);
process.exit(1);
if (!nonInteractive) {
Log.warn(`You are about to permanently delete this webhook.\nThis action is irreversible.`);
Log.newLine();
const confirmed = await toggleConfirmAsync({
message: 'Are you sure you wish to proceed?',
});
if (!confirmed) {
Log.error(`Canceled deletion of the webhook`);
process.exit(1);
}
}
const spinner = ora('Deleting the webhook').start();
+11 -9
View File
@@ -9,15 +9,15 @@ import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { formatWebhook } from '../../webhooks/formatWebhook';
export default class WebhookList extends EasCommand {
static description = 'list webhooks';
static override description = 'list webhooks';
static flags = {
static override flags = {
event: Flags.enum({
description: 'Event type that triggers the webhook',
options: [WebhookType.Build, WebhookType.Submit],
@@ -31,16 +31,18 @@ export default class WebhookList extends EasCommand {
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectFullName = await getProjectFullNameAsync(exp);
const spinner = ora(`Fetching the list of webhook on project ${projectFullName}`).start();
// this command is non-interactive by design
const projectId = await getProjectIdAsync(exp, { nonInteractive: true });
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
const spinner = ora(`Fetching the list of webhook on project ${projectDisplayName}`).start();
try {
const webhooks = await WebhookQuery.byAppIdAsync(projectId, event && { event });
if (webhooks.length === 0) {
spinner.fail(`There are no webhooks on project ${projectFullName}`);
spinner.fail(`There are no webhooks on project ${projectDisplayName}`);
} else {
spinner.succeed(`Found ${webhooks.length} webhooks on project ${projectFullName}`);
spinner.succeed(`Found ${webhooks.length} webhooks on project ${projectDisplayName}`);
const list = webhooks
.map(webhook => formatWebhook(webhook))
.join(`\n\n${chalk.dim('———')}\n\n`);
@@ -48,7 +50,7 @@ export default class WebhookList extends EasCommand {
}
} catch (err) {
spinner.fail(
`Something went wrong and we couldn't fetch the webhook list for the project ${projectFullName}`
`Something went wrong and we couldn't fetch the webhook list for the project ${projectDisplayName}`
);
throw err;
}
@@ -1,6 +1,7 @@
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { WebhookType } from '../../graphql/generated';
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
@@ -9,9 +10,9 @@ import pick from '../../utils/expodash/pick';
import { prepareInputParamsAsync } from '../../webhooks/input';
export default class WebhookUpdate extends EasCommand {
static description = 'update a webhook';
static override description = 'update a webhook';
static flags = {
static override flags = {
id: Flags.string({
description: 'Webhook ID',
required: true,
@@ -27,6 +28,7 @@ export default class WebhookUpdate extends EasCommand {
description:
"Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature' header.",
}),
...EASNonInteractiveFlag,
};
async runAsync(): Promise<void> {
@@ -36,7 +38,7 @@ export default class WebhookUpdate extends EasCommand {
const webhook = await WebhookQuery.byIdAsync(webhookId);
const webhookInputParams = await prepareInputParamsAsync(
pick(flags, ['event', 'url', 'secret']),
pick(flags, ['event', 'url', 'secret', 'non-interactive']),
webhook
);
@@ -5,9 +5,9 @@ import { ora } from '../../ora';
import { formatWebhook } from '../../webhooks/formatWebhook';
export default class WebhookView extends EasCommand {
static description = 'view a webhook';
static override description = 'view a webhook';
static args = [
static override args = [
{
name: 'ID',
required: true,
@@ -55,6 +55,10 @@ export const testAppFragment: AppFragment = {
id: 'test-app-id',
fullName: '@testuser/testapp',
slug: 'testapp',
ownerAccount: {
id: 'test-account-id',
name: 'testuser',
},
};
export const testLegacyAndroidFcmFragment: AndroidFcmFragment = {
@@ -1,3 +1,4 @@
import { AppFragment } from '../../graphql/generated';
import { Actor } from '../../user/User';
export const jester: Actor = {
@@ -47,3 +48,10 @@ export const testAppJsonWithDifferentOwner = {
...testAppJson,
owner: jester2.username,
};
export const testAppQueryByIdResponse: AppFragment = {
id: testProjectId,
slug: testSlug,
fullName: testExperienceName,
ownerAccount: jester.accounts[0],
};
@@ -55,6 +55,10 @@ export const testAppFragment: AppFragment = {
id: 'test-app-id',
fullName: '@testuser/testapp',
slug: 'testapp',
ownerAccount: {
id: 'test-account-id',
name: 'testuser',
},
};
export const testAscApiKeyFragment: AppStoreConnectApiKeyFragment = {
@@ -76,9 +80,13 @@ export const testPushKey: ApplePushKeyFragment = {
iosAppCredentialsList: [],
};
export const testTargets: Target[] = [
{ targetName: 'testapp', bundleIdentifier: 'foo.bar.com', entitlements: {} },
];
export const testTarget = {
targetName: 'testapp',
bundleIdentifier: 'foo.bar.com',
entitlements: {},
};
export const testTargets: Target[] = [testTarget];
export const testProvisioningProfileFragment: AppleProvisioningProfileFragment = {
id: 'test-prov-prof-id-1',
@@ -5,9 +5,12 @@ import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
import { ora } from '../../../ora';
import { getApplicationIdAsync } from '../../../project/android/applicationId';
import { GradleBuildContext } from '../../../project/android/gradle';
import { getProjectAccountName, getProjectConfigDescription } from '../../../project/projectUtils';
import {
getOwnerAccountForProjectIdAsync,
getProjectConfigDescription,
getProjectIdAsync,
} from '../../../project/projectUtils';
import { promptAsync } from '../../../prompts';
import { findAccountByName } from '../../../user/Account';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
@@ -90,11 +93,8 @@ export async function getAppLookupParamsFromContextAsync(
): Promise<AppLookupParams> {
ctx.ensureProjectContext();
const projectName = ctx.exp.slug;
const accountName = getProjectAccountName(ctx.exp, ctx.user);
const account = findAccountByName(ctx.user.accounts, accountName);
if (!account) {
throw new Error(`You do not have access to account: ${accountName}`);
}
const projectId = await getProjectIdAsync(ctx.exp, { nonInteractive: ctx.nonInteractive });
const account = await getOwnerAccountForProjectIdAsync(projectId);
const androidApplicationIdentifier = await getApplicationIdAsync(
ctx.projectDir,
@@ -16,7 +16,7 @@ export class CreateKeystore {
throw new Error(`New keystore cannot be created in non-interactive mode.`);
}
const projectId = await getProjectIdAsync(ctx.exp);
const projectId = await getProjectIdAsync(ctx.exp, { nonInteractive: ctx.nonInteractive });
const keystore = await this.provideOrGenerateAsync(projectId);
const keystoreFragment = await ctx.android.createKeystoreAsync(this.account, keystore);
Log.succeed('Created keystore');
@@ -1,9 +1,16 @@
import { AppQuery } from '../../../../graphql/queries/AppQuery';
import { testLegacyAndroidFcmFragment } from '../../../__tests__/fixtures-android';
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { AssignFcm } from '../AssignFcm';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
jest.mock('../../../../graphql/queries/AppQuery');
describe(AssignFcm, () => {
beforeEach(() => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
it('assigns an fcm api key in Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: false,
@@ -1,9 +1,16 @@
import { AppQuery } from '../../../../graphql/queries/AppQuery';
import { testGoogleServiceAccountKeyFragment } from '../../../__tests__/fixtures-android';
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { AssignGoogleServiceAccountKey } from '../AssignGoogleServiceAccountKey';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
jest.mock('../../../../graphql/queries/AppQuery');
describe(AssignGoogleServiceAccountKey, () => {
beforeEach(() => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
it('assigns a Google Service Account key in Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: false,
@@ -1,3 +1,4 @@
import { AppQuery } from '../../../../graphql/queries/AppQuery';
import { confirmAsync, promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -7,6 +8,7 @@ import {
testLegacyAndroidBuildCredentialsFragment,
testLegacyAndroidFcmFragment,
} from '../../../__tests__/fixtures-android';
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import {
canCopyLegacyCredentialsAsync,
@@ -17,8 +19,12 @@ import {
jest.mock('../../../../ora');
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
jest.mock('../../../../graphql/queries/AppQuery');
describe('BuildCredentialsUtils', () => {
beforeEach(() => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
describe(canCopyLegacyCredentialsAsync, () => {
it('returns true if the user has legacy credentials and no modern ones', async () => {
const ctx = createCtxMock({
@@ -1,12 +1,19 @@
import { AppQuery } from '../../../../graphql/queries/AppQuery';
import { promptAsync } from '../../../../prompts';
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateFcm } from '../CreateFcm';
jest.mock('../../../../prompts');
jest.mocked(promptAsync).mockImplementation(async () => ({ fcmApiKey: 'blah' }));
jest.mock('../../../../graphql/queries/AppQuery');
describe(CreateFcm, () => {
beforeEach(() => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
it('creates an fcm api key in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
@@ -1,6 +1,8 @@
import { vol } from 'memfs';
import { AppQuery } from '../../../../graphql/queries/AppQuery';
import { promptAsync } from '../../../../prompts';
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateGoogleServiceAccountKey } from '../CreateGoogleServiceAccountKey';
@@ -10,12 +12,17 @@ jest.mock('fs');
jest.mocked(promptAsync).mockImplementation(async () => ({
filePath: '/google-service-account-key.json',
}));
jest.mock('../../../../graphql/queries/AppQuery');
beforeEach(() => {
vol.reset();
});
describe(CreateGoogleServiceAccountKey, () => {
beforeEach(() => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
it('creates a Google Service Account Key in Interactive Mode', async () => {
vol.fromJSON({
'/google-service-account-key.json': JSON.stringify({
@@ -1,4 +1,8 @@
import { jester as mockJester } from '../../../../credentials/__tests__/fixtures-constants';
import {
jester as mockJester,
testAppQueryByIdResponse,
} from '../../../../credentials/__tests__/fixtures-constants';
import { AppQuery } from '../../../../graphql/queries/AppQuery';
import { testKeystore } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { askForUserProvidedAsync } from '../../../utils/promptForCredentials';
@@ -6,15 +10,16 @@ import { generateRandomKeystoreAsync } from '../../utils/keystore';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateKeystore } from '../CreateKeystore';
jest.mock('../../../../project/ensureProjectExists');
jest.mock('../../../../prompts', () => ({ confirmAsync: jest.fn(() => true) }));
jest.mock('../../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
jest.mock('../../../utils/promptForCredentials');
jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn() }));
jest.mock('../../../../graphql/queries/AppQuery');
describe('CreateKeystore', () => {
beforeEach(() => {
jest.mocked(generateRandomKeystoreAsync).mockReset();
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
it('creates a keystore in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
@@ -1,13 +1,19 @@
import fs from 'fs-extra';
import { AppQuery } from '../../../../graphql/queries/AppQuery';
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { DownloadKeystore } from '../DownloadKeystore';
jest.mock('fs-extra');
jest.mock('../../../../graphql/queries/AppQuery');
describe(DownloadKeystore, () => {
beforeEach(() => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
it('downloads a keystore', async () => {
const fsWriteFileSpy = jest.spyOn(fs, 'writeFile');
const ctx = createCtxMock({
@@ -1,16 +1,22 @@
import { AppQuery } from '../../../../graphql/queries/AppQuery';
import { confirmAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
testAndroidAppCredentialsFragment,
} from '../../../__tests__/fixtures-android';
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { RemoveFcm } from '../RemoveFcm';
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
jest.mock('../../../../graphql/queries/AppQuery');
describe(RemoveFcm, () => {
beforeEach(() => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
it('removes an FCM API Key', async () => {
const ctx = createCtxMock({
nonInteractive: false,
@@ -1,5 +1,7 @@
import { AppQuery } from '../../../../graphql/queries/AppQuery';
import { confirmAsync } from '../../../../prompts';
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { RemoveKeystore } from '../RemoveKeystore';
@@ -7,8 +9,13 @@ import { RemoveKeystore } from '../RemoveKeystore';
jest.mock('fs-extra');
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
jest.mock('../../../../graphql/queries/AppQuery');
describe(RemoveKeystore, () => {
beforeEach(() => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
it('removes a keystore', async () => {
const ctx = createCtxMock({
nonInteractive: false,
@@ -1,9 +1,13 @@
import { AppQuery } from '../../../../graphql/queries/AppQuery';
import {
getNewAndroidApiMock,
testAndroidBuildCredentialsFragment,
testJksAndroidKeystoreFragment,
} from '../../../__tests__/fixtures-android';
import { jester as mockJester } from '../../../__tests__/fixtures-constants';
import {
jester as mockJester,
testAppQueryByIdResponse,
} from '../../../__tests__/fixtures-constants';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { generateRandomKeystoreAsync } from '../../utils/keystore';
@@ -11,14 +15,15 @@ import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetUpBuildCredentials } from '../SetUpBuildCredentials';
jest.mock('../../../../ora');
jest.mock('../../../../project/ensureProjectExists');
jest.mock('../../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
jest.mock('../../../../prompts', () => ({ confirmAsync: jest.fn(() => true) }));
jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn() }));
jest.mock('../../../../graphql/queries/AppQuery');
describe('SetUpBuildCredentials', () => {
beforeEach(() => {
jest.mocked(generateRandomKeystoreAsync).mockReset();
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
it('skips setup when there are prior credentials', async () => {
@@ -1,12 +1,14 @@
import { vol } from 'memfs';
import { AndroidKeystoreType } from '../../../../graphql/generated';
import { AppQuery } from '../../../../graphql/queries/AppQuery';
import { confirmAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
testAndroidBuildCredentialsFragment,
testJksAndroidKeystoreFragment,
} from '../../../__tests__/fixtures-android';
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import {
SelectAndroidBuildCredentials,
@@ -21,12 +23,16 @@ jest.mock('fs');
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
jest.mock('../../../manager/SelectAndroidBuildCredentials');
jest.mock('../../../../graphql/queries/AppQuery');
beforeEach(() => {
vol.reset();
});
describe(SetUpBuildCredentialsFromCredentialsJson, () => {
beforeEach(() => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
it('sets up a new build configuration from credentials.json upon user request', async () => {
jest.mocked(SelectAndroidBuildCredentials).mockImplementation((() => {
return {
@@ -1,11 +1,13 @@
import { vol } from 'memfs';
import { AppQuery } from '../../../../graphql/queries/AppQuery';
import { promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
testAndroidAppCredentialsFragment,
testGoogleServiceAccountKeyFragment,
} from '../../../__tests__/fixtures-android';
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
@@ -16,12 +18,16 @@ jest.mock('fs');
jest.mocked(promptAsync).mockImplementation(async () => ({
filePath: '/google-service-account-key.json',
}));
jest.mock('../../../../graphql/queries/AppQuery');
beforeEach(() => {
vol.reset();
});
describe(SetUpGoogleServiceAccountKey, () => {
beforeEach(() => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
it('skips setup when there is a Google Service Account Key already assigned to the project', async () => {
const ctx = createCtxMock({
nonInteractive: false,
Loaded 100 of 229 files, more files were not shown because too many files have changed in this diff. Show more