Compare commits

..
Author SHA1 Message Date
Dominik Sokal 9b711eae77 address PR feedback 2022-11-04 12:16:22 +01:00
Dominik Sokal 2bca449793 set EAS Update default channel to the build profile name 2022-11-04 10:42:15 +01:00
Dominik Sokal 170e5145f0 update CHANGELOG.md 2022-11-03 09:15:37 +01:00
Evan Bacon 405c532f13 use version check for disabling global CLI (#1474) 2022-11-02 21:22:29 -05:00
Juwan Wheatley 5bed1c3a01 [eas-cli][update] Attach Git Commit to EAS Update (#1499)
* [eas-cli][update] Attach Git Commit to EAS Update

* update changelog

* include commit in update summary log
2022-11-02 19:06:24 -04:00
Cedric van PuttenandEvan Bacon 8d0d34c497 [eas-cli] Improve configuration setup for EAS update (#1479)
* refactor(update): auto configure EAS updates when running eas update

* fix(update): make sure eas update does what it needs to do

* refactor(update): drop obsolete eas update validation errors

* refactor(update): drop duplicate truncate update message

* refactor(update): drop duplicate install expo-update message

* fix(update): use the old truncated update message limit

* refactor(update): ensure the latest changes are integrated in the PR

* fix(update): use the EAS update configured expo config

* chore(update): make the EAS Update outage a bit more readable

* fix(update): remove _internal from merged manifest if defined

* fix(update): make sure people can run `eas update:configure` without project id

* update CHANGELOG.md

* refactor(update): skip setting the sdkVersion when configuring

* fix(update): only merge nested objects when configuring

* fix(update): refetch the config whenever projects are auto-setup

* refactor(update): simplify the config object merging

* refactor(update): change logging based on review input

Co-authored-by: Evan Bacon <[email protected]>

* refactor(update): drop fyi .md extension from logging

Co-authored-by: Evan Bacon <[email protected]>

* refactor(update): alawys reload the project config after ensuring eas updates

* fix(update): linting issue caused by logging changes

* refactor(update): drop unused exports from updates configure code

* refactor(update): split merge config to separate method for testing

* fix(update): show full install log when debugging is enabled

* fix(update): add back the missing <remove this key> instruction

Co-authored-by: Evan Bacon <[email protected]>
2022-11-02 23:18:50 +01:00
Dominik Sokal 6270bc122b [ENG-6619] validate icon PNGs before running build (#1477)
* validate icon PNGs before running build

* address PR feedback

* address PR feedback

* address more PR feedback
2022-11-02 18:05:55 +01:00
Cedric van PuttenandWill Schurman 57f1128450 [eas-cli] Extract entity utilities from update command class (#1478)
* refactor(update): remove default non-interactive value to rely on falsy values

Co-authored-by: Will Schurman <[email protected]>

* refactor(updates): make non interactive optional to rely on falsy values

* Revert "refactor(update): remove default non-interactive value to rely on falsy values"

This reverts commit 23ac36d36e35b7a87724229234d41dcac8cf02db.

* Revert "refactor(updates): make non interactive optional to rely on falsy values"

This reverts commit 94ae7cc039c94f28b74a5cfd9857f79e5a11e82c.

* refactor(update): move ensure methods outside the update command

* fix(update): add back log about channel pointing to branch

* refactor(branch): reuse query from shared utilsin `branch:create`

* refactor(channel): reuse query from shared utilsin `channel:create`

Co-authored-by: Will Schurman <[email protected]>
2022-10-31 18:47:56 +01:00
Cedric van PuttenandWill Schurman cb09906b2e [eas-cli] Add flag sanitization in updates command (#1473)
* [eas-cli] Add flag sanitization in updates command

* refactor(update): remove default non-interactive value to rely on falsy values

Co-authored-by: Will Schurman <[email protected]>

* refactor(updates): make non interactive optional to rely on falsy values

* Revert "refactor(update): remove default non-interactive value to rely on falsy values"

This reverts commit 23ac36d36e35b7a87724229234d41dcac8cf02db.

* Revert "refactor(updates): make non interactive optional to rely on falsy values"

This reverts commit 94ae7cc039c94f28b74a5cfd9857f79e5a11e82c.

Co-authored-by: Will Schurman <[email protected]>
2022-10-31 18:18:44 +01:00
Bartosz Kaszubowski fa704c43b3 [eas-cli] Update: improve DX on first run, correct error message (#1427) 2022-10-27 17:01:27 +02:00
Dominik Sokal eede0c3dd7 fix CHANGELOG.md 2022-10-27 12:07:44 +02:00
Expo CI 1758a4e5e5 update CHANGELOG.md 2022-10-27 08:06:51 +00:00
Dominik Sokal 20ebf24a20 v2.6.0 2022-10-27 10:00:11 +02:00
Juwan WheatleyandWill Schurman 427ececb0b [eas-cli] Warn about outdated build deployment when configuring EAS Update (#1467)
* [eas-cli] Warn about outdated build deployment when configuring EAS Update

* update changelog with PR number

* print warning no matter what

* Update packages/eas-cli/src/commands/update/configure.ts

Co-authored-by: Will Schurman <[email protected]>

Co-authored-by: Will Schurman <[email protected]>
2022-10-26 17:18:50 -04:00
Cedric van Putten f65670e18f [eas-cli] Drop remaining cli tables (#1481)
* refactor: Replace `branch:view` and `update:list` table with formatted fields

* refactor: Replace `channel:view` and `channel:list` table with formatted fields

* refactor: Replace `update:list --all` table with formatted fields

* refactor: drop cli-table3 dependency

* update CHANGELOG.md
2022-10-26 11:55:51 -07:00
Dominik Sokal 41a0a4554d upgrade dependencies (#1480)
* upgrade dependencies

* regenerate graphql code

* fix types

* revert axios upgrade

* fix tests

* upgrade oclif deps

* fix more tests

* update CHANGELOG.md
2022-10-26 14:09:33 +02:00
Will Schurman c1da053f8c [eas-cli] Add feature gate support (#1475) 2022-10-25 08:55:14 -07:00
Szymon Dziedzic 57da02af9d [ENG-6238][eas-cli] Add eas build:run command and implement running iOS simulator builds directly from EAS CLI (#1447)
* create eas run command

* improve run command

* downaload and install iOS sim app

* add logic to select best avaliable simulator

* add not implemented android command

* use expo cli implementation of detecting if simulator is running

* support both archive and app paths in path argument

* better validate path argument

* refactor iOS code

* assert that Xcode and xcrun are installed

* rename ios and android run files

* assert simulator app is installed

* apply suggestions from code review

* improve select sim build prompt

* hide command

* minor fixes

* apply improvements suggested in code review

* update CHANGELOG.md
2022-10-25 12:02:38 +02:00
Brent Vatne b9c505bbdc Update CODEOWNERS 2022-10-24 11:28:02 -07:00
Brent Vatne 633e6a6721 Update CODEOWNERS 2022-10-24 11:19:59 -07:00
Cedric van PuttenandDominik Sokal f7ddb4ba8f [eas-cli] Add metadata validation command with complex rules (#1416)
* refactor: update ajv, add human errors, and prepare lint command

* feature: add custom validation rules

* refactor: allow metadata context to be created without auth state

* refactor: update validation errors with new custom issues

* refactor: update existing code to use new features

* feature: add custom apple validation rules

* feature: implement validation command with json option

* chore: clean up outdated files

* refactor: update config schema type to improve human error message

* [eas-cli] Fix typo in ajv version

* [eas-cli] Use built-in context API to get project dir

* [eas-cli] Remove commented code

* [eas-cli] Load project without enforcing project id in metadata:lint

* [eas-cli] Guard against faulty config objects

* update CHANGELOG.md

* docs(cli): fix keyword length typo

* refactor(cli): use optional chaining over ternary

Co-authored-by: Dominik Sokal <[email protected]>

* refactor(cli): drop non-null assertion in config resolver

* refactor(cli): drop implicit coercion in expressions

Co-authored-by: Dominik Sokal <[email protected]>

* refactor(cli): only initialize ajv when used

* refactor(cli): simplify error logging

* refactor(cli): move coascle to expodash as filter

* fix(cli): use proper empty check on readable errors

* refactor(cli): support non interactive in metadata lint

* refactor(cli): remove metadata context from lint command

* refactor(cli): split up metadata context into different methods

* refactor(cli): use profile instead of metadata path in config resolve methods

* refactor(cli): rename context to auth and simplify usage

* refactor(cli): move json schema files to top-level imports

* refactor(cli): use predicates in expodash filter instead of filter functions

* test(cli): update config tests to use mocked profile isntead

* test(cli): fix issue in filter predicate test

* [eas-cli] Reimplement the analytics manager

Co-authored-by: Dominik Sokal <[email protected]>
2022-10-24 18:09:11 +02:00
Cedric van Putten 91eb2c467b [eas-cli] Replace branch:list table with formatted fields (#1466)
* [eas-cli] Replace channel:view table with formatted fields

* update CHANGELOG.md

* [eas-cli] Add getBranchDescription method like update group
2022-10-24 15:57:41 +02:00
Cedric van Putten f237a7e074 [eas-cli] Replace device:create input summary table with format fields (#1465)
* [eas-cli] Replace device:create input summary table with format fields

* update CHANGELOG.md

* [eas-cli] Add device class to format device
2022-10-24 15:17:57 +02:00
Cedric van PuttenandDominik Sokal 242c3cffb6 [eas-cli] Replace secret:list table with format fields (#1464)
* [eas-cli] Replace secret:list table with format fields

* update CHANGELOG.md

* [eas-cli] Replace created at with update in secrets list

Co-authored-by: Dominik Sokal <[email protected]>

* Revert updated at

Co-authored-by: Dominik Sokal <[email protected]>
2022-10-24 14:59:55 +02:00
Dominik Sokal eee54af6ee Update CHANGELOG.md 2022-10-24 12:28:44 +02:00
Expo CI 86f86dbe86 update CHANGELOG.md 2022-10-24 10:26:53 +00:00
120 changed files with 6988 additions and 3468 deletions

No files matched your search

+3
View File
@@ -1,2 +1,5 @@
* text eol=lf
*.png binary
*.jpg binary
**/yarn.lock linguist-generated
+30 -2
View File
@@ -8,12 +8,39 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Add Git commit to EAS Update record. ([#1499](https://github.com/expo/eas-cli/pull/1499) by [@fiberjw](https://github.com/fiberjw))
- Use local CLI for `expo export` in SDK +46. ([#1474](https://github.com/expo/eas-cli/pull/1474) by [@EvanBacon](https://github.com/EvanBacon))
- Validate icon PNGs before running Android build. ([#1477](https://github.com/expo/eas-cli/pull/1477) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
### 🧹 Chores
- Remove friction from the initial EAS update setup. ([#1479](https://github.com/expo/eas-cli/pull/1479) by [@byCedric](https://github.com/byCedric))
## [2.6.0](https://github.com/expo/eas-cli/releases/tag/v2.6.0) - 2022-10-27
### 🎉 New features
- Add custom metadata validation command with more complex rules. ([#1416](https://github.com/expo/eas-cli/pull/1416) by [@byCedric](https://github.com/byCedric))
- Add `eas build:run` command which runs iOS simulator builds from the CLI. ([#1447](https://github.com/expo/eas-cli/pull/1447) by [@szdziedzic](https://github.com/szdziedzic))
- Add feature gate support. ([#1475](https://github.com/expo/eas-cli/pull/1475) by [@wschurman](https://github.com/wschurman))
- Warn about outdated build deployment when configuring EAS Update. ([#1467](https://github.com/expo/eas-cli/pull/1467) by [@fiberjw](https://github.com/fiberjw))
### 🧹 Chores
- Replace `secret:list` table with formatted fields. ([#1464](https://github.com/expo/eas-cli/pull/1464) by [@byCedric](https://github.com/byCedric))
- Replace `device:create` input table with formatted fields. ([#1465](https://github.com/expo/eas-cli/pull/1465) by [@byCedric](https://github.com/byCedric))
- Replace `channel:view` table with formatted fields. ([#1466](https://github.com/expo/eas-cli/pull/1466) by [@byCedric](https://github.com/byCedric))
- Upgrade dependencies. ([#1480](https://github.com/expo/eas-cli/pull/1480) by [@dsokal](https://github.com/dsokal))
- Replace all remaining tables with formatted fields. ([#1481](https://github.com/expo/eas-cli/pull/1481) by [@byCedric](https://github.com/byCedric))
## [2.5.1](https://github.com/expo/eas-cli/releases/tag/v2.5.1) - 2022-10-24
### 🐛 Bug fixes
- Revert using local CLI for `expo export` ([#1460](https://github.com/expo/eas-cli/pull/1460)). ([#1472](https://github.com/expo/eas-cli/pull/1472) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
## [2.5.0](https://github.com/expo/eas-cli/releases/tag/v2.5.0) - 2022-10-24
### 🎉 New features
@@ -24,6 +51,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- Initialize analytics consistently and move analytics into context. ([#1444](https://github.com/expo/eas-cli/pull/1444) by [@wschurman](https://github.com/wschurman))
- - Skip using `--non-interactive` and `--experimental-bundle` flags when local Expo CLI is installed. ([#1460](https://github.com/expo/eas-cli/pull/1460) by [@EvanBacon](https://github.com/EvanBacon))
### 🧹 Chores
+2 -2
View File
@@ -6,8 +6,8 @@ packages/eas-cli/src/commands/build @dsokal @wkozyra95
packages/eas-cli/src/submit @dsokal
packages/eas-cli/src/commands/submit.ts @dsokal
packages/eas-cli/src/update @kgc00 @wschurman
packages/eas-cli/src/commands/update @kgc00 @wschurman
packages/eas-cli/src/update @EvanBacon @byCedric @wschurman
packages/eas-cli/src/commands/update @EvanBacon @byCedric @wschurman
packages/eas-cli/schema @bycedric
packages/eas-cli/src/metadata @bycedric
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "2.5.1"
"version": "2.6.0"
}
+9 -9
View File
@@ -18,19 +18,19 @@
"clean": "lerna run clean && rm -rf node_modules coverage"
},
"devDependencies": {
"@types/jest": "28.1.6",
"@types/node": "18.6.5",
"eslint": "8.21.0",
"@types/jest": "29.2.0",
"@types/node": "18.11.5",
"eslint": "8.26.0",
"eslint-config-universe": "11.1.0",
"eslint-plugin-async-protect": "2.1.0",
"eslint-plugin-node": "11.1.0",
"jest": "28.1.3",
"jest-watch-typeahead": "2.0.0",
"lerna": "4.0.0",
"oclif": "3.1.2",
"jest": "29.2.2",
"jest-watch-typeahead": "2.2.0",
"lerna": "6.0.1",
"oclif": "3.2.22",
"prettier": "2.7.1",
"ts-jest": "28.0.7",
"typescript": "4.7.4"
"ts-jest": "29.0.3",
"typescript": "4.8.4"
},
"volta": {
"node": "18.6.0",
+89 -49
View File
@@ -89,6 +89,7 @@ eas --help COMMAND
* [`eas init`](#eas-init)
* [`eas login`](#eas-login)
* [`eas logout`](#eas-logout)
* [`eas metadata:lint`](#eas-metadatalint)
* [`eas metadata:pull`](#eas-metadatapull)
* [`eas metadata:push`](#eas-metadatapush)
* [`eas open`](#eas-open)
@@ -97,6 +98,7 @@ eas --help COMMAND
* [`eas secret:create`](#eas-secretcreate)
* [`eas secret:delete`](#eas-secretdelete)
* [`eas secret:list`](#eas-secretlist)
* [`eas secret:push`](#eas-secretpush)
* [`eas submit`](#eas-submit)
* [`eas update`](#eas-update)
* [`eas update:configure`](#eas-updateconfigure)
@@ -125,7 +127,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -142,7 +144,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -159,7 +161,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -173,7 +175,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -202,7 +204,7 @@ EXAMPLES
$ eas autocomplete --refresh-cache
```
_See code: [@expo/plugin-autocomplete](https://github.com/expo/plugin-autocomplete/blob/v1.4.0/src/commands/autocomplete/index.ts)_
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v1.3.4/src/commands/autocomplete/index.ts)_
## `eas branch:create [NAME]`
@@ -223,7 +225,7 @@ DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -244,7 +246,7 @@ DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -264,7 +266,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -284,7 +286,7 @@ DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -307,7 +309,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -337,7 +339,7 @@ DESCRIPTION
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -354,7 +356,7 @@ DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -371,7 +373,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:inspect`
@@ -406,7 +408,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/v2.5.1/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -443,7 +445,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:submit`
@@ -490,7 +492,7 @@ DESCRIPTION
Update version of an app.
```
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v2.5.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.6.0/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -509,7 +511,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/v2.5.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.6.0/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -526,7 +528,7 @@ DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -547,7 +549,7 @@ DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -569,7 +571,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -589,7 +591,7 @@ DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:view [NAME]`
@@ -612,7 +614,7 @@ DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -631,7 +633,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -648,7 +650,7 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -662,7 +664,7 @@ DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -682,7 +684,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -703,7 +705,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -717,7 +719,7 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -731,7 +733,7 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -751,7 +753,7 @@ DESCRIPTION
display help for eas-cli
```
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.3.0/src/commands/help.ts)_
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.1.16/src/commands/help.ts)_
## `eas init`
@@ -803,6 +805,24 @@ ALIASES
$ eas logout
```
## `eas metadata:lint`
validate the local store configuration
```
USAGE
$ eas metadata:lint [--json] [--profile <value>]
FLAGS
--json Enable JSON output, non-JSON messages will be printed to stderr
--profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.
DESCRIPTION
validate the local store configuration
```
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
generate the local store configuration from the app stores
@@ -818,7 +838,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/v2.5.1/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -835,7 +855,7 @@ DESCRIPTION
sync the local store configuration to the app stores
```
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas open`
@@ -849,7 +869,7 @@ DESCRIPTION
open the project page in a web browser
```
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/open.ts)_
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -863,7 +883,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -885,7 +905,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/project/init.ts)_
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/project/init.ts)_
## `eas secret:create`
@@ -908,7 +928,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/v2.5.1/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -926,7 +946,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -940,7 +960,27 @@ DESCRIPTION
list environment secrets available for your current app
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/secret/list.ts)_
## `eas secret:push`
read environment secrets from env file and store on the server
```
USAGE
$ eas secret:push [--scope account|project] [--env-file <value>] [--force] [--non-interactive]
FLAGS
--env-file=<value> Env file with secrets
--force Delete and recreate existing secrets
--non-interactive Run the command in non-interactive mode.
--scope=(account|project) [default: project] Scope for the secrets
DESCRIPTION
read environment secrets from env file and store on the server
```
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/secret/push.ts)_
## `eas submit`
@@ -970,7 +1010,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1000,7 +1040,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/update/index.ts)_
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1017,7 +1057,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1038,7 +1078,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:list`
@@ -1060,7 +1100,7 @@ DESCRIPTION
view the recent updates
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/update/list.ts)_
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:view GROUPID`
@@ -1080,7 +1120,7 @@ DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/update/view.ts)_
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1101,7 +1141,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1121,7 +1161,7 @@ DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1138,7 +1178,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1160,7 +1200,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1177,7 +1217,7 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v2.5.1/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
+2
View File
@@ -5,6 +5,8 @@ documents:
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/commands/**/*.ts'
- 'src/branch/**/*.ts'
- 'src/channel/**/*.ts'
generates:
src/graphql/generated.ts:
plugins:
+516 -52
View File
@@ -2274,6 +2274,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "revokeEndpoint",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "tokenEndpoint",
"description": "",
@@ -2442,6 +2454,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "revokeEndpoint",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "tokenEndpoint",
"description": "",
@@ -2734,6 +2758,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "revokeEndpoint",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "tokenEndpoint",
"description": "",
@@ -11913,6 +11949,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "actualResourceClass",
"description": "The actual resource class of the builder assigned to the build job",
"args": [],
"type": {
"kind": "ENUM",
"name": "BuildResourceClass",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "appBuildVersion",
"description": "",
@@ -12391,12 +12439,16 @@
},
{
"name": "resourceClass",
"description": "",
"description": "The builder resource class requested by the developer",
"args": [],
"type": {
"kind": "ENUM",
"name": "BuildResourceClass",
"ofType": null
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "BuildResourceClass",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
@@ -15406,6 +15458,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "archiveSource",
"description": "",
"type": {
"kind": "INPUT_OBJECT",
"name": "SubmissionArchiveSourceInput",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "archiveUrl",
"description": "",
@@ -15641,6 +15705,22 @@
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodeIdentifier",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
@@ -15712,6 +15792,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "archiveSource",
"description": "",
"type": {
"kind": "INPUT_OBJECT",
"name": "SubmissionArchiveSourceInput",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "archiveUrl",
"description": "",
@@ -16080,6 +16172,60 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeleteIosAppBuildCredentialsResult",
"description": "",
"fields": [
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeleteIosAppCredentialsResult",
"description": "",
"fields": [
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeleteRobotResult",
@@ -17284,7 +17430,7 @@
"name": "Int",
"ofType": null
},
"defaultValue": "1",
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
@@ -17296,7 +17442,7 @@
"name": "Int",
"ofType": null
},
"defaultValue": "50",
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
@@ -17305,17 +17451,9 @@
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GitHubAppInstallationAccessibleRepository",
"ofType": null
}
}
"kind": "OBJECT",
"name": "GitHubRepositoryPaginationResult",
"ofType": null
}
},
"isDeprecated": false,
@@ -17468,6 +17606,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodeId",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "owner",
"description": "",
@@ -17716,6 +17870,39 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "installation",
"description": "",
"args": [
{
"name": "id",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GitHubAppInstallation",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "searchRepositories",
"description": "",
@@ -17736,30 +17923,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "page",
"description": "",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": "1",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "perPage",
"description": "",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": "50",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "query",
"description": "",
@@ -17781,17 +17944,9 @@
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GitHubAppInstallationAccessibleRepository",
"ofType": null
}
}
"kind": "OBJECT",
"name": "GitHubRepositoryPaginationResult",
"ofType": null
}
},
"isDeprecated": false,
@@ -17883,6 +18038,22 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodeIdentifier",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -18161,6 +18332,57 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GitHubRepositoryPaginationResult",
"description": "",
"fields": [
{
"name": "repositories",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GitHubAppInstallationAccessibleRepository",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "totalCount",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GitHubRepositorySettings",
@@ -19474,6 +19696,39 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deleteIosAppBuildCredentials",
"description": "Disassociate the build credentials from an iOS app",
"args": [
{
"name": "id",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DeleteIosAppBuildCredentialsResult",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "setDistributionCertificate",
"description": "Set the distribution certificate to be used for an iOS app",
@@ -19887,6 +20142,39 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deleteIosAppCredentials",
"description": "Delete a set of credentials for an iOS app",
"args": [
{
"name": "id",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DeleteIosAppCredentialsResult",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "setAppStoreConnectApiKeyForSubmissions",
"description": "Set the App Store Connect Api Key to be used for submitting an iOS app",
@@ -19991,6 +20279,50 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "IosAppCredentialsQuery",
"description": "",
"fields": [
{
"name": "byId",
"description": "",
"args": [
{
"name": "iosAppCredentialsId",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "IosAppCredentials",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "IosBuildType",
@@ -22844,6 +23176,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "gitCommitHash",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "message",
"description": "",
@@ -24394,6 +24738,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "iosAppCredentials",
"description": "Top-level query object for querying IosAppCredentials.",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "IosAppCredentialsQuery",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "me",
"description": "If authenticated as a typical end user, this is the appropriate top-level\nquery object",
@@ -26444,6 +26804,86 @@
],
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "SubmissionArchiveSourceInput",
"description": "",
"fields": null,
"inputFields": [
{
"name": "bucketKey",
"description": "Required if the archive source type is GCS_BUILD_APPLICATION_ARCHIVE or GCS_SUBMIT_ARCHIVE",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "SubmissionArchiveSourceType",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "url",
"description": "Required if the archive source type is URL",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "SubmissionArchiveSourceType",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "GCS_BUILD_APPLICATION_ARCHIVE",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "GCS_SUBMIT_ARCHIVE",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "URL",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SubmissionError",
@@ -27084,6 +27524,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "gitCommitHash",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "group",
"description": "",
@@ -28222,6 +28674,12 @@
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "EAS_BUILD_GCS_PROJECT_SOURCES",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "EAS_BUILD_PROJECT_SOURCES",
"description": "",
@@ -28233,6 +28691,12 @@
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "EAS_SUBMIT_GCS_APP_ARCHIVE",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
+35 -29
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "2.5.1",
"version": "2.6.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -9,51 +9,53 @@
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.34",
"@expo/code-signing-certificates": "0.0.2",
"@expo/code-signing-certificates": "0.0.3",
"@expo/config": "7.0.1",
"@expo/config-plugins": "5.0.1",
"@expo/config-types": "46.0.2",
"@expo/eas-build-job": "0.2.94",
"@expo/eas-json": "2.2.0",
"@expo/eas-build-job": "0.2.96",
"@expo/eas-json": "2.6.0",
"@expo/json-file": "8.2.36",
"@expo/multipart-body-parser": "1.1.0",
"@expo/osascript": "2.0.33",
"@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.3.0",
"@expo/plugin-warn-if-update-available": "2.3.0",
"@expo/plugin-help": "5.1.16",
"@expo/plugin-warn-if-update-available": "2.4.0",
"@expo/prebuild-config": "5.0.3",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.0",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "1.13.10",
"@urql/core": "2.6.1",
"@urql/exchange-retry": "0.3.3",
"ajv": "^6.12.6",
"@oclif/core": "1.19.2",
"@oclif/plugin-autocomplete": "1.3.4",
"@segment/ajv-human-errors": "^2.1.2",
"@urql/core": "3.0.5",
"@urql/exchange-retry": "1.0.0",
"ajv": "^8.11.0",
"ajv-formats": "2.1.1",
"better-opn": "3.0.2",
"chalk": "4.1.2",
"cli-progress": "3.11.2",
"cli-table3": "0.6.2",
"dateformat": "4.6.3",
"dotenv": "16.0.3",
"env-paths": "2.2.0",
"envinfo": "7.8.1",
"fast-deep-equal": "3.1.3",
"fast-glob": "3.2.11",
"fast-glob": "3.2.12",
"figures": "3.2.0",
"form-data": "4.0.0",
"fs-extra": "10.1.0",
"getenv": "1.0.0",
"gradle-to-js": "2.0.1",
"graphql": "16.5.0",
"graphql": "16.6.0",
"graphql-tag": "2.12.6",
"https-proxy-agent": "5.0.1",
"ignore": "5.2.0",
"indent-string": "4.0.0",
"jks-js": "1.0.2",
"joi": "17.6.0",
"jks-js": "1.1.0",
"joi": "17.6.4",
"keychain": "1.3.0",
"log-symbols": "4.1.0",
"mime": "3.0.0",
@@ -64,51 +66,55 @@
"nullthrows": "1.1.1",
"ora": "5.1.0",
"pkg-dir": "4.2.0",
"pngjs": "6.0.0",
"promise-limit": "2.7.0",
"promise-retry": "2.0.1",
"prompts": "2.4.2",
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
"semver": "7.3.7",
"semver": "7.3.8",
"slash": "3.0.0",
"tar": "6.1.11",
"terminal-link": "2.1.1",
"tslib": "2.4.0",
"turndown": "7.1.1",
"untildify": "4.0.0",
"uuid": "8.3.2",
"uuid": "9.0.0",
"wrap-ansi": "7.0.0"
},
"devDependencies": {
"@graphql-codegen/cli": "2.11.5",
"@graphql-codegen/cli": "2.13.7",
"@graphql-codegen/introspection": "2.2.1",
"@graphql-codegen/typescript": "2.7.3",
"@graphql-codegen/typescript-operations": "2.5.3",
"@graphql-codegen/typescript": "2.8.0",
"@graphql-codegen/typescript-operations": "2.5.5",
"@types/cli-progress": "3.11.0",
"@types/dateformat": "3.0.1",
"@types/envinfo": "7.8.1",
"@types/express": "4.17.14",
"@types/fs-extra": "9.0.13",
"@types/getenv": "^1.0.0",
"@types/mime": "2.0.3",
"@types/mime": "3.0.1",
"@types/node-fetch": "2.6.2",
"@types/node-forge": "1.0.4",
"@types/node-forge": "1.3.0",
"@types/pngjs": "6.0.1",
"@types/promise-retry": "1.1.3",
"@types/prompts": "2.0.14",
"@types/semver": "7.3.10",
"@types/tar": "6.1.2",
"@types/prompts": "2.4.1",
"@types/semver": "7.3.12",
"@types/tar": "6.1.3",
"@types/tough-cookie": "4.0.2",
"@types/uuid": "8.3.4",
"@types/wrap-ansi": "3.0.0",
"axios": "0.27.2",
"eslint-plugin-graphql": "4.0.0",
"express": "4.18.2",
"memfs": "3.4.7",
"mockdate": "3.0.5",
"nock": "13.2.9",
"rimraf": "3.0.2",
"ts-deepmerge": "2.0.5",
"ts-deepmerge": "4.0.0",
"ts-mockito": "2.6.1",
"ts-node": "10.9.1",
"typescript": "4.7.4"
"typescript": "4.8.4"
},
"engines": {
"node": ">=14.0.0"
@@ -130,7 +136,7 @@
"bin": "eas",
"commands": "./build/commands",
"plugins": [
"@expo/plugin-autocomplete",
"@oclif/plugin-autocomplete",
"@expo/plugin-help",
"@expo/plugin-warn-if-update-available"
],
+1 -1
View File
@@ -778,7 +778,7 @@
},
"properties": {
"configVersion": {
"const": 0,
"enum": [0],
"description": "The EAS metadata store configuration version"
},
"apple": {
+77 -29
View File
@@ -1,17 +1,24 @@
import chalk from 'chalk';
import CliTable from 'cli-table3';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { UpdateBranchFragment } from '../graphql/generated';
import { withErrorHandlingAsync } from '../graphql/client';
import {
CreateUpdateBranchForAppMutation,
CreateUpdateBranchForAppMutationVariables,
UpdateBranch,
UpdateBranchFragment,
} from '../graphql/generated';
import { BranchQuery } from '../graphql/queries/BranchQuery';
import Log from '../log';
import { UPDATE_COLUMNS, formatUpdateMessage, getPlatformsForGroup } from '../update/utils';
import { formatBranch, getBranchDescription } from '../update/utils';
import { printJsonOnlyOutput } from '../utils/json';
import {
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
import { BranchNotFoundError } from './utils';
export const BRANCHES_LIMIT = 50;
@@ -102,34 +109,75 @@ function renderPageOfBranches(
if (json) {
printJsonOnlyOutput(currentPage);
} else {
const table = new CliTable({
head: ['Branch', ...UPDATE_COLUMNS],
wordWrap: true,
});
table.push(
...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();
Log.log(chalk.bold('Branches:'));
Log.addNewLineIfNone();
Log.log(table.toString());
Log.log(
currentPage
.map(branch => formatBranch(getBranchDescription(branch)))
.join(`\n\n${chalk.dim('———')}\n\n`)
);
}
}
export async function createUpdateBranchOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, name }: CreateUpdateBranchForAppMutationVariables
): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
const result = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
gql`
mutation CreateUpdateBranchForApp($appId: ID!, $name: String!) {
updateBranch {
createUpdateBranchForApp(appId: $appId, name: $name) {
id
name
}
}
}
`,
{
appId,
name,
}
)
.toPromise()
);
const newBranch = result.updateBranch.createUpdateBranchForApp;
if (!newBranch) {
throw new Error(`Could not create branch ${name}.`);
}
return newBranch;
}
export async function ensureBranchExistsAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
branchName,
}: {
appId: string;
branchName: string;
}
): Promise<{ branchId: string }> {
try {
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId,
name: branchName,
});
const { id } = updateBranch;
return { branchId: id };
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
appId,
name: branchName,
});
return { branchId: newUpdateBranch.id };
} else {
throw error;
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,142 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { ExitError } from '@oclif/core/lib/errors';
import path from 'path';
import { instance, mock, when } from 'ts-mockito';
import { CommonContext } from '../context';
import { validatePNGsForManagedProjectAsync } from '../validate';
const fixturesPath = path.join(__dirname, 'fixtures');
describe(validatePNGsForManagedProjectAsync, () => {
it('does not validate PNGs for generic projects', async () => {
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
when(ctxMock.workflow).thenReturn(Workflow.GENERIC);
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
});
it('does not validate PNGs for projects with SDK >= 47', async () => {
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
when(ctxMock.workflow).thenReturn(Workflow.GENERIC);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
android: { adaptiveIcon: { foregroundImage: 'assets/icon.jpg' } },
sdkVersion: '47.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
});
describe(Platform.ANDROID, () => {
it('exits if foregroundImage is not a file with .png extension', async () => {
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.ANDROID);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
android: { adaptiveIcon: { foregroundImage: 'assets/icon.jpg' } },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
});
it('exits if foregroundImage is not a png file', async () => {
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.ANDROID);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
android: { adaptiveIcon: { foregroundImage: path.join(fixturesPath, 'icon-jpg.png') } },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
});
it('does not throw if foregroundImage is a png file', async () => {
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.ANDROID);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
android: { adaptiveIcon: { foregroundImage: path.join(fixturesPath, 'icon-alpha.png') } },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
});
});
// Validating iOS PNGs is currently disabled
// See https://github.com/expo/eas-cli/pull/1477 for context
xdescribe(Platform.IOS, () => {
it('exits if icon is not a file with .png extension', async () => {
const ctxMock = mock<CommonContext<Platform.IOS>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.IOS);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
ios: { icon: path.join(fixturesPath, 'assets/icon.jpg') },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
});
it('exits if icon is not a png file', async () => {
const ctxMock = mock<CommonContext<Platform.IOS>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.IOS);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
ios: { icon: path.join(fixturesPath, 'icon-jpg.png') },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
});
it('exits if icon has alpha channel (transparency)', async () => {
const ctxMock = mock<CommonContext<Platform.IOS>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.IOS);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
ios: { icon: path.join(fixturesPath, 'icon-alpha.png') },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
});
it('does not throw if icon is a png file and does not have alpha channel', async () => {
const ctxMock = mock<CommonContext<Platform.IOS>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.IOS);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
ios: { icon: path.join(fixturesPath, 'icon-no-alpha.png') },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
});
});
});
+6 -1
View File
@@ -24,7 +24,11 @@ import {
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
import { transformMetadata } from '../graphql';
import { logCredentialsSource } from '../utils/credentials';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import {
checkGoogleServicesFileAsync,
checkNodeEnvVariable,
validatePNGsForManagedProjectAsync,
} from '../validate';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
@@ -53,6 +57,7 @@ This means that it will most likely produce an AAB and you will not be able to i
checkNodeEnvVariable(ctx);
await checkGoogleServicesFileAsync(ctx);
await validatePNGsForManagedProjectAsync(ctx);
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
@@ -7,6 +7,7 @@ import { AndroidCredentials } from '../../credentials/android/AndroidCredentials
import { getUsername } from '../../project/projectUtils';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
import { resolveChannel } from '../utils/updates';
interface JobData {
projectArchive: ArchiveSource;
@@ -68,7 +69,7 @@ export async function prepareJobAsync(
...buildCredentials,
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
updates: { channel: resolveChannel(ctx) },
developmentClient: buildProfile.developmentClient,
gradleCommand: buildProfile.gradleCommand,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
+6 -1
View File
@@ -10,7 +10,11 @@ import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/ta
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommonContext, IosBuildContext } from '../context';
import { transformMetadata } from '../graphql';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import {
checkGoogleServicesFileAsync,
checkNodeEnvVariable,
validatePNGsForManagedProjectAsync,
} from '../validate';
import { ensureIosCredentialsAsync } from './credentials';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
@@ -28,6 +32,7 @@ export async function createIosContextAsync(
checkNodeEnvVariable(ctx);
await checkGoogleServicesFileAsync(ctx);
await validatePNGsForManagedProjectAsync(ctx);
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
+2 -1
View File
@@ -6,6 +6,7 @@ import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { getUsername } from '../../project/projectUtils';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
import { resolveChannel } from '../utils/updates';
interface JobData {
projectArchive: ArchiveSource;
@@ -58,7 +59,7 @@ export async function prepareJobAsync(
buildCredentials,
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
updates: { channel: resolveChannel(ctx) },
developmentClient: ctx.buildProfile.developmentClient,
simulator: ctx.buildProfile.simulator,
scheme: jobData.buildScheme,
+13 -7
View File
@@ -5,7 +5,11 @@ import fs from 'fs-extra';
import resolveFrom from 'resolve-from';
import Log from '../log';
import { getUsername, isExpoUpdatesInstalled } from '../project/projectUtils';
import {
getUsername,
isEASUpdateConfigured,
isExpoUpdatesInstalled,
} from '../project/projectUtils';
import {
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
@@ -19,6 +23,7 @@ import { getVcsClient } from '../vcs';
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
import { BuildContext } from './context';
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
import { resolveChannel } from './utils/updates';
export async function collectMetadataAsync<T extends Platform>(
ctx: BuildContext<T>
@@ -111,16 +116,17 @@ async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
if (!isExpoUpdatesInstalled(ctx.projectDir)) {
return null;
}
if (ctx.buildProfile.channel) {
return { channel: ctx.buildProfile.channel };
const maybeChannel = resolveChannel(ctx);
if (maybeChannel) {
return { channel: maybeChannel };
}
const nativeChannel = await getNativeChannelAsync(ctx);
if (isEASUpdateConfigured(ctx.projectDir, ctx.exp) && nativeChannel) {
return { channel: nativeChannel };
}
if (ctx.buildProfile.releaseChannel) {
return { releaseChannel: ctx.buildProfile.releaseChannel };
}
const channel = await getNativeChannelAsync(ctx);
if (channel) {
return { channel };
}
const releaseChannel = await getNativeReleaseChannelAsync(ctx);
return { releaseChannel };
}
+65
View File
@@ -5,6 +5,8 @@ import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { BuildFilter, BuildFragment } from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log from '../log';
import { appPlatformDisplayNames } from '../platform';
import { promptAsync } from '../prompts';
import { printJsonOnlyOutput } from '../utils/json';
import { paginatedQueryWithConfirmPromptAsync } from '../utils/queries';
import { formatGraphQLBuild } from './utils/formatBuild';
@@ -53,6 +55,69 @@ export async function listAndRenderBuildsOnAppAsync(
}
}
export async function listAndSelectBuildsOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
projectDisplayName,
filter,
queryOptions,
}: {
projectId: string;
projectDisplayName: string;
filter?: BuildFilter;
queryOptions: PaginatedQueryOptions;
}
): Promise<BuildFragment> {
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
limit: queryOptions.limit ?? BUILDS_LIMIT,
offset: queryOptions.offset,
filter,
});
if (builds.length === 0) {
throw new Error('Found no builds matching the provided criteria.');
}
const { selectedSimulatorBuild } = await promptAsync({
type: 'select',
message: `Select simulator build to run for ${projectDisplayName} app`,
name: 'selectedSimulatorBuild',
choices: builds.map(build => ({
title: `id: ${build.id}, platform: ${appPlatformDisplayNames[build.platform]}, version: ${
build.appVersion
}, build number: ${build.appBuildVersion}`,
value: build,
})),
});
return selectedSimulatorBuild;
}
export async function getLatestBuildAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
filter,
}: {
projectId: string;
filter?: BuildFilter;
}
): Promise<BuildFragment> {
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
limit: 1,
offset: 0,
filter,
});
if (builds.length === 0) {
throw new Error('Found no build matching the provided criteria.');
}
return builds[0];
}
function renderPageOfBuilds({
builds,
projectDisplayName,
@@ -0,0 +1,72 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { instance, mock, when } from 'ts-mockito';
import { isEASUpdateConfigured } from '../../../project/projectUtils';
import { BuildContext } from '../../context';
import { resolveChannel } from '../updates';
jest.mock('../../../project/projectUtils', () => ({
isEASUpdateConfigured: jest.fn(),
}));
describe(resolveChannel, () => {
it('returns "channel" from build profile if specified', () => {
jest.mocked(isEASUpdateConfigured).mockReturnValue(true);
const buildProfileMock = mock<BuildProfile<Platform>>();
when(buildProfileMock.channel).thenReturn('blahblah');
const buildProfile = instance(buildProfileMock);
const ctxMock = mock<BuildContext<Platform>>();
when(ctxMock.exp).thenReturn({
name: 'todo',
slug: 'todo',
updates: { url: 'http://sokal.dev/todo' },
});
when(ctxMock.buildProfileName).thenReturn('production');
when(ctxMock.buildProfile).thenReturn(buildProfile);
const ctx = instance(ctxMock);
expect(resolveChannel(ctx)).toBe('blahblah');
});
it('returns undefined if "channel" is not specified in build profile and EAS Update is not configured', () => {
jest.mocked(isEASUpdateConfigured).mockReturnValue(false);
const buildProfileMock = mock<BuildProfile<Platform>>();
when(buildProfileMock.channel).thenReturn(undefined);
const buildProfile = instance(buildProfileMock);
const ctxMock = mock<BuildContext<Platform>>();
when(ctxMock.exp).thenReturn({
name: 'todo',
slug: 'todo',
});
when(ctxMock.buildProfileName).thenReturn('production');
when(ctxMock.buildProfile).thenReturn(buildProfile);
const ctx = instance(ctxMock);
expect(resolveChannel(ctx)).toBe(undefined);
});
it('returns the build profile name if "channel" is not specified in build profile and EAS Update is configured', () => {
jest.mocked(isEASUpdateConfigured).mockReturnValue(true);
const buildProfileMock = mock<BuildProfile<Platform>>();
when(buildProfileMock.channel).thenReturn(undefined);
const buildProfile = instance(buildProfileMock);
const ctxMock = mock<BuildContext<Platform>>();
when(ctxMock.exp).thenReturn({
name: 'todo',
slug: 'todo',
updates: { url: 'http://sokal.dev/todo' },
});
when(ctxMock.buildProfileName).thenReturn('production');
when(ctxMock.buildProfile).thenReturn(buildProfile);
const ctx = instance(ctxMock);
expect(resolveChannel(ctx)).toBe('production');
});
});
@@ -0,0 +1,12 @@
import { Platform } from '@expo/eas-build-job';
import { isEASUpdateConfigured } from '../../project/projectUtils';
import { BuildContext } from '../context';
export function resolveChannel(ctx: BuildContext<Platform>): string | undefined {
let fallback: string | undefined = undefined;
if (isEASUpdateConfigured(ctx.projectDir, ctx.exp)) {
fallback = ctx.buildProfileName;
}
return ctx.buildProfile.channel ?? fallback;
}
+137
View File
@@ -1,8 +1,11 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { Errors } from '@oclif/core';
import fs from 'fs-extra';
import path from 'path';
import semver from 'semver';
import Log, { learnMore } from '../log';
import { isPNGAsync } from '../utils/image';
import { getVcsClient } from '../vcs';
import { CommonContext } from './context';
@@ -50,3 +53,137 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
function isInsideDirectory(file: string, directory: string): boolean {
return file.startsWith(directory);
}
export async function validatePNGsForManagedProjectAsync<T extends Platform>(
ctx: CommonContext<T>
): Promise<void> {
if (ctx.workflow !== Workflow.MANAGED) {
return;
}
// don't run PNG checks on SDK 47 and newer
// see https://github.com/expo/eas-cli/pull/1477#issuecomment-1293914917
if (!ctx.exp.sdkVersion || semver.satisfies(ctx.exp.sdkVersion, '>= 47.0.0')) {
return;
}
if (ctx.platform === Platform.ANDROID) {
await validateAndroidPNGsAsync(ctx as CommonContext<Platform.ANDROID>);
}
// Validating iOS PNGs is currently disabled
// See https://github.com/expo/eas-cli/pull/1477 for context
//
// else {
// await validateIosPNGsAsync(ctx as CommonContext<Platform.IOS>);
// }
}
type ConfigPng = { configPath: string; pngPath: string | undefined };
async function validateAndroidPNGsAsync(ctx: CommonContext<Platform.ANDROID>): Promise<void> {
const pngs: ConfigPng[] = [
{
configPath: 'exp.icon',
pngPath: ctx.exp.icon,
},
{
configPath: 'exp.android.icon',
pngPath: ctx.exp.android?.icon,
},
{
configPath: 'exp.android.adaptiveIcon.foregroundImage',
pngPath: ctx.exp.android?.adaptiveIcon?.foregroundImage,
},
{
configPath: 'exp.android.adaptiveIcon.backgroundImage',
pngPath: ctx.exp.android?.adaptiveIcon?.backgroundImage,
},
{
configPath: 'exp.splash.image',
pngPath: ctx.exp.splash?.image,
},
{
configPath: 'exp.notification.icon',
pngPath: ctx.exp.notification?.icon,
},
];
await validatePNGsAsync(pngs);
}
// Validating iOS PNGs is currently disabled
// See https://github.com/expo/eas-cli/pull/1477 for context
//
// async function validateIosPNGsAsync(ctx: CommonContext<Platform.IOS>): Promise<void> {
// const pngs: ConfigPng[] = [
// {
// configPath: 'exp.icon',
// pngPath: ctx.exp.icon,
// },
// {
// configPath: 'exp.ios.icon',
// pngPath: ctx.exp.ios?.icon,
// },
// {
// configPath: 'exp.splash.image',
// pngPath: ctx.exp.splash?.image,
// },
// {
// configPath: 'exp.notification.icon',
// pngPath: ctx.exp.notification?.icon,
// },
// ];
// await validatePNGsAsync(pngs);
// const icon = ctx.exp.ios?.icon ?? ctx.exp.icon;
// if (!icon) {
// return;
// }
// const iconConfigPath = `expo${ctx.exp.ios?.icon ? '.ios' : ''}.icon`;
// try {
// await ensurePNGIsNotTransparentAsync(icon);
// } catch (err: any) {
// if (err instanceof ImageTransparencyError) {
// Log.error(
// `Your iOS app icon (${iconConfigPath}) can't have transparency if you wish to upload your app to the Apple App Store.`
// );
// Log.error(learnMore('https://expo.fyi/remove-alpha-channel', { dim: false }));
// Errors.exit(1);
// } else {
// throw err;
// }
// }
// }
async function validatePNGsAsync(configPngs: ConfigPng[]): Promise<void> {
const validationPromises = configPngs.map(configPng => validatePNGAsync(configPng));
const validationResults = await Promise.allSettled(validationPromises);
const failedValidations = validationResults.filter(
(result): result is PromiseRejectedResult => result.status === 'rejected'
);
if (failedValidations.length === 0) {
return;
}
Log.error('PNG images validation failed:');
for (const { reason } of failedValidations) {
const error: Error = reason;
Log.error(`- ${error.message}`);
}
Errors.exit(1);
}
async function validatePNGAsync({ configPath, pngPath }: ConfigPng): Promise<void> {
if (!pngPath) {
return;
}
if (!pngPath.endsWith('.png')) {
throw new Error(`"${configPath}" is not a PNG file`);
}
if (!(await isPNGAsync(pngPath))) {
throw new Error(`"${configPath}" is not valid PNG`);
}
}
+71 -12
View File
@@ -1,8 +1,12 @@
import chalk from 'chalk';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { withErrorHandlingAsync } from '../graphql/client';
import {
CreateUpdateChannelOnAppMutation,
CreateUpdateChannelOnAppMutationVariables,
ViewBranchesOnUpdateChannelQueryVariables,
ViewUpdateChannelsOnAppQueryVariables,
} from '../graphql/generated';
@@ -153,14 +157,13 @@ function renderPageOfChannels(
printJsonOnlyOutput({ currentPage });
} else {
for (const channel of currentPage) {
renderChannelHeaderContent({ channelName: channel.name, channelId: channel.id });
Log.addNewLineIfNone();
Log.log(
formatFields([
{ label: 'Name', value: channel.name },
{ label: 'ID', value: channel.id },
])
);
logChannelDetails(channel);
if (currentPage.indexOf(channel) < currentPage.length - 1) {
Log.log(`\n${chalk.dim('———')}\n`);
}
}
}
}
@@ -174,13 +177,8 @@ function renderPageOfBranchesOnChannel(
if (json) {
printJsonOnlyOutput({ currentPage: channelWithNewBranches });
} else {
// The channel details contain both the branch and latest update group
Log.addNewLineIfNone();
Log.log(
formatFields([
{ label: 'Name', value: channel.name },
{ label: 'ID', value: channel.id },
])
);
logChannelDetails(channelWithNewBranches);
}
}
@@ -203,3 +201,64 @@ function renderChannelHeaderContent({
Log.addNewLineIfNone();
Log.log(chalk`{bold Branches pointed at this channel and their most recent update group:}`);
}
export async function createChannelOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
branchId,
channelName,
}: {
appId: string;
branchId: string;
channelName: string;
}
): Promise<CreateUpdateChannelOnAppMutation> {
// Point the new channel at a branch with its same name.
const branchMapping = JSON.stringify({
data: [{ branchId, branchMappingLogic: 'true' }],
version: 0,
});
return await withErrorHandlingAsync(
graphqlClient
.mutation<CreateUpdateChannelOnAppMutation, CreateUpdateChannelOnAppMutationVariables>(
gql`
mutation CreateUpdateChannelOnApp($appId: ID!, $name: String!, $branchMapping: String!) {
updateChannel {
createUpdateChannelForApp(appId: $appId, name: $name, branchMapping: $branchMapping) {
id
name
branchMapping
}
}
}
`,
{
appId,
name: channelName,
branchMapping,
}
)
.toPromise()
);
}
export async function ensureChannelExistsAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, branchId, channelName }: { appId: string; branchId: string; channelName: string }
): Promise<void> {
try {
await createChannelOnAppAsync(graphqlClient, {
appId,
channelName,
branchId,
});
} catch (e: any) {
const isIgnorableError =
e.graphQLErrors?.length === 1 &&
e.graphQLErrors[0].extensions.errorCode === 'CHANNEL_ALREADY_EXISTS';
if (!isIgnorableError) {
throw e;
}
}
}
+31 -26
View File
@@ -1,9 +1,13 @@
import assert from 'assert';
import CliTable from 'cli-table3';
import chalk from 'chalk';
import { UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import Log from '../log';
import { UPDATE_COLUMNS, getUpdateGroupDescriptionsWithBranch } from '../update/utils';
import {
FormattedBranchDescription,
formatBranch,
getUpdateGroupDescriptionsWithBranch,
} from '../update/utils';
export type BranchMapping = {
version: number;
@@ -75,38 +79,39 @@ export function getBranchMapping(branchMappingString?: string): {
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 branchDescription = channel.updateBranches.flatMap(branch => {
const updateGroupWithBranchDescriptions = getUpdateGroupDescriptionsWithBranch(
branch.updateGroups
);
const isRolloutBranch = isRollout && rolloutBranchIds.includes(currentBranch.id);
const isBaseBranch = rolloutBranchIds.length > 0 && rolloutBranchIds[0] === currentBranch.id;
const isRolloutBranch = isRollout && rolloutBranchIds.includes(branch.id);
const isBaseBranch = rolloutBranchIds.length > 0 && rolloutBranchIds[0] === branch.id;
let rolloutPercentNumber: number | undefined = undefined;
if (isRolloutBranch) {
rolloutPercentNumber = isBaseBranch ? rolloutPercent! * 100 : (1 - rolloutPercent!) * 100;
}
updateGroupDescriptions.forEach(({ branch, message, runtimeVersion, group, platforms }) => {
updateChannelsTable.push([
return updateGroupWithBranchDescriptions.map(
({ branch, ...updateGroup }): FormattedBranchDescription => ({
branch,
...(isRolloutBranch
? [isBaseBranch ? `${rolloutPercent! * 100}%` : `${(1 - rolloutPercent!) * 100}%`]
: []),
message,
runtimeVersion,
group,
platforms,
]);
});
branchRolloutPercentage: rolloutPercentNumber,
update: updateGroup,
})
);
});
if (branchDescription.length === 0) {
Log.log(chalk.dim('No branches are pointed to this channel.'));
} else {
Log.log(
branchDescription
.map(description => formatBranch(description))
.join(`\n\n${chalk.dim('———')}\n\n`)
);
}
Log.log(updateChannelsTable.toString());
}
@@ -1,20 +1,30 @@
import { Actor } from '../../user/User';
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
import FeatureGating from '../gating/FeatureGating';
import ContextField, { ContextOptions } from './ContextField';
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
export default class LoggedInContextField extends ContextField<{
actor: Actor;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
}> {
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<{
actor: Actor;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
}> {
const { actor, authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const featureGateServerValues: { [key: string]: boolean } = actor?.featureGates ?? {};
const graphqlClient = createGraphqlClient(authenticationInfo);
return { actor, graphqlClient };
return {
actor,
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
graphqlClient,
};
}
}
@@ -1,13 +1,17 @@
import { Actor } from '../../user/User';
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
import FeatureGating from '../gating/FeatureGating';
import ContextField, { ContextOptions } from './ContextField';
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
export default class MaybeLoggedInContextField extends ContextField<{
actor: Actor | null;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
}> {
async getValueAsync({ sessionManager }: ContextOptions): Promise<{
actor: Actor | null;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
}> {
const authenticationInfo = {
@@ -17,7 +21,12 @@ export default class MaybeLoggedInContextField extends ContextField<{
const graphqlClient = createGraphqlClient(authenticationInfo);
const actor = await sessionManager.getUserAsync();
const featureGateServerValues: { [key: string]: boolean } = actor?.featureGates ?? {};
return { actor: actor ?? null, graphqlClient };
return {
actor: actor ?? null,
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
graphqlClient,
};
}
}
@@ -51,6 +51,7 @@ describe(getProjectIdAsync, () => {
},
],
isExpoAdmin: false,
featureGates: {},
},
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
});
@@ -1,4 +1,5 @@
import {
AnyVariables,
Client,
OperationContext,
OperationResult,
@@ -17,9 +18,9 @@ import { getExpoApiBaseUrl } from '../../../api';
import { httpsProxyAgent } from '../../../fetch';
export interface ExpoGraphqlClient extends Client {
query<Data = any, Variables extends object = object>(
query<Data = any, Variables extends AnyVariables = AnyVariables>(
query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
variables: Variables | undefined,
variables: Variables,
context: Partial<OperationContext> & { additionalTypenames: string[] }
): PromisifiedSource<OperationResult<Data, Variables>>;
}
@@ -0,0 +1,40 @@
import nullthrows from 'nullthrows';
import env from '../../env';
import { FeatureGateKey } from './FeatureGateKey';
function parseFeatureGateEnvVariableValue(value: string | undefined): string[] {
if (!value) {
return [];
}
return value.split(',').map(v => v.trim());
}
export default class FeatureGateEnvOverrides {
private readonly map = new Map<string, boolean>();
constructor() {
const overrideEnableGateKeys = new Set(parseFeatureGateEnvVariableValue(env.featureGateEnable));
const overrideDisableGateKeys = new Set(
parseFeatureGateEnvVariableValue(env.featureGateDisable)
);
for (const overrideEnableKey of overrideEnableGateKeys) {
if (overrideDisableGateKeys.has(overrideEnableKey)) {
continue;
}
this.map.set(overrideEnableKey, true);
}
for (const overrideDisableGateKey of overrideDisableGateKeys) {
this.map.set(overrideDisableGateKey, false);
}
}
public isOverridden(key: FeatureGateKey): boolean {
return this.map.has(key) ?? false;
}
public getOverride(key: FeatureGateKey): boolean {
return nullthrows(this.map.get(key));
}
}
@@ -0,0 +1,8 @@
export enum FeatureGateKey {
// for tests
TEST = 'test',
}
export const featureGateDefaultValueWhenNoServerValue: Record<FeatureGateKey, boolean> = {
[FeatureGateKey.TEST]: true,
};
@@ -0,0 +1,15 @@
import { FeatureGateKey } from './FeatureGateKey';
export const setOverride = (_key: FeatureGateKey, _enabled: boolean): void => {
throw new Error('Must use mocked FeatureGateTestOverrides');
};
export const removeOverride = (_key: FeatureGateKey): void => {
throw new Error('Must use mocked FeatureGateTestOverrides');
};
export const isOverridden = (_key: FeatureGateKey): boolean => false;
export const getOverride = (_key: FeatureGateKey): boolean => {
throw new Error('Must use mocked FeatureGateTestOverrides');
};
@@ -0,0 +1,69 @@
import FeatureGateEnvOverrides from './FeatureGateEnvOverrides';
import { FeatureGateKey, featureGateDefaultValueWhenNoServerValue } from './FeatureGateKey';
import * as FeatureGateTestOverrides from './FeatureGateTestOverrides';
export default class FeatureGating {
constructor(
private readonly serverValues: { [key: string]: boolean },
private readonly envOverrides: FeatureGateEnvOverrides
) {}
public isEnabled(experimentKey: FeatureGateKey): boolean {
if (process.env.NODE_ENV === 'test' && FeatureGateTestOverrides.isOverridden(experimentKey)) {
return FeatureGateTestOverrides.getOverride(experimentKey);
}
if (this.envOverrides.isOverridden(experimentKey)) {
return this.envOverrides.getOverride(experimentKey);
}
if (experimentKey in this.serverValues) {
return this.serverValues[experimentKey];
}
return featureGateDefaultValueWhenNoServerValue[experimentKey];
}
/**
* Test gate override methods
*/
public static overrideKeyForScope(
key: FeatureGateKey,
enabled: boolean,
scope: () => void
): void {
if (process.env.NODE_ENV !== 'test') {
throw new Error(`Cannot overrideKeyForScope outside of test environment`);
}
FeatureGateTestOverrides.setOverride(key, enabled);
try {
scope();
} finally {
FeatureGateTestOverrides.removeOverride(key);
}
}
public static async overrideKeyForScopeAsync(
key: FeatureGateKey,
enabled: boolean,
scope: () => Promise<void>
): Promise<void> {
if (process.env.NODE_ENV !== 'test') {
throw new Error(`Cannot overrideKeyForScopeAsync outside of test environment`);
}
FeatureGateTestOverrides.setOverride(key, enabled);
try {
await scope();
} finally {
FeatureGateTestOverrides.removeOverride(key);
}
}
public static overrideKeyForEachInTest(key: FeatureGateKey, enabled: boolean): void {
beforeEach(() => FeatureGateTestOverrides.setOverride(key, enabled));
afterEach(() => FeatureGateTestOverrides.removeOverride(key));
}
}
@@ -0,0 +1,20 @@
import { FeatureGateKey } from '../FeatureGateKey';
const map = new Map();
export const setOverride = (key: FeatureGateKey, enabled: boolean): void => {
map.set(key, enabled);
};
export const removeOverride = (key: FeatureGateKey): void => {
map.delete(key);
};
export const isOverridden = (key: FeatureGateKey): boolean => map.has(key);
export const getOverride = (key: FeatureGateKey): boolean => {
if (map.has(key)) {
return map.get(key);
}
throw new Error(`Key ${key} not overridden`);
};
@@ -0,0 +1,92 @@
import env from '../../../env';
import FeatureGateEnvOverrides from '../FeatureGateEnvOverrides';
import { FeatureGateKey } from '../FeatureGateKey';
import FeatureGating from '../FeatureGating';
jest.mock('../FeatureGateTestOverrides');
describe(FeatureGating, () => {
it('supports scoped overriding', () => {
const featureGating = new FeatureGating({}, new FeatureGateEnvOverrides());
FeatureGating.overrideKeyForScope(FeatureGateKey.TEST, false, () => {
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(false);
});
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
});
it('supports async scoped overriding', async () => {
const featureGating = new FeatureGating({}, new FeatureGateEnvOverrides());
await FeatureGating.overrideKeyForScopeAsync(FeatureGateKey.TEST, false, async () => {
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(false);
});
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
});
it('uses value from the server, falls back to defined value', () => {
const featureGating = new FeatureGating(
{ [FeatureGateKey.TEST]: true },
new FeatureGateEnvOverrides()
);
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
const featureGating2 = new FeatureGating(
{ [FeatureGateKey.TEST]: false },
new FeatureGateEnvOverrides()
);
expect(featureGating2.isEnabled(FeatureGateKey.TEST)).toBe(false);
const featureGating3 = new FeatureGating({}, new FeatureGateEnvOverrides());
expect(featureGating3.isEnabled(FeatureGateKey.TEST)).toBe(true);
});
describe('environment variable overriding', () => {
afterAll(() => {
env.featureGateEnable = undefined;
env.featureGateDisable = undefined;
});
it('supports env overrides affirmative', () => {
env.featureGateEnable = [FeatureGateKey.TEST].join(',');
env.featureGateDisable = [].join(',');
const featureGating = new FeatureGating(
{ [FeatureGateKey.TEST]: false },
new FeatureGateEnvOverrides()
);
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
});
it('supports env overrides negative', () => {
env.featureGateEnable = [].join(',');
env.featureGateDisable = [FeatureGateKey.TEST].join(',');
const featureGating = new FeatureGating(
{ [FeatureGateKey.TEST]: true },
new FeatureGateEnvOverrides()
);
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(false);
});
});
});
describe('jest scoped overriding', () => {
FeatureGating.overrideKeyForEachInTest(FeatureGateKey.TEST, false);
it('supports jest scoped overriding', () => {
const featureGating = new FeatureGating(
{ [FeatureGateKey.TEST]: true },
new FeatureGateEnvOverrides()
);
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(false);
});
});
describe('removal of jest scoped overriding', () => {
it('jest scoped overriding is only for describe scope above', () => {
const featureGating = new FeatureGating(
{ [FeatureGateKey.TEST]: true },
new FeatureGateEnvOverrides()
);
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
});
});
@@ -1,4 +1,5 @@
import { Flags } from '@oclif/core';
import { OptionFlag } from '@oclif/core/lib/interfaces';
import { EasNonInteractiveAndJsonFlags } from './flags';
@@ -37,7 +38,7 @@ export const getLimitFlagWithCustomValues = ({
}: {
defaultTo: number;
limit: number;
}): any =>
}): OptionFlag<number | undefined> =>
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
+1 -39
View File
@@ -1,52 +1,14 @@
import chalk from 'chalk';
import gql from 'graphql-tag';
import { createUpdateBranchOnAppAsync } from '../../branch/queries';
import { getDefaultBranchNameAsync } from '../../branch/utils';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { withErrorHandlingAsync } from '../../graphql/client';
import {
CreateUpdateBranchForAppMutation,
CreateUpdateBranchForAppMutationVariables,
UpdateBranch,
} from '../../graphql/generated';
import Log from '../../log';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export async function createUpdateBranchOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, name }: CreateUpdateBranchForAppMutationVariables
): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
const result = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
gql`
mutation createUpdateBranchForApp($appId: ID!, $name: String!) {
updateBranch {
createUpdateBranchForApp(appId: $appId, name: $name) {
id
name
}
}
}
`,
{
appId,
name,
}
)
.toPromise()
);
const newBranch = result.updateBranch.createUpdateBranchForApp;
if (!newBranch) {
throw new Error(`Could not create branch ${name}.`);
}
return newBranch;
}
export default class BranchCreate extends EasCommand {
static override description = 'create a branch';
+4 -20
View File
@@ -9,13 +9,10 @@ import {
getLimitFlagWithCustomValues,
getPaginatedQueryOptions,
} from '../../commandUtils/pagination';
import {
AppPlatform,
DistributionType,
BuildStatus as GraphQLBuildStatus,
} from '../../graphql/generated';
import { AppPlatform, BuildStatus as GraphQLBuildStatus } from '../../graphql/generated';
import { RequestedPlatform } from '../../platform';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { buildDistributionTypeToGraphQLDistributionType } from '../../utils/buildDistribution';
import { enableJsonOutput } from '../../utils/json';
export default class BuildList extends EasCommand {
@@ -82,7 +79,8 @@ export default class BuildList extends EasCommand {
const platform = toAppPlatform(requestedPlatform);
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution);
const graphqlBuildDistribution =
buildDistributionTypeToGraphQLDistributionType(buildDistribution);
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
await listAndRenderBuildsOnAppAsync(graphqlClient, {
@@ -133,17 +131,3 @@ const toGraphQLBuildStatus = (buildStatus?: BuildStatus): GraphQLBuildStatus | u
return GraphQLBuildStatus.Canceled;
}
};
const toGraphQLBuildDistribution = (
buildDistribution?: BuildDistributionType
): DistributionType | undefined => {
if (buildDistribution === BuildDistributionType.STORE) {
return DistributionType.Store;
} else if (buildDistribution === BuildDistributionType.INTERNAL) {
return DistributionType.Internal;
} else if (buildDistribution === BuildDistributionType.SIMULATOR) {
return DistributionType.Simulator;
} else {
return undefined;
}
};
+213
View File
@@ -0,0 +1,213 @@
import { Errors, Flags } from '@oclif/core';
import { existsSync } from 'fs-extra';
import assert from 'node:assert';
import { getLatestBuildAsync, listAndSelectBuildsOnAppAsync } from '../../build/queries';
import { BuildDistributionType } from '../../build/types';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
EasPaginatedQueryFlags,
PaginatedQueryOptions,
getPaginatedQueryOptions,
} from '../../commandUtils/pagination';
import { AppPlatform, BuildFragment, BuildStatus } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { RunArchiveFlags, runAsync } from '../../run/run';
import { buildDistributionTypeToGraphQLDistributionType } from '../../utils/buildDistribution';
import { downloadAndExtractAppAsync, extractAppFromLocalArchiveAsync } from '../../utils/download';
interface RawRunFlags {
latest?: boolean;
id?: string;
url?: string;
path?: string;
platform?: string;
limit?: number;
offset?: number;
}
interface RunCommandFlags {
selectedPlatform: AppPlatform;
runArchiveFlags: RunArchiveFlags;
limit?: number;
offset?: number;
}
export default class Run extends EasCommand {
static override hidden = true;
static override description = 'run simulator/emulator builds from eas-cli';
static override flags = {
latest: Flags.boolean({
description: 'Run the latest simulator/emulator build for specified platform',
exclusive: ['id', 'path', 'url'],
}),
url: Flags.string({
description: 'Simulator/Emulator build archive url',
exclusive: ['latest', 'id', 'path'],
}),
path: Flags.string({
description: 'Path to the simulator/emulator build archive or simulator build app',
exclusive: ['latest', 'id', 'url'],
}),
id: Flags.string({
description: 'ID of the simulator/emulator build to run',
exclusive: ['latest, path, url'],
}),
platform: Flags.enum({
char: 'p',
options: ['android', 'ios'],
}),
...EasPaginatedQueryFlags,
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.ProjectDir,
};
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(Run);
const flags = await this.sanitizeFlagsAsync(rawFlags);
const queryOptions = getPaginatedQueryOptions(flags);
const {
loggedIn: { graphqlClient },
projectConfig: { projectId },
} = await this.getContextAsync(Run, {
nonInteractive: false,
});
const simulatorBuildPath = await getPathToSimulatorBuildAppAsync(
graphqlClient,
projectId,
flags,
queryOptions
);
await runAsync(simulatorBuildPath, flags.selectedPlatform);
}
private async sanitizeFlagsAsync(flags: RawRunFlags): Promise<RunCommandFlags> {
const { platform, limit, offset, ...runArchiveFlags } = flags;
const selectedPlatform = await resolvePlatformAsync(platform);
if (
runArchiveFlags.path &&
!(
(runArchiveFlags.path.endsWith('.tar.gz') ||
runArchiveFlags.path.endsWith('.app') ||
runArchiveFlags.path.endsWith('.apk')) &&
existsSync(runArchiveFlags.path)
)
) {
Errors.error('The path must point to a .tar.gz archive, .apk file, or .app directory', {
exit: 1,
});
}
return {
selectedPlatform,
runArchiveFlags,
limit,
offset,
};
}
}
async function resolvePlatformAsync(platform?: string): Promise<AppPlatform> {
if (platform && Object.values(AppPlatform).includes(platform.toUpperCase() as AppPlatform)) {
return platform.toUpperCase() as AppPlatform;
}
const { selectedPlatform } = await promptAsync({
type: 'select',
message: 'Select platform',
name: 'selectedPlatform',
choices: [
{ title: 'Android', value: AppPlatform.Android },
{ title: 'iOS', value: AppPlatform.Ios },
],
});
return selectedPlatform;
}
async function maybeGetBuildAsync(
graphqlClient: ExpoGraphqlClient,
flags: RunCommandFlags,
projectId: string,
paginatedQueryOptions: PaginatedQueryOptions
): Promise<BuildFragment | null> {
if (flags.runArchiveFlags.id) {
return BuildQuery.byIdAsync(graphqlClient, flags.runArchiveFlags.id);
} else if (
!flags.runArchiveFlags.id &&
!flags.runArchiveFlags.path &&
!flags.runArchiveFlags.url &&
!flags.runArchiveFlags.latest
) {
return await listAndSelectBuildsOnAppAsync(graphqlClient, {
projectId,
projectDisplayName: await getDisplayNameForProjectIdAsync(graphqlClient, projectId),
filter: {
platform: flags.selectedPlatform,
distribution: buildDistributionTypeToGraphQLDistributionType(
BuildDistributionType.SIMULATOR
),
status: BuildStatus.Finished,
},
queryOptions: paginatedQueryOptions,
});
} else if (flags.runArchiveFlags.latest) {
return await getLatestBuildAsync(graphqlClient, {
projectId,
filter: {
platform: flags.selectedPlatform,
distribution: buildDistributionTypeToGraphQLDistributionType(
BuildDistributionType.SIMULATOR
),
status: BuildStatus.Finished,
},
});
} else {
return null;
}
}
async function getPathToSimulatorBuildAppAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
flags: RunCommandFlags,
queryOptions: PaginatedQueryOptions
): Promise<string> {
const maybeBuild = await maybeGetBuildAsync(graphqlClient, flags, projectId, queryOptions);
const appExtension = flags.selectedPlatform === AppPlatform.Ios ? 'app' : 'apk';
if (maybeBuild) {
if (!maybeBuild.artifacts?.applicationArchiveUrl) {
throw new Error('Build does not have an application archive url');
}
return await downloadAndExtractAppAsync(
maybeBuild.artifacts.applicationArchiveUrl,
appExtension
);
}
if (flags.runArchiveFlags.url) {
return await downloadAndExtractAppAsync(flags.runArchiveFlags.url, appExtension);
}
if (flags.runArchiveFlags.path?.endsWith('.tar.gz')) {
return await extractAppFromLocalArchiveAsync(flags.runArchiveFlags.path!, appExtension);
}
// this should never fail, due to the validation in sanitizeFlagsAsync
assert(flags.runArchiveFlags.path);
return flags.runArchiveFlags.path;
}
@@ -1,63 +1,16 @@
import chalk from 'chalk';
import gql from 'graphql-tag';
import { createUpdateBranchOnAppAsync } from '../../branch/queries';
import { BranchNotFoundError } from '../../branch/utils';
import { createChannelOnAppAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { withErrorHandlingAsync } from '../../graphql/client';
import {
CreateUpdateChannelOnAppMutation,
CreateUpdateChannelOnAppMutationVariables,
} from '../../graphql/generated';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import Log from '../../log';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { createUpdateBranchOnAppAsync } from '../branch/create';
export async function createUpdateChannelOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
channelName,
branchId,
}: {
appId: string;
channelName: string;
branchId: string;
}
): Promise<CreateUpdateChannelOnAppMutation> {
// Point the new channel at a branch with its same name.
const branchMapping = JSON.stringify({
data: [{ branchId, branchMappingLogic: 'true' }],
version: 0,
});
return await withErrorHandlingAsync(
graphqlClient
.mutation<CreateUpdateChannelOnAppMutation, CreateUpdateChannelOnAppMutationVariables>(
gql`
mutation CreateUpdateChannelOnApp($appId: ID!, $name: String!, $branchMapping: String!) {
updateChannel {
createUpdateChannelForApp(appId: $appId, name: $name, branchMapping: $branchMapping) {
id
name
branchMapping
}
}
}
`,
{
appId,
name: channelName,
branchMapping,
}
)
.toPromise()
);
}
export default class ChannelCreate extends EasCommand {
static override description = 'create a channel';
@@ -132,7 +85,7 @@ export default class ChannelCreate extends EasCommand {
const {
updateChannel: { createUpdateChannelForApp: newChannel },
} = await createUpdateChannelOnAppAsync(graphqlClient, {
} = await createChannelOnAppAsync(graphqlClient, {
appId: projectId,
channelName,
branchId,
@@ -0,0 +1,79 @@
import { Platform } from '@expo/eas-build-job';
import { EasJsonAccessor } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import Log from '../../log';
import { loadConfigAsync } from '../../metadata/config/resolve';
import { MetadataValidationError, logMetadataValidationError } from '../../metadata/errors';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { getProfilesAsync } from '../../utils/profiles';
export default class MetadataLint extends EasCommand {
static override description = 'validate the local store configuration';
static override flags = {
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
default: false,
}),
profile: Flags.string({
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
};
static override contextDefinition = {
// The metadata lint command is created to integrate in other dev tooling, like vscode-expo.
// These integrations might spam this command, so we avoid communicating with our services here.
// Note that this is an exception and you should normally use `ProjectConfig` instead.
...this.ContextOptions.ProjectDir,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(MetadataLint);
const { projectDir } = await this.getContextAsync(MetadataLint, {
nonInteractive: true,
});
if (flags.json) {
enableJsonOutput();
} else {
Log.warn('EAS Metadata is in beta and subject to breaking changes.');
}
const submitProfiles = await getProfilesAsync({
type: 'submit',
easJsonAccessor: new EasJsonAccessor(projectDir),
platforms: [Platform.IOS],
profileName: flags.profile,
});
if (submitProfiles.length !== 1) {
throw new Error('Metadata only supports iOS and a single submit profile.');
}
const submitProfile = submitProfiles[0].profile;
try {
await loadConfigAsync({ projectDir, profile: submitProfile });
if (flags.json) {
return printJsonOnlyOutput([]);
}
Log.log('✅ Store configuration is valid.');
} catch (error) {
if (!(error instanceof MetadataValidationError)) {
throw error;
}
if (flags.json) {
return printJsonOnlyOutput(error.errors);
}
logMetadataValidationError(error);
Log.addNewLineIfNone();
}
}
}
+22 -10
View File
@@ -1,3 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { EasJsonAccessor } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import path from 'path';
@@ -6,9 +8,9 @@ import { ensureProjectConfiguredAsync } from '../../build/configure';
import EasCommand from '../../commandUtils/EasCommand';
import { CredentialsContext } from '../../credentials/context';
import Log, { learnMore } from '../../log';
import { createMetadataContextAsync } from '../../metadata/context';
import { downloadMetadataAsync } from '../../metadata/download';
import { handleMetadataError } from '../../metadata/errors';
import { getProfilesAsync } from '../../utils/profiles';
export default class MetadataPull extends EasCommand {
static override description = 'generate the local store configuration from the app stores';
@@ -42,6 +44,18 @@ export default class MetadataPull extends EasCommand {
// this command is interactive (all nonInteractive flags passed to utility functions are false)
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
const submitProfiles = await getProfilesAsync({
type: 'submit',
easJsonAccessor: new EasJsonAccessor(projectDir),
platforms: [Platform.IOS],
profileName: flags.profile,
});
if (submitProfiles.length !== 1) {
throw new Error('Metadata only supports iOS and a single submit profile.');
}
const submitProfile = submitProfiles[0].profile;
const credentialsCtx = new CredentialsContext({
projectInfo: { exp, projectId },
projectDir,
@@ -51,16 +65,14 @@ export default class MetadataPull extends EasCommand {
nonInteractive: false,
});
const metadataCtx = await createMetadataContextAsync({
credentialsCtx,
analytics,
projectDir,
exp,
profileName: flags.profile,
});
try {
const filePath = await downloadMetadataAsync(metadataCtx);
const filePath = await downloadMetadataAsync({
analytics,
exp,
credentialsCtx,
projectDir,
profile: submitProfile,
});
const relativePath = path.relative(process.cwd(), filePath);
Log.addNewLineIfNone();
+23 -10
View File
@@ -1,12 +1,14 @@
import { Platform } from '@expo/eas-build-job';
import { EasJsonAccessor } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import { ensureProjectConfiguredAsync } from '../../build/configure';
import EasCommand from '../../commandUtils/EasCommand';
import { CredentialsContext } from '../../credentials/context';
import Log, { learnMore } from '../../log';
import { createMetadataContextAsync } from '../../metadata/context';
import { handleMetadataError } from '../../metadata/errors';
import { uploadMetadataAsync } from '../../metadata/upload';
import { getProfilesAsync } from '../../utils/profiles';
export default class MetadataPush extends EasCommand {
static override description = 'sync the local store configuration to the app stores';
@@ -40,6 +42,18 @@ export default class MetadataPush extends EasCommand {
// this command is interactive (all nonInteractive flags passed to utility functions are false)
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
const submitProfiles = await getProfilesAsync({
type: 'submit',
easJsonAccessor: new EasJsonAccessor(projectDir),
platforms: [Platform.IOS],
profileName: flags.profile,
});
if (submitProfiles.length !== 1) {
throw new Error('Metadata only supports iOS and a single submit profile.');
}
const submitProfile = submitProfiles[0].profile;
const credentialsCtx = new CredentialsContext({
projectInfo: { exp, projectId },
projectDir,
@@ -49,16 +63,15 @@ export default class MetadataPush extends EasCommand {
nonInteractive: false,
});
const metadataCtx = await createMetadataContextAsync({
credentialsCtx,
analytics,
projectDir,
exp,
profileName: flags.profile,
});
try {
const { appleLink } = await uploadMetadataAsync(metadataCtx);
const { appleLink } = await uploadMetadataAsync({
analytics,
exp,
credentialsCtx,
projectDir,
profile: submitProfile,
});
Log.addNewLineIfNone();
Log.log(`🎉 Store configuration is synced with the app stores.
@@ -7,6 +7,8 @@ import LoggedInContextField from '../../../commandUtils/context/LoggedInContextF
import ProjectDirContextField from '../../../commandUtils/context/ProjectDirContextField';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { saveProjectIdToAppConfigAsync } from '../../../commandUtils/context/contextUtils/getProjectIdAsync';
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
import { jester, jester2 } from '../../../credentials/__tests__/fixtures-constants';
import { AppMutation } from '../../../graphql/mutations/AppMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
@@ -79,9 +81,11 @@ function mockTestProject(options: {
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
jest.spyOn(ProjectDirContextField.prototype, 'getValueAsync').mockResolvedValue('/test-project');
jest
.spyOn(LoggedInContextField.prototype, 'getValueAsync')
.mockResolvedValue({ actor: jester, graphqlClient });
jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
actor: jester,
featureGating: new FeatureGating({}, new FeatureGateEnvOverrides()),
graphqlClient,
});
}
const commandOptions = { root: '/test-project' } as any;
+17 -20
View File
@@ -1,11 +1,13 @@
import chalk from 'chalk';
import Table from 'cli-table3';
import dateFormat from 'dateformat';
import EasCommand from '../../commandUtils/EasCommand';
import { EnvironmentSecretsQuery } from '../../graphql/queries/EnvironmentSecretsQuery';
import { EnvironmentSecretTypeToSecretType } from '../../graphql/types/EnvironmentSecret';
import {
EnvironmentSecretWithScope,
EnvironmentSecretsQuery,
} from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log';
import formatFields from '../../utils/formatFields';
export default class EnvironmentSecretList extends EasCommand {
static override description = 'list environment secrets available for your current app';
@@ -25,23 +27,18 @@ export default class EnvironmentSecretList extends EasCommand {
const secrets = await EnvironmentSecretsQuery.allAsync(graphqlClient, projectId);
const table = new Table({
head: ['Name', 'Type', 'Scope', 'ID', 'Updated at'],
wordWrap: true,
});
for (const secret of secrets) {
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:}`);
Log.log(table.toString());
Log.log(secrets.map(secret => formatSecret(secret)).join(`\n\n${chalk.dim('———')}\n\n`));
}
}
function formatSecret(secret: EnvironmentSecretWithScope): string {
return formatFields([
{ label: 'ID', value: secret.id },
{ label: 'Name', value: secret.name },
{ label: 'Scope', value: secret.scope },
{ label: 'Type', value: secret.type },
// TODO: Figure out why do we name it updated, while it's created at?
{ label: 'Updated at', value: dateFormat(secret.createdAt, 'mmm dd HH:MM:ss') },
]);
}
+13 -279
View File
@@ -1,24 +1,10 @@
import { ExpoConfig, modifyConfigAsync } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import { getEASUpdateURL } from '../../api';
import EasCommand from '../../commandUtils/EasCommand';
import { AppPlatform } from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import { RequestedPlatform, appPlatformDisplayNames } from '../../platform';
import {
installExpoUpdatesAsync,
isExpoUpdatesInstalledOrAvailable,
} from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
const DEFAULT_MANAGED_RUNTIME_VERSION = { policy: 'sdkVersion' } as const;
const DEFAULT_BARE_RUNTIME_VERSION = '1.0.0' as const;
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import Log from '../../log';
import { RequestedPlatform } from '../../platform';
import { ensureEASUpdatesIsConfiguredAsync } from '../../update/configure';
export default class UpdateConfigure extends EasCommand {
static override description = 'configure the project to support EAS Update';
@@ -30,6 +16,7 @@ export default class UpdateConfigure extends EasCommand {
options: ['android', 'ios', 'all'],
default: 'all',
}),
...EASNonInteractiveFlag,
};
static override contextDefinition = {
@@ -38,280 +25,27 @@ export default class UpdateConfigure extends EasCommand {
};
async runAsync(): Promise<void> {
Log.log(
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
);
const { flags } = await this.parse(UpdateConfigure);
const platform = flags.platform as RequestedPlatform;
const {
projectConfig: { projectId, exp, projectDir },
loggedIn: { graphqlClient },
} = await this.getContextAsync(UpdateConfigure, {
nonInteractive: true,
nonInteractive: flags['non-interactive'],
});
const platform = flags.platform as RequestedPlatform;
if (!isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion)) {
await installExpoUpdatesAsync(projectDir);
}
Log.log(
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
);
const [androidWorkflow, iosWorkflow] = await Promise.all([
resolveWorkflowAsync(projectDir, Platform.ANDROID),
resolveWorkflowAsync(projectDir, Platform.IOS),
]);
const updatedExp = await configureAppJSONForEASUpdateAsync({
projectDir,
await ensureEASUpdatesIsConfiguredAsync(graphqlClient, {
exp,
platform,
workflows: {
android: androidWorkflow,
ios: iosWorkflow,
},
projectId,
projectDir,
platform,
});
Log.withTick(`Configured ${chalk.bold('app.json')} for EAS Update`);
// configure native files for EAS Update
if (
[RequestedPlatform.Android, RequestedPlatform.All].includes(platform) &&
androidWorkflow === Workflow.GENERIC
) {
await syncAndroidUpdatesConfigurationAsync(graphqlClient, projectDir, updatedExp, projectId);
Log.withTick(`Configured ${chalk.bold('AndroidManifest.xml')} for EAS Update`);
}
if (
[RequestedPlatform.Ios, RequestedPlatform.All].includes(platform) &&
iosWorkflow === Workflow.GENERIC
) {
await syncIosUpdatesConfigurationAsync(graphqlClient, projectDir, updatedExp, projectId);
Log.withTick(`Configured ${chalk.bold('Expo.plist')} for EAS Update`);
}
Log.addNewLineIfNone();
Log.log(`🎉 Your app is configured to run EAS Update!`);
}
}
async function configureAppJSONForEASUpdateAsync({
projectDir,
exp,
platform,
workflows,
projectId,
}: {
projectDir: string;
exp: ExpoConfig;
platform: RequestedPlatform;
workflows: {
[key in RequestedPlatform.Android | RequestedPlatform.Ios]: Workflow;
};
projectId: string;
}): Promise<ExpoConfig> {
// this command is non-interactive in the way it was designed
const easUpdateURL = getEASUpdateURL(projectId);
const updates = { ...exp.updates, url: easUpdateURL };
const androidDefaultRuntimeVersion =
workflows['android'] === Workflow.GENERIC
? DEFAULT_BARE_RUNTIME_VERSION
: DEFAULT_MANAGED_RUNTIME_VERSION;
const iosDefaultRuntimeVersion =
workflows['ios'] === Workflow.GENERIC
? DEFAULT_BARE_RUNTIME_VERSION
: DEFAULT_MANAGED_RUNTIME_VERSION;
const newAndroidRuntimeVersion =
exp.android?.runtimeVersion ?? exp.runtimeVersion ?? androidDefaultRuntimeVersion;
const newIosRuntimeVersion =
exp.ios?.runtimeVersion ?? exp.runtimeVersion ?? iosDefaultRuntimeVersion;
let newConfig: Partial<ExpoConfig>;
let newConfigOnlyAddedValues: Partial<ExpoConfig>;
switch (platform) {
case RequestedPlatform.All: {
if (isRuntimeEqual(newAndroidRuntimeVersion, newIosRuntimeVersion)) {
newConfig = {
runtimeVersion: newAndroidRuntimeVersion,
android: {
...exp.android,
runtimeVersion: undefined,
},
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
android: {
...exp.android,
runtimeVersion: newAndroidRuntimeVersion,
},
ios: {
...exp.ios,
runtimeVersion: newIosRuntimeVersion,
},
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;
}
case RequestedPlatform.Android: {
newConfig = {
android: {
...exp.android,
runtimeVersion: newAndroidRuntimeVersion,
},
updates,
};
newConfigOnlyAddedValues = {
android: {
runtimeVersion: newAndroidRuntimeVersion,
},
updates: {
url: easUpdateURL,
},
};
break;
}
case RequestedPlatform.Ios: {
newConfig = {
ios: {
...exp.ios,
runtimeVersion: newIosRuntimeVersion,
},
updates,
};
newConfigOnlyAddedValues = {
ios: {
runtimeVersion: newIosRuntimeVersion,
},
updates: {
url: easUpdateURL,
},
};
break;
}
default: {
throw new Error(`Unsupported platform: ${platform}`);
}
}
const result = await modifyConfigAsync(projectDir, newConfig);
const preexistingAndroidRuntimeVersion = exp.android?.runtimeVersion ?? exp.runtimeVersion;
const preexistingIosRuntimeVersion = exp.ios?.runtimeVersion ?? exp.runtimeVersion;
switch (result.type) {
case 'success':
if (exp.updates?.url) {
if (exp.updates.url !== easUpdateURL) {
Log.withTick(
`Overwrote "${exp.updates?.url}" with "${easUpdateURL}" for the updates.url value in app.json`
);
}
} else {
Log.withTick(`Set updates.url value, to "${easUpdateURL}" in app.json`);
}
if (
!preexistingAndroidRuntimeVersion &&
[RequestedPlatform.Android, RequestedPlatform.All].includes(platform)
) {
Log.withTick(
`Set ${appPlatformDisplayNames[AppPlatform.Android]} runtimeVersion to "${JSON.stringify(
newConfig.android?.runtimeVersion ?? newConfig.runtimeVersion
)}" in app.json`
);
}
if (
!preexistingIosRuntimeVersion &&
[RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)
) {
Log.withTick(
`Set ${appPlatformDisplayNames[AppPlatform.Ios]} runtimeVersion to "${JSON.stringify(
newConfig.ios?.runtimeVersion ?? newConfig.runtimeVersion
)}" in app.json`
);
}
break;
case 'warn': {
Log.addNewLineIfNone();
Log.warn(
`It looks like you are using a dynamic configuration! ${learnMore(
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs)'
)}`
);
Log.warn(
`In order to finish configuring your project for EAS Update, you are going to need manually add the following to your app.config.js:\n${learnMore(
'https://expo.fyi/eas-update-config.md'
)}\n`
);
Log.log(chalk.bold(JSON.stringify(newConfigOnlyAddedValues, null, 2)));
Log.addNewLineIfNone();
if (workflows['android'] === Workflow.GENERIC || workflows['ios'] === Workflow.GENERIC) {
Log.warn(
`You will also have to manually edit the projects ${chalk.bold(
'Expo.plist/AndroidManifest.xml'
)}. ${learnMore('https://expo.fyi/eas-update-config.md#native-configuration')}`
);
}
Log.addNewLineIfNone();
throw new Error(result.message);
}
case 'fail':
throw new Error(result.message);
default:
throw new Error('Unexpected result type from modifyConfigAsync');
}
assert(result.config, 'A successful result should have a config');
return result.config.expo;
}
function isRuntimeEqual(
runtimeVersionA: string | { policy: 'sdkVersion' | 'nativeVersion' | 'appVersion' },
runtimeVersionB: string | { policy: 'sdkVersion' | 'nativeVersion' | 'appVersion' }
): boolean {
if (typeof runtimeVersionA === 'string' && typeof runtimeVersionB === 'string') {
return runtimeVersionA === runtimeVersionB;
} else if (typeof runtimeVersionA === 'object' && typeof runtimeVersionB === 'object') {
return runtimeVersionA.policy === runtimeVersionB.policy;
} else {
return false;
}
}
+138 -170
View File
@@ -6,12 +6,11 @@ import assert from 'assert';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { getEASUpdateURL } from '../../api';
import { selectBranchOnAppAsync } from '../../branch/queries';
import { BranchNotFoundError, getDefaultBranchNameAsync } from '../../branch/utils';
import { ensureBranchExistsAsync, selectBranchOnAppAsync } from '../../branch/queries';
import { getDefaultBranchNameAsync } from '../../branch/utils';
import { getUpdateGroupUrl } from '../../build/utils/url';
import { ensureChannelExistsAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import fetch from '../../fetch';
@@ -21,18 +20,13 @@ import {
Update,
UpdateInfoGroup,
UpdatePublishMutation,
ViewBranchQueryVariables,
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log, { learnMore, link } from '../../log';
import { ora } from '../../ora';
import {
getOwnerAccountForProjectIdAsync,
installExpoUpdatesAsync,
isExpoUpdatesInstalledOrAvailable,
} from '../../project/projectUtils';
import { RequestedPlatform, requestedPlatformDisplayNames } from '../../platform';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import {
PublishPlatform,
buildBundlesAsync,
@@ -42,9 +36,10 @@ import {
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
import { confirmAsync, promptAsync } from '../../prompts';
import { promptAsync } from '../../prompts';
import { ensureEASUpdatesIsConfiguredAsync } from '../../update/configure';
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
import { formatUpdateMessage } from '../../update/utils';
import { formatUpdateMessage, truncateString as truncateUpdateMessage } from '../../update/utils';
import {
checkManifestBodyAgainstUpdateInfoGroup,
getCodeSigningInfoAsync,
@@ -55,76 +50,36 @@ 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';
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
export type PublishPlatformFlag = PublishPlatform | 'all';
async function ensureChannelExistsAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
branchId,
channelName,
}: {
appId: string;
branchId: string;
channelName: string;
}
): Promise<void> {
try {
await createUpdateChannelOnAppAsync(graphqlClient, {
appId,
channelName,
branchId,
});
Log.withTick(
`Created a channel: ${chalk.bold(channelName)} pointed at branch: ${chalk.bold(channelName)}.`
);
} catch (e: any) {
const isIgnorableError =
e.graphQLErrors?.length === 1 &&
e.graphQLErrors[0].extensions.errorCode === 'CHANNEL_ALREADY_EXISTS';
if (!isIgnorableError) {
throw e;
}
}
}
type RawUpdateFlags = {
auto: boolean;
branch?: string;
message?: string;
group?: string;
republish?: boolean;
platform: string;
'input-dir': string;
'skip-bundler': boolean;
'private-key-path'?: string;
'non-interactive': boolean;
json: boolean;
};
export async function ensureBranchExistsAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, name: branchName }: ViewBranchQueryVariables
): Promise<{
branchId: string;
}> {
try {
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId,
name: branchName,
});
const { id } = updateBranch;
await ensureChannelExistsAsync(graphqlClient, { appId, branchId: id, channelName: branchName });
return { branchId: id };
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
appId,
name: branchName,
});
Log.withTick(`Created branch: ${chalk.bold(branchName)}`);
await ensureChannelExistsAsync(graphqlClient, {
appId,
branchId: newUpdateBranch.id,
channelName: branchName,
});
return { branchId: newUpdateBranch.id };
} else {
throw error;
}
}
}
type UpdateFlags = {
auto: boolean;
platform: RequestedPlatform;
branchName?: string;
updateMessage?: string;
republish: boolean;
groupId?: string;
inputDir: string;
skipBundler: boolean;
privateKeyPath?: string;
json: boolean;
nonInteractive: boolean;
};
export default class UpdatePublish extends EasCommand {
static override description = 'publish an update group';
@@ -179,21 +134,22 @@ export default class UpdatePublish extends EasCommand {
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(UpdatePublish);
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
const { flags: rawFlags } = await this.parse(UpdatePublish);
const paginatedQueryOptions = getPaginatedQueryOptions(rawFlags);
let {
branch: branchName,
auto: autoFlag,
message,
platform: platformFlag,
branchName,
updateMessage,
republish,
group,
'input-dir': inputDir,
'skip-bundler': skipBundler,
platform,
'private-key-path': privateKeyPath,
'non-interactive': nonInteractive,
groupId,
inputDir,
skipBundler,
privateKeyPath,
json: jsonFlag,
} = flags;
nonInteractive,
} = this.sanitizeFlags(rawFlags);
const {
getDynamicProjectConfigAsync,
loggedIn: { graphqlClient },
@@ -205,11 +161,11 @@ export default class UpdatePublish extends EasCommand {
enableJsonOutput();
}
const platformFlag = platform as PublishPlatformFlag;
// If a group was specified, that means we are republishing it.
republish = republish || !!group;
const { exp, projectId, projectDir } = await getDynamicProjectConfigAsync({
const {
exp: expPossiblyWithoutEasUpdateConfigured,
projectId,
projectDir,
} = await getDynamicProjectConfigAsync({
isPublicConfig: true,
});
@@ -219,34 +175,16 @@ export default class UpdatePublish extends EasCommand {
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
await ensureEASUpdatesIsConfiguredAsync(graphqlClient, {
exp: expPossiblyWithoutEasUpdateConfigured,
platform: platformFlag,
projectDir,
projectId,
});
const { exp } = await getDynamicProjectConfigAsync();
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion);
if (!hasExpoUpdates && nonInteractive) {
Errors.error(
`${chalk.bold(
'expo-updates'
)} must already be installed when executing in non-interactive mode`,
{ exit: 1 }
);
}
if (!hasExpoUpdates) {
const install = await confirmAsync({
message: chalk`The module {cyan expo-updates} must be installed to load EAS updates in-app. Install?`,
instructions: 'The command will abort unless you agree.',
});
if (install) {
await installExpoUpdatesAsync(projectDir);
} else {
Errors.error(`Install ${chalk.bold('expo-updates')} and try again.`, {
exit: 1,
});
}
}
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
await checkEASUpdateURLIsSetAsync(exp, projectId);
if (!branchName) {
if (autoFlag) {
@@ -294,9 +232,9 @@ export default class UpdatePublish extends EasCommand {
Update,
'group' | 'message' | 'runtimeVersion' | 'manifestFragment' | 'platform'
>[];
if (group) {
if (groupId) {
const updatesByGroup = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
groupId: group,
groupId,
});
updatesToRepublish = updatesByGroup;
} else {
@@ -317,7 +255,7 @@ export default class UpdatePublish extends EasCommand {
if (updatesToRepublishFilteredByPlatform.length === 0) {
throw new Error(
`There are no updates on branch "${branchName}" published for the platform(s) "${platformFlag}" with group ID "${
group ? group : updatesToRepublish[0].group
groupId ? groupId : updatesToRepublish[0].group
}". Did you mean to publish a new update instead?`
);
}
@@ -327,7 +265,7 @@ export default class UpdatePublish extends EasCommand {
if (updatesToRepublishFilteredByPlatform.length !== defaultPublishPlatforms.length) {
Log.warn(`You are republishing an update that wasn't published for all platforms.`);
}
publicationPlatformMessage = `The republished update will appear on the same plaforms it was originally published on: ${updatesToRepublishFilteredByPlatform
publicationPlatformMessage = `The republished update will appear on the same platforms it was originally published on: ${updatesToRepublishFilteredByPlatform
.map(update => update.platform)
.join(', ')}`;
} else {
@@ -343,11 +281,11 @@ export default class UpdatePublish extends EasCommand {
}
// These are the same for each member of an update group
group = updatesToRepublishFilteredByPlatform[0].group;
groupId = updatesToRepublishFilteredByPlatform[0].group;
oldMessage = updatesToRepublishFilteredByPlatform[0].message ?? '';
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
if (!message) {
if (!updateMessage) {
if (nonInteractive) {
throw new Error('Must supply --message when in non-interactive mode');
}
@@ -356,20 +294,20 @@ export default class UpdatePublish extends EasCommand {
if (jsonFlag) {
throw new Error(validationMessage);
}
({ publishMessage: message } = await promptAsync({
({ updateMessage } = await promptAsync({
type: 'text',
name: 'publishMessage',
name: 'updateMessage',
message: `Provide an update message.`,
initial: `Republish "${oldMessage!}" - group: ${group}`,
initial: `Republish "${oldMessage!}" - group: ${groupId}`,
validate: (value: any) => (value ? true : validationMessage),
}));
}
} else {
if (!message && autoFlag) {
message = (await getVcsClient().getLastCommitMessageAsync())?.trim();
if (!updateMessage && autoFlag) {
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
}
if (!message) {
if (!updateMessage) {
if (nonInteractive) {
throw new Error('Must supply --message or use --auto when in non-interactive mode');
}
@@ -378,9 +316,9 @@ export default class UpdatePublish extends EasCommand {
if (jsonFlag) {
throw new Error(validationMessage);
}
({ publishMessage: message } = await promptAsync({
({ updateMessage } = await promptAsync({
type: 'text',
name: 'publishMessage',
name: 'updateMessage',
message: `Provide an update message.`,
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
validate: (value: any) => (value ? true : validationMessage),
@@ -391,7 +329,7 @@ export default class UpdatePublish extends EasCommand {
if (!skipBundler) {
const bundleSpinner = ora().start('Building bundle...');
try {
await buildBundlesAsync({ projectDir, inputDir });
await buildBundlesAsync({ projectDir, inputDir, exp });
bundleSpinner.succeed('Built bundle!');
} catch (e) {
bundleSpinner.fail('Failed to build bundle!');
@@ -426,7 +364,10 @@ export default class UpdatePublish extends EasCommand {
}
}
const truncatedMessage = truncatePublishUpdateMessage(message!);
const truncatedMessage = truncateUpdateMessage(updateMessage!, 1024);
if (truncatedMessage !== updateMessage) {
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
}
const runtimeToPlatformMapping: Record<string, string[]> = {};
for (const runtime of new Set(Object.values(runtimeVersions))) {
@@ -437,8 +378,16 @@ export default class UpdatePublish extends EasCommand {
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
name: branchName,
branchName,
});
await ensureChannelExistsAsync(graphqlClient, {
appId: projectId,
branchId,
channelName: branchName,
});
Log.withTick(`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`);
const gitCommitHash = await getVcsClient().getCommitHashAsync();
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups: PublishUpdateGroupInput[] = Object.entries(runtimeToPlatformMapping).map(
@@ -460,6 +409,7 @@ export default class UpdatePublish extends EasCommand {
updateInfoGroup: localUpdateInfoGroup,
runtimeVersion: republish ? oldRuntimeVersion : runtime,
message: truncatedMessage,
gitCommitHash,
awaitingCodeSigningInfo: !!codeSigningInfo,
};
}
@@ -558,6 +508,7 @@ export default class UpdatePublish extends EasCommand {
: []),
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
{ label: 'Message', value: truncatedMessage },
...(gitCommitHash ? [{ label: 'Commit', value: gitCommitHash }] : []),
{ label: 'Website link', value: updateGroupLink },
])
);
@@ -577,14 +528,60 @@ export default class UpdatePublish extends EasCommand {
}
}
}
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
const nonInteractive = flags['non-interactive'] ?? false;
const { auto, branch: branchName, message: updateMessage } = flags;
if (nonInteractive && !auto && !(branchName && updateMessage)) {
Errors.error(
'--auto or both --branch and --message are required when updating in non-interactive mode',
{ exit: 1 }
);
}
const groupId = flags.group;
const republish = flags.republish || !!groupId; // When --group is defined, we are republishing
if (nonInteractive && republish && !groupId) {
Errors.error(`--group is required when updating in non-interactive mode`, { exit: 1 });
}
return {
auto,
branchName,
updateMessage,
groupId,
republish,
inputDir: flags['input-dir'],
skipBundler: flags['skip-bundler'],
platform: flags.platform as RequestedPlatform,
privateKeyPath: flags['private-key-path'],
nonInteractive,
json: flags.json ?? false,
};
}
}
function transformRuntimeVersions(exp: ExpoConfig, platforms: Platform[]): Record<string, string> {
return Object.fromEntries(
platforms.map(platform => [
platform,
nullthrows(
Updates.getRuntimeVersion(exp, platform),
`Unable to determine runtime version for ${
requestedPlatformDisplayNames[platform]
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
),
])
);
}
async function getRuntimeVersionObjectAsync(
exp: ExpoConfig,
platformFlag: PublishPlatformFlag,
platform: RequestedPlatform,
projectDir: string
): Promise<Record<string, string>> {
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
const platforms = (platform === 'all' ? ['android', 'ios'] : [platform]) as Platform[];
for (const platform of platforms) {
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
@@ -598,34 +595,5 @@ async function getRuntimeVersionObjectAsync(
}
}
return Object.fromEntries(
platforms.map(platform => [
platform,
nullthrows(
Updates.getRuntimeVersion(exp, platform),
`Unable to determine runtime version for ${platform}`
),
])
);
return transformRuntimeVersions(exp, platforms);
}
async function checkEASUpdateURLIsSetAsync(exp: ExpoConfig, projectId: string): Promise<void> {
const configuredURL = exp.updates?.url;
const expectedURL = getEASUpdateURL(projectId);
if (configuredURL !== expectedURL) {
throw new Error(
`The update URL is incorrectly configured for EAS Update. Set updates.url to ${expectedURL} in your ${chalk.bold(
'app.json'
)}.`
);
}
}
export const truncatePublishUpdateMessage = (originalMessage: string): string => {
if (originalMessage.length > 1024) {
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
return originalMessage.substring(0, 1021) + '...';
}
return originalMessage;
};
@@ -1,6 +1,7 @@
import { AppFragment, Role } from '../../graphql/generated';
import { Actor } from '../../user/User';
export const jester = {
export const jester: Actor = {
__typename: 'User' as const,
id: 'jester-id',
username: 'jester',
@@ -22,6 +23,7 @@ export const jester = {
},
],
isExpoAdmin: false,
featureGates: {},
};
export const jester2 = {
@@ -36,6 +38,7 @@ export const jester2 = {
},
],
isExpoAdmin: false,
featureGates: {},
};
export const testUsername = jester.username;
@@ -254,7 +254,7 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
]);
expect(results.enabled).toMatchInlineSnapshot(`
Array [
[
"HealthKit",
"Apple Pay Payment Processing",
"iCloud",
@@ -43,6 +43,7 @@ describe(AccountResolver, () => {
},
],
isExpoAdmin: false,
featureGates: {},
};
describe('when inside project dir', () => {
@@ -1,5 +1,4 @@
import chalk from 'chalk';
import Table from 'cli-table3';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { AppleDeviceMutation } from '../../../credentials/ios/api/graphql/mutations/AppleDeviceMutation';
@@ -8,6 +7,7 @@ import Log from '../../../log';
import { ora } from '../../../ora';
import { confirmAsync, promptAsync } from '../../../prompts';
import { isValidUDID, normalizeUDID } from '../../udids';
import { formatNewDevice } from '../../utils/formatDevice';
interface DeviceData {
udid: string;
@@ -15,11 +15,6 @@ interface DeviceData {
deviceClass: AppleDeviceClass | null;
}
const DEVICE_CLASS_DISPLAY_NAMES: Record<AppleDeviceClass, string> = {
[AppleDeviceClass.Iphone]: 'iPhone',
[AppleDeviceClass.Ipad]: 'iPad',
};
export async function runInputMethodAsync(
graphqlClient: ExpoGraphqlClient,
accountId: string,
@@ -89,7 +84,7 @@ async function collectDeviceDataAsync(
This will ${chalk.bold('not')} register the device on the Apple Developer Portal yet.`
);
Log.newLine();
printDeviceDataSummary(deviceData, appleTeam);
Log.log(formatNewDevice({ ...deviceData, identifier: deviceData.udid }, appleTeam));
Log.newLine();
const registrationConfirmed = await confirmAsync({
@@ -156,21 +151,3 @@ async function promptForDeviceClassAsync(
});
return deviceClass;
}
function printDeviceDataSummary(
{ udid, name, deviceClass }: DeviceData,
appleTeam: Pick<AppleTeam, 'appleTeamIdentifier' | 'appleTeamName'>
): void {
const deviceSummary = new Table({
colWidths: [25, 55],
wordWrap: true,
});
deviceSummary.push(
['Apple Team ID', appleTeam.appleTeamIdentifier],
['Apple Team Name', appleTeam.appleTeamName ?? '(unknown)'],
['Device UDID', udid],
['Device Name', name ?? '(empty)'],
['Device Class', deviceClass ? DEVICE_CLASS_DISPLAY_NAMES[deviceClass] : '(unknown)']
);
Log.log(deviceSummary.toString());
}
@@ -1,29 +1,61 @@
import { AppleDevice, AppleTeam } from '../../graphql/generated';
import formatFields from '../../utils/formatFields';
import { AppleDevice, AppleDeviceClass, AppleTeam } from '../../graphql/generated';
import formatFields, { FormatFieldsItem } from '../../utils/formatFields';
type Device = Pick<AppleDevice, 'id' | 'identifier' | 'name' | 'deviceClass' | 'enabled' | 'model'>;
type NewDevice = Pick<AppleDevice, 'identifier' | 'name' | 'deviceClass'>;
export type AppleTeamIdAndName = Pick<AppleTeam, 'appleTeamIdentifier' | 'appleTeamName'>;
const DEVICE_CLASS_DISPLAY_NAMES: Record<AppleDeviceClass, string> = {
[AppleDeviceClass.Iphone]: 'iPhone',
[AppleDeviceClass.Ipad]: 'iPad',
};
function formatDeviceClass(device: Device | NewDevice): string {
if (!device.deviceClass || !DEVICE_CLASS_DISPLAY_NAMES[device.deviceClass]) {
return 'Unknown';
}
return [DEVICE_CLASS_DISPLAY_NAMES[device.deviceClass], 'model' in device ? device.model : '']
.filter(value => !!value)
.join(' ');
}
export default function formatDevice(device: Device, team?: AppleTeamIdAndName): string {
const fields = [
const fields: FormatFieldsItem[] = [
{ label: 'ID', value: device.id },
{ label: 'Name', value: device.name ?? 'Unknown' },
{
label: 'Class',
value: device.deviceClass
? `${device.deviceClass}${device.model ? ` ${device.model}` : ''}`
: 'Unknown',
value: formatDeviceClass(device),
},
{ label: 'UDID', value: device.identifier },
];
if (team) {
fields.push(
...[
{ label: 'Apple Team ID', value: team.appleTeamIdentifier },
{ label: 'Apple Team Name', value: team.appleTeamName ?? 'Unknown' },
]
{ label: 'Apple Team ID', value: team.appleTeamIdentifier },
{ label: 'Apple Team Name', value: team.appleTeamName ?? 'Unknown' }
);
}
return formatFields(fields);
}
export function formatNewDevice(device: NewDevice, team?: AppleTeamIdAndName): string {
const fields: FormatFieldsItem[] = [
{ label: 'Name', value: device.name ?? '(empty)' },
{
label: 'Class',
value: formatDeviceClass(device),
},
{ label: 'UDID', value: device.identifier },
];
if (team) {
fields.push(
{ label: 'Apple Team ID', value: team.appleTeamIdentifier },
{ label: 'Apple Team Name', value: team.appleTeamName ?? 'Unknown' }
);
}
+8
View File
@@ -8,4 +8,12 @@ export default {
* ignore failures when parsing build.gradle.
*/
overrideAndroidApplicationId: process.env.EAS_DANGEROUS_OVERRIDE_ANDROID_APPLICATION_ID,
/**
* Comma separated list of feature gate keys of feature gates to evaluate override to true.
*/
featureGateEnable: process.env.EAS_FG_ENABLE,
/**
* Comma separated list of feature gate keys of feature gates to evaluate override to false.
*/
featureGateDisable: process.env.EAS_FG_DISABLE,
};
+98 -23
View File
@@ -401,6 +401,7 @@ export type AccountSsoConfiguration = {
issuer?: Maybe<Scalars['String']>;
jwksEndpoint?: Maybe<Scalars['String']>;
redirectUri: Scalars['String'];
revokeEndpoint?: Maybe<Scalars['String']>;
tokenEndpoint?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime'];
userInfoEndpoint?: Maybe<Scalars['String']>;
@@ -415,6 +416,7 @@ export type AccountSsoConfigurationData = {
issuer?: InputMaybe<Scalars['String']>;
jwksEndpoint?: InputMaybe<Scalars['String']>;
redirectUri: Scalars['String'];
revokeEndpoint?: InputMaybe<Scalars['String']>;
tokenEndpoint?: InputMaybe<Scalars['String']>;
userInfoEndpoint?: InputMaybe<Scalars['String']>;
};
@@ -457,6 +459,7 @@ export type AccountSsoConfigurationPublicData = {
issuer?: Maybe<Scalars['String']>;
jwksEndpoint?: Maybe<Scalars['String']>;
redirectUri: Scalars['String'];
revokeEndpoint?: Maybe<Scalars['String']>;
tokenEndpoint?: Maybe<Scalars['String']>;
userInfoEndpoint?: Maybe<Scalars['String']>;
};
@@ -1744,6 +1747,8 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
__typename?: 'Build';
activityTimestamp: Scalars['DateTime'];
actor?: Maybe<Actor>;
/** The actual resource class of the builder assigned to the build job */
actualResourceClass?: Maybe<BuildResourceClass>;
appBuildVersion?: Maybe<Scalars['String']>;
appVersion?: Maybe<Scalars['String']>;
artifacts?: Maybe<BuildArtifacts>;
@@ -1784,7 +1789,8 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
queuePosition?: Maybe<Scalars['Int']>;
reactNativeVersion?: Maybe<Scalars['String']>;
releaseChannel?: Maybe<Scalars['String']>;
resourceClass?: Maybe<BuildResourceClass>;
/** The builder resource class requested by the developer */
resourceClass: BuildResourceClass;
runFromCI?: Maybe<Scalars['Boolean']>;
runtimeVersion?: Maybe<Scalars['String']>;
sdkVersion?: Maybe<Scalars['String']>;
@@ -2219,6 +2225,7 @@ export type CreateAccessTokenResponse = {
export type CreateAndroidSubmissionInput = {
appId: Scalars['ID'];
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
archiveUrl?: InputMaybe<Scalars['String']>;
config: AndroidSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']>;
@@ -2245,6 +2252,7 @@ export type CreateGitHubRepositoryInput = {
appId: Scalars['ID'];
githubAppInstallationId: Scalars['ID'];
githubRepositoryIdentifier: Scalars['Int'];
nodeIdentifier: Scalars['String'];
};
export type CreateGitHubRepositorySettingsInput = {
@@ -2255,6 +2263,7 @@ export type CreateGitHubRepositorySettingsInput = {
export type CreateIosSubmissionInput = {
appId: Scalars['ID'];
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
archiveUrl?: InputMaybe<Scalars['String']>;
config: IosSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']>;
@@ -2317,6 +2326,16 @@ export type DeleteGoogleServiceAccountKeyResult = {
id: Scalars['ID'];
};
export type DeleteIosAppBuildCredentialsResult = {
__typename?: 'DeleteIosAppBuildCredentialsResult';
id: Scalars['ID'];
};
export type DeleteIosAppCredentialsResult = {
__typename?: 'DeleteIosAppCredentialsResult';
id: Scalars['ID'];
};
export type DeleteRobotResult = {
__typename?: 'DeleteRobotResult';
id: Scalars['ID'];
@@ -2506,7 +2525,7 @@ export type GetSignedAssetUploadSpecificationsResult = {
export type GitHubAppInstallation = {
__typename?: 'GitHubAppInstallation';
accessibleRepositories: Array<GitHubAppInstallationAccessibleRepository>;
accessibleRepositories: GitHubRepositoryPaginationResult;
account: Account;
actor?: Maybe<Actor>;
id: Scalars['ID'];
@@ -2526,6 +2545,7 @@ export type GitHubAppInstallationAccessibleRepository = {
description?: Maybe<Scalars['String']>;
id: Scalars['Int'];
name: Scalars['String'];
nodeId: Scalars['String'];
owner: GitHubRepositoryOwner;
private: Scalars['Boolean'];
url: Scalars['String'];
@@ -2566,14 +2586,18 @@ export type GitHubAppQuery = {
__typename?: 'GitHubAppQuery';
appIdentifier: Scalars['String'];
clientIdentifier: Scalars['String'];
searchRepositories: Array<GitHubAppInstallationAccessibleRepository>;
installation: GitHubAppInstallation;
searchRepositories: GitHubRepositoryPaginationResult;
};
export type GitHubAppQueryInstallationArgs = {
id: Scalars['ID'];
};
export type GitHubAppQuerySearchRepositoriesArgs = {
githubAppInstallationId: Scalars['ID'];
page?: InputMaybe<Scalars['Int']>;
perPage?: InputMaybe<Scalars['Int']>;
query: Scalars['String'];
};
@@ -2584,6 +2608,7 @@ export type GitHubRepository = {
githubRepositoryIdentifier: Scalars['Int'];
id: Scalars['ID'];
metadata?: Maybe<GitHubRepositoryMetadata>;
nodeIdentifier: Scalars['String'];
};
export type GitHubRepositoryMetadata = {
@@ -2623,6 +2648,12 @@ export type GitHubRepositoryOwner = {
url: Scalars['String'];
};
export type GitHubRepositoryPaginationResult = {
__typename?: 'GitHubRepositoryPaginationResult';
repositories: Array<GitHubAppInstallationAccessibleRepository>;
totalCount: Scalars['Int'];
};
export type GitHubRepositorySettings = {
__typename?: 'GitHubRepositorySettings';
app: App;
@@ -2788,6 +2819,8 @@ export type IosAppBuildCredentialsMutation = {
__typename?: 'IosAppBuildCredentialsMutation';
/** Create a set of build credentials for an iOS app */
createIosAppBuildCredentials: IosAppBuildCredentials;
/** Disassociate the build credentials from an iOS app */
deleteIosAppBuildCredentials: DeleteIosAppBuildCredentialsResult;
/** Set the distribution certificate to be used for an iOS app */
setDistributionCertificate: IosAppBuildCredentials;
/** Set the provisioning profile to be used for an iOS app */
@@ -2801,6 +2834,11 @@ export type IosAppBuildCredentialsMutationCreateIosAppBuildCredentialsArgs = {
};
export type IosAppBuildCredentialsMutationDeleteIosAppBuildCredentialsArgs = {
id: Scalars['ID'];
};
export type IosAppBuildCredentialsMutationSetDistributionCertificateArgs = {
distributionCertificateId: Scalars['ID'];
id: Scalars['ID'];
@@ -2849,6 +2887,8 @@ export type IosAppCredentialsMutation = {
__typename?: 'IosAppCredentialsMutation';
/** Create a set of credentials for an iOS app */
createIosAppCredentials: IosAppCredentials;
/** Delete a set of credentials for an iOS app */
deleteIosAppCredentials: DeleteIosAppCredentialsResult;
/** Set the App Store Connect Api Key to be used for submitting an iOS app */
setAppStoreConnectApiKeyForSubmissions: IosAppCredentials;
/** Set the push key to be used in an iOS app */
@@ -2863,6 +2903,11 @@ export type IosAppCredentialsMutationCreateIosAppCredentialsArgs = {
};
export type IosAppCredentialsMutationDeleteIosAppCredentialsArgs = {
id: Scalars['ID'];
};
export type IosAppCredentialsMutationSetAppStoreConnectApiKeyForSubmissionsArgs = {
ascApiKeyId: Scalars['ID'];
id: Scalars['ID'];
@@ -2874,6 +2919,16 @@ export type IosAppCredentialsMutationSetPushKeyArgs = {
pushKeyId: Scalars['ID'];
};
export type IosAppCredentialsQuery = {
__typename?: 'IosAppCredentialsQuery';
byId: IosAppCredentials;
};
export type IosAppCredentialsQueryByIdArgs = {
iosAppCredentialsId: Scalars['ID'];
};
/** @deprecated Use developmentClient option instead. */
export enum IosBuildType {
DevelopmentClient = 'DEVELOPMENT_CLIENT',
@@ -3281,6 +3336,7 @@ export type PublicArtifacts = {
export type PublishUpdateGroupInput = {
awaitingCodeSigningInfo?: InputMaybe<Scalars['Boolean']>;
branchId: Scalars['String'];
gitCommitHash?: InputMaybe<Scalars['String']>;
message?: InputMaybe<Scalars['String']>;
runtimeVersion: Scalars['String'];
updateInfoGroup: UpdateInfoGroup;
@@ -3499,6 +3555,8 @@ export type RootQuery = {
githubApp: GitHubAppQuery;
/** Top-level query object for querying Stripe Invoices. */
invoice: InvoiceQuery;
/** Top-level query object for querying IosAppCredentials. */
iosAppCredentials: IosAppCredentialsQuery;
/**
* If authenticated as a typical end user, this is the appropriate top-level
* query object
@@ -3829,6 +3887,20 @@ export enum SubmissionAndroidTrack {
Production = 'PRODUCTION'
}
export type SubmissionArchiveSourceInput = {
/** Required if the archive source type is GCS_BUILD_APPLICATION_ARCHIVE or GCS_SUBMIT_ARCHIVE */
bucketKey?: InputMaybe<Scalars['String']>;
type: SubmissionArchiveSourceType;
/** Required if the archive source type is URL */
url?: InputMaybe<Scalars['String']>;
};
export enum SubmissionArchiveSourceType {
GcsBuildApplicationArchive = 'GCS_BUILD_APPLICATION_ARCHIVE',
GcsSubmitArchive = 'GCS_SUBMIT_ARCHIVE',
Url = 'URL'
}
export type SubmissionError = {
__typename?: 'SubmissionError';
errorCode?: Maybe<Scalars['String']>;
@@ -3921,6 +3993,7 @@ export type Update = ActivityTimelineProjectActivity & {
branchId: Scalars['ID'];
codeSigningInfo?: Maybe<CodeSigningInfo>;
createdAt: Scalars['DateTime'];
gitCommitHash?: Maybe<Scalars['String']>;
group: Scalars['String'];
id: Scalars['ID'];
manifestFragment: Scalars['String'];
@@ -4093,8 +4166,10 @@ export type UploadSessionCreateUploadSessionArgs = {
};
export enum UploadSessionType {
EasBuildGcsProjectSources = 'EAS_BUILD_GCS_PROJECT_SOURCES',
EasBuildProjectSources = 'EAS_BUILD_PROJECT_SOURCES',
EasSubmitAppArchive = 'EAS_SUBMIT_APP_ARCHIVE'
EasSubmitAppArchive = 'EAS_SUBMIT_APP_ARCHIVE',
EasSubmitGcsAppArchive = 'EAS_SUBMIT_GCS_APP_ARCHIVE'
}
export type UsageMetricTotal = {
@@ -4467,6 +4542,15 @@ export type CreateUpdateBranchForAppMutationVariables = Exact<{
export type CreateUpdateBranchForAppMutation = { __typename?: 'RootMutation', updateBranch: { __typename?: 'UpdateBranchMutation', createUpdateBranchForApp: { __typename?: 'UpdateBranch', id: string, name: string } } };
export type CreateUpdateChannelOnAppMutationVariables = Exact<{
appId: Scalars['ID'];
name: Scalars['String'];
branchMapping: Scalars['String'];
}>;
export type CreateUpdateChannelOnAppMutation = { __typename?: 'RootMutation', updateChannel: { __typename?: 'UpdateChannelMutation', createUpdateChannelForApp: { __typename?: 'UpdateChannel', id: string, name: string, branchMapping: string } } };
export type GetBranchInfoQueryVariables = Exact<{
appId: Scalars['String'];
name: Scalars['String'];
@@ -4496,15 +4580,6 @@ export type CancelBuildMutationVariables = Exact<{
export type CancelBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', cancel: { __typename?: 'Build', id: string, status: BuildStatus } } };
export type CreateUpdateChannelOnAppMutationVariables = Exact<{
appId: Scalars['ID'];
name: Scalars['String'];
branchMapping: Scalars['String'];
}>;
export type CreateUpdateChannelOnAppMutation = { __typename?: 'RootMutation', updateChannel: { __typename?: 'UpdateChannelMutation', createUpdateChannelForApp: { __typename?: 'UpdateChannel', id: string, name: string, branchMapping: string } } };
export type DeleteUpdateChannelMutationVariables = Exact<{
channelId: Scalars['ID'];
}>;
@@ -4931,7 +5006,7 @@ export type CreateAndroidBuildMutationVariables = Exact<{
}>;
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateIosBuildMutationVariables = Exact<{
appId: Scalars['ID'];
@@ -4941,7 +5016,7 @@ export type CreateIosBuildMutationVariables = Exact<{
}>;
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateEnvironmentSecretForAccountMutationVariables = Exact<{
input: CreateEnvironmentSecretInput;
@@ -5096,14 +5171,14 @@ export type BuildsByIdQueryVariables = Exact<{
}>;
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type BuildsWithSubmissionsByIdQueryVariables = Exact<{
buildId: Scalars['ID'];
}>;
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type ViewBuildsOnAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5113,7 +5188,7 @@ export type ViewBuildsOnAppQueryVariables = Exact<{
}>;
export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }> } } };
export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }> } } };
export type ViewUpdateChannelOnAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5209,7 +5284,7 @@ export type ViewUpdateGroupsOnAppQuery = { __typename?: 'RootQuery', app: { __ty
export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
export type CurrentUserQuery = { __typename?: 'RootQuery', meActor?: { __typename: 'Robot', firstName?: string | null, id: string, isExpoAdmin: boolean, accounts: Array<{ __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'User', id: string } }> }> } | { __typename: 'User', username: string, id: string, isExpoAdmin: boolean, primaryAccount: { __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'User', id: string } }> }, accounts: Array<{ __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'User', id: string } }> }> } | null };
export type CurrentUserQuery = { __typename?: 'RootQuery', meActor?: { __typename: 'Robot', firstName?: string | null, id: string, featureGates: any, isExpoAdmin: boolean, accounts: Array<{ __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'User', id: string } }> }> } | { __typename: 'User', username: string, id: string, featureGates: any, isExpoAdmin: boolean, primaryAccount: { __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'User', id: string } }> }, accounts: Array<{ __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'User', id: string } }> }> } | null };
export type WebhooksByAppIdQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5230,9 +5305,9 @@ export type AccountFragment = { __typename?: 'Account', id: string, name: string
export type AppFragment = { __typename?: 'App', id: string, fullName: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'User', id: string } }> } };
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any };
@@ -18,7 +18,8 @@ export const KeystoreGenerationUrlMutation = {
}
}
}
`
`,
{}
)
.toPromise()
);
@@ -30,12 +30,13 @@ export const UserQuery = {
id
...AccountFragment
}
featureGates
isExpoAdmin
}
}
${print(AccountFragmentNode)}
`,
/* variables */ undefined,
{},
{
additionalTypenames: ['User'],
}
@@ -16,6 +16,7 @@ export const BuildFragmentNode = gql`
artifacts {
buildUrl
xcodeBuildLogsUrl
applicationArchiveUrl
}
initiatingActor {
__typename
+4
View File
@@ -45,6 +45,10 @@ export default class Log {
Log.warn(`› ${chalk.bold('--' + flag)} flag is deprecated. ${message}`);
}
public static fail(message: string): void {
Log.log(`${chalk.red(logSymbols.error)} ${message}`);
}
public static succeed(message: string): void {
Log.log(`${chalk.green(logSymbols.success)} ${message}`);
}
@@ -1,128 +1,149 @@
import { SubmitProfile } from '@expo/eas-json';
import path from 'path';
import { getStaticConfigFilePath, loadConfigAsync } from '../config';
import { getStaticConfigFilePath, loadConfigAsync } from '../config/resolve';
import { MetadataValidationError } from '../errors';
describe(getStaticConfigFilePath, () => {
const projectDir = '/app';
it(`returns same file for store.config.json`, () => {
const metadataPath = 'store.config.json';
const expectedFile = path.join(projectDir, metadataPath);
function mockProfile(metadataPath: string): SubmitProfile {
return { metadataPath } as any;
}
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
it(`returns same file for store.config.json`, () => {
expect(getStaticConfigFilePath({ projectDir, profile: mockProfile('store.config.json') })).toBe(
path.join(projectDir, 'store.config.json')
);
});
it(`returns store.config.json for store.config.js`, () => {
const metadataPath = 'store.config.js';
const expectedFile = path.join(projectDir, 'store.config.json');
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
expect(getStaticConfigFilePath({ projectDir, profile: mockProfile('store.config.js') })).toBe(
path.join(projectDir, 'store.config.json')
);
});
it(`returns store.staging.json file for store.staging.js`, () => {
const metadataPath = 'store.staging.js';
const expectedFile = path.join(projectDir, 'store.staging.json');
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
expect(getStaticConfigFilePath({ projectDir, profile: mockProfile('store.staging.js') })).toBe(
path.join(projectDir, 'store.staging.json')
);
});
// This shouldn't be used IRL, but it tests if this function is working properly
it(`returns custom-name.json file for custom-name.js`, () => {
const metadataPath = 'custom-name.js';
const expectedFile = path.join(projectDir, 'custom-name.json');
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
expect(getStaticConfigFilePath({ projectDir, profile: mockProfile('custom-name.js') })).toBe(
path.join(projectDir, 'custom-name.json')
);
});
});
describe(loadConfigAsync, () => {
const projectDir = path.resolve(__dirname, 'fixtures');
function mockProfile(metadataPath: string): SubmitProfile {
return { metadataPath } as any;
}
it(`throws when file doesn't exist`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'doesnt-exist.json' })
loadConfigAsync({ projectDir, profile: mockProfile('doesnt-exist.json') })
).rejects.toThrow('file not found');
});
it(`throws when validation errors are found without skipping validation`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid.config.json' })
loadConfigAsync({ projectDir, profile: mockProfile('invalid.config.json') })
).rejects.toThrow('errors found');
});
it(`returns config from "store.config.json"`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'store.config.json' })
loadConfigAsync({ projectDir, profile: mockProfile('store.config.json') })
).resolves.toHaveProperty('apple.copyright', 'ACME');
});
it(`returns config from "store.config.js"`, async () => {
const year = new Date().getFullYear();
await expect(
loadConfigAsync({ projectDir, metadataPath: 'store.config.js' })
loadConfigAsync({ projectDir, profile: mockProfile('store.config.js') })
).resolves.toHaveProperty('apple.copyright', `${year} ACME`);
});
it(`returns config from "store.function.js"`, async () => {
const year = new Date().getFullYear();
await expect(
loadConfigAsync({ projectDir, metadataPath: 'store.function.js' })
loadConfigAsync({ projectDir, profile: mockProfile('store.function.js') })
).resolves.toHaveProperty('apple.copyright', `${year} ACME`);
});
it(`returns config from "store.async.js"`, async () => {
const year = new Date().getFullYear();
await expect(
loadConfigAsync({ projectDir, metadataPath: 'store.async.js' })
loadConfigAsync({ projectDir, profile: mockProfile('store.async.js') })
).resolves.toHaveProperty('apple.copyright', `${year} ACME`);
});
it(`returns invalid config from "invalid.config.json`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid.config.json', skipValidation: true })
loadConfigAsync({
projectDir,
profile: mockProfile('invalid.config.json'),
skipValidation: true,
})
).resolves.toMatchObject({ configVersion: -1 });
});
it(`returns invalid config from "invalid.config.js`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid.config.js', skipValidation: true })
loadConfigAsync({
projectDir,
profile: mockProfile('invalid.config.js'),
skipValidation: true,
})
).resolves.toMatchObject({ configVersion: -1 });
});
it(`returns invalid config from "invalid.function.js`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid.function.js', skipValidation: true })
loadConfigAsync({
projectDir,
profile: mockProfile('invalid.function.js'),
skipValidation: true,
})
).resolves.toMatchObject({ configVersion: -1 });
});
it(`returns invalid config from "invalid.async.js`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid.async.js', skipValidation: true })
loadConfigAsync({
projectDir,
profile: mockProfile('invalid.async.js'),
skipValidation: true,
})
).resolves.toMatchObject({ configVersion: -1 });
});
it(`throws invalid config type from "invalid-type.config.json"`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.config.json' })
loadConfigAsync({ projectDir, profile: mockProfile('invalid-type.config.json') })
).rejects.toThrow(MetadataValidationError);
});
it(`throws invalid config type from "invalid-type.config.js"`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.config.js' })
loadConfigAsync({ projectDir, profile: mockProfile('invalid-type.config.js') })
).rejects.toThrow(MetadataValidationError);
});
it(`throws invalid config type from "invalid-type.function.js"`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.function.js' })
loadConfigAsync({ projectDir, profile: mockProfile('invalid-type.function.js') })
).rejects.toThrow(MetadataValidationError);
});
it(`throws invalid config type from "invalid-type.async.js"`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.async.js' })
loadConfigAsync({ projectDir, profile: mockProfile('invalid-type.async.js') })
).rejects.toThrow(MetadataValidationError);
});
});
@@ -0,0 +1,4 @@
import { infoKeywordLength } from './infoKeywordLength';
import { infoRestrictedWords } from './infoRestrictedWords';
export const appleRules = [infoKeywordLength, infoRestrictedWords];
@@ -0,0 +1,36 @@
import { truthy } from '../../../utils/expodash/filter';
import { IssueRule } from '../../config/issue';
const KEYWORD_CHARACTER_LIMIT = 100;
/**
* Keywords are limited to 100 characters when converted to a comma separated string.
* @see https://developer.apple.com/app-store/search/
*/
export const infoKeywordLength: IssueRule = {
id: 'apple.info.keyword.length',
severity: 2,
validate(config) {
if (!config.apple?.info || Object.keys(config.apple.info).length === 0) {
return null;
}
return Object.keys(config.apple.info)
.map(locale => {
const keywords = config.apple?.info?.[locale].keywords ?? [];
const length = keywords.join(',').length;
if (length > KEYWORD_CHARACTER_LIMIT) {
return {
id: this.id,
severity: this.severity,
path: ['apple', 'info', locale, 'keywords'],
message: `Keywords are limited to ${KEYWORD_CHARACTER_LIMIT} characters, but found ${length}.`,
};
}
return null;
})
.filter(truthy);
},
};
@@ -0,0 +1,67 @@
import { truthy } from '../../../utils/expodash/filter';
import { IssueRule } from '../../config/issue';
import { AppleInfo } from '../types';
const RESTRICTED_PROPERTIES: (keyof AppleInfo)[] = ['title', 'subtitle', 'description', 'keywords'];
const RESTRICTED_WORDS = {
beta: 'Apple restricts the word "beta" and synonyms implying incomplete functionality.',
};
/**
* Apple restricts certain words from being used in name, description, or keywords.
* Using these words likely result in a rejection.
*/
export const infoRestrictedWords: IssueRule = {
id: 'apple.info.restrictedWords',
severity: 1,
validate(config) {
if (!config.apple?.info || Object.keys(config.apple.info).length === 0) {
return null;
}
return Object.keys(config.apple.info)
.map(locale =>
RESTRICTED_PROPERTIES.map(property => {
const value = getStringValue(config.apple?.info?.[locale][property]);
const issueDescription = getDescriptionForFirstMatch(value);
if (issueDescription) {
return {
id: this.id,
severity: this.severity,
path: ['apple', 'info', locale, property],
message: issueDescription,
};
}
return null;
}).filter(truthy)
)
.filter(truthy)
.flat();
},
};
function getDescriptionForFirstMatch(value: string): string | null {
const sanitized = value.toLowerCase();
for (const [word, description] of Object.entries(RESTRICTED_WORDS)) {
if (sanitized.includes(word)) {
return description;
}
}
return null;
}
function getStringValue(value?: null | string | string[]): string {
if (!value) {
return '';
}
if (Array.isArray(value)) {
return value.join(' ');
}
return value;
}
@@ -1,4 +1,3 @@
import { MetadataContext } from '../../context';
import { AppleTask } from '../task';
import { AgeRatingTask } from './age-rating';
import { AppInfoTask } from './app-info';
@@ -12,10 +11,7 @@ type AppleTaskOptions = {
/**
* List of all eligible tasks to sync local store configuration to the App store.
*/
export function createAppleTasks(
_ctx: MetadataContext,
{ version }: AppleTaskOptions = {}
): AppleTask[] {
export function createAppleTasks({ version }: AppleTaskOptions = {}): AppleTask[] {
return [
new AppVersionTask({ version }),
new AppInfoTask(),
+58
View File
@@ -0,0 +1,58 @@
import { App, Session } from '@expo/apple-utils';
import { ExpoConfig } from '@expo/config';
import { SubmitProfile } from '@expo/eas-json';
import assert from 'assert';
import { CredentialsContext } from '../credentials/context';
import { getRequestContext } from '../credentials/ios/appstore/authenticate';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
export type MetadataAppStoreAuthentication = {
/** The root entity of the App store */
app: App;
/** The authentication state we used to fetch the root entity */
auth: Partial<Session.AuthState>;
};
/**
* Resolve the bundle identifier from the selected submit profile.
* This bundle identifier is used as target for the metadata submission.
*/
async function resolveAppStoreBundleIdentifierAsync(
projectDir: string,
profile: SubmitProfile,
exp: ExpoConfig
): Promise<string> {
if ('bundleIdentifier' in profile) {
return profile.bundleIdentifier ?? (await getBundleIdentifierAsync(projectDir, exp));
}
return await getBundleIdentifierAsync(projectDir, exp);
}
/**
* To start syncing ASC entities, we need access to the apple utils App instance.
* This resolves both the authentication and that App instance.
*/
export async function getAppStoreAuthAsync({
projectDir,
profile,
exp,
credentialsCtx,
}: {
projectDir: string;
profile: SubmitProfile;
exp: ExpoConfig;
credentialsCtx: CredentialsContext;
}): Promise<MetadataAppStoreAuthentication> {
const bundleId = await resolveAppStoreBundleIdentifierAsync(projectDir, profile, exp);
const authCtx = await credentialsCtx.appStore.ensureAuthenticatedAsync();
assert(authCtx.authState, 'Failed to authenticate with App Store Connect');
// TODO: improve error handling by mentioning possible configuration errors
const app = await App.findAsync(getRequestContext(authCtx), { bundleId });
assert(app, `Failed to load app "${bundleId}" from App Store Connect`);
return { app, auth: authCtx.authState };
}
@@ -0,0 +1,24 @@
import { MetadataConfig } from './schema';
export enum IssueSeverity {
info = 0,
warn = 1,
error = 2,
}
export interface Issue {
/** The type of issue, auto-generated for AJV errors based on the validation type */
id: string;
/** The severity of the lint message */
severity: IssueSeverity;
/** The data path, using segments, to the source of this message */
path: string[];
/** A human readable description of the issue, presented to users */
message: string;
}
export interface IssueRule<Data = MetadataConfig> {
id: string;
severity: IssueSeverity;
validate(config: Data): null | Issue | Issue[];
}
@@ -1,19 +1,13 @@
import Ajv from 'ajv';
import { SubmitProfile } from '@expo/eas-json';
import assert from 'assert';
import fs from 'fs-extra';
import path from 'path';
import { AppleConfigReader } from './apple/config/reader';
import { AppleConfigWriter } from './apple/config/writer';
import { AppleMetadata } from './apple/types';
import { MetadataValidationError } from './errors';
export interface MetadataConfig {
/** The store configuration version */
configVersion: number;
/** All App Store related configuration */
apple?: AppleMetadata;
}
import { AppleConfigReader } from '../apple/config/reader';
import { AppleConfigWriter } from '../apple/config/writer';
import { MetadataValidationError } from '../errors';
import { MetadataConfig } from './schema';
import { validateConfig } from './validate';
/**
* Resolve the dynamic config from the user.
@@ -27,18 +21,30 @@ async function resolveDynamicConfigAsync(configFile: string): Promise<unknown> {
: userConfigOrFunction;
}
/**
* Resolve the prefered store config file name from the submit profile.
* This is relative to the project directory, and uses `store.config.json` by default.
*/
function resolveConfigFilePath(profile: SubmitProfile): string {
if ('metadataPath' in profile) {
return profile.metadataPath ?? 'store.config.json';
}
return 'store.config.json';
}
/**
* Get the static configuration file path, based on the metadata context.
* This uses any custom name provided, but swaps out the extension for `.json`.
*/
export function getStaticConfigFilePath({
projectDir,
metadataPath,
profile,
}: {
projectDir: string;
metadataPath: string;
profile: SubmitProfile;
}): string {
const configFile = path.join(projectDir, metadataPath);
const configFile = path.join(projectDir, resolveConfigFilePath(profile));
const configExtension = path.extname(configFile);
return path.join(projectDir, `${path.basename(configFile, configExtension)}.json`);
@@ -52,14 +58,14 @@ export function getStaticConfigFilePath({
*/
export async function loadConfigAsync({
projectDir,
metadataPath,
profile,
skipValidation = false,
}: {
projectDir: string;
metadataPath: string;
profile: SubmitProfile;
skipValidation?: boolean;
}): Promise<MetadataConfig> {
const configFile = path.join(projectDir, metadataPath);
const configFile = path.join(projectDir, resolveConfigFilePath(profile));
if (!(await fs.pathExists(configFile))) {
throw new MetadataValidationError(`Metadata store config file not found: "${configFile}"`);
}
@@ -67,37 +73,20 @@ export async function loadConfigAsync({
const configData = await resolveDynamicConfigAsync(configFile);
if (!skipValidation) {
const { valid, errors: validationErrors } = validateConfig(configData);
const issues = validateConfig(configData);
if (!valid) {
throw new MetadataValidationError(`Metadata store config errors found`, validationErrors);
if (issues.length > 0) {
throw new MetadataValidationError(`Metadata store config errors found`, issues);
}
}
return configData as MetadataConfig;
}
/**
* Run the JSON Schema validation to normalize defaults and flag early config errors.
* This includes validating the known store limitations for every configurable property.
*/
export function validateConfig(config: unknown): {
valid: boolean;
errors: Ajv.ErrorObject[];
} {
const validator = new Ajv({ allErrors: true, useDefaults: true })
.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'))
.compile(require('../../schema/metadata-0.json'));
const valid = validator(config) as boolean;
return { valid, errors: validator.errors || [] };
}
/** Create a versioned deserializer to fetch App Store data from the store configuration. */
export function createAppleReader(config: MetadataConfig): AppleConfigReader {
assert(config.configVersion === 0, 'Unsupported store configuration version');
assert(config.apple, 'No apple configuration found');
assert(config.apple !== undefined, 'No apple configuration found');
return new AppleConfigReader(config.apple);
}
@@ -0,0 +1,8 @@
import { AppleMetadata } from '../apple/types';
export interface MetadataConfig {
/** The store configuration version */
configVersion: number;
/** All App Store related configuration */
apple?: AppleMetadata;
}
@@ -0,0 +1,51 @@
import { appleRules } from '../apple/rules';
import { createValidator, getReadableErrors } from '../utils/ajv';
import { Issue, IssueRule, IssueSeverity } from './issue';
import { MetadataConfig } from './schema';
const metadataSchema = require('../../../schema/metadata-0.json');
/** Validate the user-provided validation for issues */
export function validateConfig(config: unknown): Issue[] {
const validator = createValidator().compile(metadataSchema);
validator(config);
const issues: Issue[] = getReadableErrors(validator.errors ?? []).map(error => {
const path = error.path === '$' ? [] : error.path.substring(2).split('.');
const id = error.original?.keyword ?? 'unknown';
return {
id: `json-schema.${id}`,
path,
severity: IssueSeverity.error,
message: error.message,
};
});
try {
issues.push(...validateRules(appleRules, config as MetadataConfig));
} catch {
// When the rules are failing, the json schema validation errors explain the issue
// TODO(cedric): optionally add debugging logging for these types of errors
}
return issues;
}
/** Validate the set of rules against the parsed metadata config */
function validateRules(rules: IssueRule[], config: MetadataConfig): Issue[] {
const issues: Issue[] = [];
for (const rule of rules) {
const result = rule.validate(config);
if (Array.isArray(result)) {
issues.push(...result);
} else if (result) {
issues.push(result);
}
}
return issues;
}
-93
View File
@@ -1,93 +0,0 @@
import { App, Session } from '@expo/apple-utils';
import { ExpoConfig } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { EasJsonAccessor, EasJsonUtils, SubmitProfile } from '@expo/eas-json';
import assert from 'assert';
import { Analytics } from '../analytics/AnalyticsManager';
import { CredentialsContext } from '../credentials/context';
import { getRequestContext } from '../credentials/ios/appstore/authenticate';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
import { Actor } from '../user/User';
export type MetadataContext = {
/** Submission profile platform to use */
platform: Platform.IOS;
/** Resolved submission profile configuration */
profile: SubmitProfile<Platform.IOS>;
/** Configured store config path, relative from projectDir (defaults to store.config.json) */
metadataPath: string;
/** Authenticated Expo account */
user: Actor;
/** The store credentials manager */
credentialsCtx: CredentialsContext;
/** The analytics manager for EAS cli */
analytics: Analytics;
/** The app bundle identifier to use for the store */
bundleIdentifier: string;
/** Root of the Expo project directory */
projectDir: string;
/** Resolved Expo app configuration */
exp: ExpoConfig;
};
export type MetadataAppStoreAuthentication = {
/** The root entity of the App store */
app: App;
/** The authentication state we used to fetch the root entity */
auth: Partial<Session.AuthState>;
};
/**
* Metadata uses the submission profile to find the configured metadata filename.
* Note, only iOS is supported for metadata right now.
*/
export async function createMetadataContextAsync(params: {
projectDir: string;
credentialsCtx: CredentialsContext;
analytics: Analytics;
exp: ExpoConfig;
profileName?: string;
}): Promise<MetadataContext> {
const easJsonAccessor = new EasJsonAccessor(params.projectDir);
const submitProfile = await EasJsonUtils.getSubmitProfileAsync(
easJsonAccessor,
Platform.IOS,
params.profileName
);
const exp = params.exp;
const user = params.credentialsCtx.user;
const bundleIdentifier =
submitProfile.bundleIdentifier ?? (await getBundleIdentifierAsync(params.projectDir, exp));
return {
platform: Platform.IOS,
profile: submitProfile,
metadataPath: submitProfile.metadataPath ?? 'store.config.json',
user,
analytics: params.analytics,
credentialsCtx: params.credentialsCtx,
bundleIdentifier,
projectDir: params.projectDir,
exp,
};
}
/**
* To start syncing ASC entities, we need access to the apple utils App instance.
* This ensures we are authenticated and have the app instance ready to use.
*/
export async function ensureMetadataAppStoreAuthenticatedAsync({
credentialsCtx,
bundleIdentifier,
}: MetadataContext): Promise<MetadataAppStoreAuthentication> {
const authCtx = await credentialsCtx.appStore.ensureAuthenticatedAsync();
assert(authCtx.authState, 'Failed to authenticate with App Store Connect');
// TODO: improve error handling by mentioning possible configuration errors
const app = await App.findAsync(getRequestContext(authCtx), { bundleId: bundleIdentifier });
assert(app, `Failed to load app "${bundleIdentifier}" from App Store Connect`);
return { app, auth: authCtx.authState };
}
+31 -10
View File
@@ -1,13 +1,16 @@
import { ExpoConfig } from '@expo/config-types';
import { SubmitProfile } from '@expo/eas-json';
import fs from 'fs-extra';
import path from 'path';
import { MetadataEvent } from '../analytics/AnalyticsManager';
import { Analytics, MetadataEvent } from '../analytics/AnalyticsManager';
import { CredentialsContext } from '../credentials/context';
import Log from '../log';
import { confirmAsync } from '../prompts';
import { AppleData } from './apple/data';
import { createAppleTasks } from './apple/tasks';
import { createAppleWriter, getStaticConfigFilePath } from './config';
import { MetadataContext, ensureMetadataAppStoreAuthenticatedAsync } from './context';
import { getAppStoreAuthAsync } from './auth';
import { createAppleWriter, getStaticConfigFilePath } from './config/resolve';
import { MetadataDownloadError, MetadataValidationError } from './errors';
import { subscribeTelemetry } from './utils/telemetry';
@@ -15,12 +18,24 @@ import { subscribeTelemetry } from './utils/telemetry';
* Generate a local store configuration from the stores.
* Note, only App Store is supported at this time.
*/
export async function downloadMetadataAsync(metadataCtx: MetadataContext): Promise<string> {
const filePath = getStaticConfigFilePath(metadataCtx);
const fileExists = await fs.pathExists(filePath);
export async function downloadMetadataAsync({
projectDir,
profile,
exp,
analytics,
credentialsCtx,
}: {
projectDir: string;
profile: SubmitProfile;
exp: ExpoConfig;
analytics: Analytics;
credentialsCtx: CredentialsContext;
}): Promise<string> {
const filePath = getStaticConfigFilePath({ projectDir, profile });
const fileExists = await fs.pathExists(filePath);
if (fileExists) {
const filePathRelative = path.relative(metadataCtx.projectDir, filePath);
const filePathRelative = path.relative(projectDir, filePath);
const overwrite = await confirmAsync({
message: `Do you want to overwrite the existing "${filePathRelative}"?`,
});
@@ -29,9 +44,15 @@ export async function downloadMetadataAsync(metadataCtx: MetadataContext): Promi
}
}
const { app, auth } = await ensureMetadataAppStoreAuthenticatedAsync(metadataCtx);
const { app, auth } = await getAppStoreAuthAsync({
exp,
credentialsCtx,
projectDir,
profile,
});
const { unsubscribeTelemetry, executionId } = subscribeTelemetry(
metadataCtx.analytics,
analytics,
MetadataEvent.APPLE_METADATA_DOWNLOAD,
{ app, auth }
);
@@ -41,7 +62,7 @@ export async function downloadMetadataAsync(metadataCtx: MetadataContext): Promi
const errors: Error[] = [];
const config = createAppleWriter();
const tasks = createAppleTasks(metadataCtx);
const tasks = createAppleTasks();
const taskCtx = { app };
for (const task of tasks) {
+5 -3
View File
@@ -1,7 +1,7 @@
import type { ErrorObject } from 'ajv';
import chalk from 'chalk';
import Log, { link } from '../log';
import { Issue } from './config/issue';
/**
* Before syncing data to the ASC API, we need to validate the metadata config.
@@ -9,7 +9,7 @@ import Log, { link } from '../log';
* and should contain useful information for the user to solve before trying again.
*/
export class MetadataValidationError extends Error {
public constructor(message?: string, public readonly errors: ErrorObject[] = []) {
public constructor(message?: string, public readonly errors: Issue[] = []) {
super(message ?? 'Store configuration validation failed');
}
}
@@ -55,7 +55,9 @@ export function logMetadataValidationError(error: MetadataValidationError): void
Log.error(chalk.bold(error.message));
if (error.errors?.length > 0) {
// TODO(cedric): group errors by property to make multiple errors for same property more readable
Log.log(error.errors.map(err => ` - ${chalk.bold(err.dataPath)} ${err.message}`).join('\n'));
for (const err of error.errors) {
Log.log(` - ${chalk.bold(`$.${err.path.join('.')}`)} ${err.message}`);
}
}
}
+35 -13
View File
@@ -1,10 +1,15 @@
import { MetadataEvent } from '../analytics/AnalyticsManager';
import { ExpoConfig } from '@expo/config-types';
import { SubmitProfile } from '@expo/eas-json';
import { Analytics, MetadataEvent } from '../analytics/AnalyticsManager';
import { CredentialsContext } from '../credentials/context';
import Log from '../log';
import { confirmAsync } from '../prompts';
import { AppleData } from './apple/data';
import { createAppleTasks } from './apple/tasks';
import { MetadataConfig, createAppleReader, loadConfigAsync } from './config';
import { MetadataContext, ensureMetadataAppStoreAuthenticatedAsync } from './context';
import { getAppStoreAuthAsync } from './auth';
import { createAppleReader, loadConfigAsync } from './config/resolve';
import { MetadataConfig } from './config/schema';
import { MetadataUploadError, MetadataValidationError, logMetadataValidationError } from './errors';
import { subscribeTelemetry } from './utils/telemetry';
@@ -12,13 +17,29 @@ import { subscribeTelemetry } from './utils/telemetry';
* Sync a local store configuration with the stores.
* Note, only App Store is supported at this time.
*/
export async function uploadMetadataAsync(
metadataCtx: MetadataContext
): Promise<{ appleLink: string }> {
const storeConfig = await loadConfigWithValidationPromptAsync(metadataCtx);
const { app, auth } = await ensureMetadataAppStoreAuthenticatedAsync(metadataCtx);
export async function uploadMetadataAsync({
projectDir,
profile,
exp,
analytics,
credentialsCtx,
}: {
projectDir: string;
profile: SubmitProfile;
exp: ExpoConfig;
analytics: Analytics;
credentialsCtx: CredentialsContext;
}): Promise<{ appleLink: string }> {
const storeConfig = await loadConfigWithValidationPromptAsync(projectDir, profile);
const { app, auth } = await getAppStoreAuthAsync({
exp,
credentialsCtx,
projectDir,
profile,
});
const { unsubscribeTelemetry, executionId } = subscribeTelemetry(
metadataCtx.analytics,
analytics,
MetadataEvent.APPLE_METADATA_UPLOAD,
{ app, auth }
);
@@ -28,7 +49,7 @@ export async function uploadMetadataAsync(
const errors: Error[] = [];
const config = createAppleReader(storeConfig);
const tasks = createAppleTasks(metadataCtx, {
const tasks = createAppleTasks({
// We need to resolve a different version as soon as possible.
// This version is the parent model of all changes we are going to push.
version: config.getVersion()?.versionString,
@@ -62,10 +83,11 @@ export async function uploadMetadataAsync(
}
async function loadConfigWithValidationPromptAsync(
metadataCtx: MetadataContext
projectDir: string,
profile: SubmitProfile
): Promise<MetadataConfig> {
try {
return await loadConfigAsync(metadataCtx);
return await loadConfigAsync({ projectDir, profile });
} catch (error) {
if (error instanceof MetadataValidationError) {
logMetadataValidationError(error);
@@ -75,7 +97,7 @@ async function loadConfigWithValidationPromptAsync(
);
if (await confirmAsync({ message: 'Do you still want to push the store configuration?' })) {
return await loadConfigAsync({ ...metadataCtx, skipValidation: true });
return await loadConfigAsync({ projectDir, profile, skipValidation: true });
}
}
@@ -0,0 +1,29 @@
import { AggregateAjvError, HumanError } from '@segment/ajv-human-errors';
import Ajv, { ErrorObject } from 'ajv';
import addFormats from 'ajv-formats';
const jsonSchema = require('ajv/lib/refs/json-schema-draft-06.json');
/**
* Create a new AJV validator using the JSON Schema 06 draft.
* It also adds the additional formats from the `ajv-formats` package.
*
* @see https://github.com/ajv-validator/ajv-formats
*/
export function createValidator(): Ajv {
const validator = new Ajv({
strict: false, // The metadata schema is shared with vscode, including vscode-only properties
verbose: true, // Required for `ajv-human-errors`
allErrors: true, // Required for `ajv-human-errors`
});
return addFormats(validator).addMetaSchema(jsonSchema);
}
export function getReadableErrors(errors: ErrorObject[] = []): HumanError[] {
if (errors.length === 0) {
return [];
}
return new AggregateAjvError(errors).toJSON();
}
@@ -48,11 +48,11 @@ export function subscribeTelemetry(
type: 'response',
phase: 'rejected',
method: error.request.method.toUpperCase(),
url: scrubber(error.config.url),
url: scrubber(error.config?.url),
error: scrubber(error.message),
status: String(error.response?.status),
statusText: scrubber(error.response?.statusText),
input: scrubber(error.config.data),
input: scrubber(error.config?.data),
output: scrubber(error.response?.data),
});
@@ -0,0 +1,76 @@
import { getPackageJson } from '@expo/config';
import { ExpoConfig } from '@expo/config-types';
import * as projectUtils from '../projectUtils';
jest.mock('@expo/config', () => ({
getPackageJson: jest.fn(),
}));
describe(projectUtils.isEASUpdateConfigured, () => {
describe('expo-updates is not installed', () => {
it('returns false if updates.url is not set', () => {
jest.mocked(getPackageJson).mockReturnValue({});
const exp: ExpoConfig = {
name: 'blah',
slug: 'blah',
};
expect(projectUtils.isEASUpdateConfigured('/path/to/project', exp)).toBe(false);
});
it('returns false if updates.url is set', () => {
jest.mocked(getPackageJson).mockReturnValue({});
const exp: ExpoConfig = {
name: 'blah',
slug: 'blah',
updates: { url: 'http://sokal.dev/todo' },
};
expect(projectUtils.isEASUpdateConfigured('/path/to/project', exp)).toBe(false);
});
it('returns true if updates.url is set (for SDK < 44)', () => {
jest.mocked(getPackageJson).mockReturnValue({});
const exp: ExpoConfig = {
name: 'blah',
slug: 'blah',
sdkVersion: '43.0.0',
updates: { url: 'http://sokal.dev/todo' },
};
expect(projectUtils.isEASUpdateConfigured('/path/to/project', exp)).toBe(true);
});
});
describe('expo-updates is installed', () => {
it('returns false if updates.url is not set', () => {
jest.mocked(getPackageJson).mockReturnValue({
dependencies: {
'expo-updates': '1.2.3',
},
});
const exp: ExpoConfig = {
name: 'blah',
slug: 'blah',
};
expect(projectUtils.isEASUpdateConfigured('/path/to/project', exp)).toBe(false);
});
it('returns true if updates.url is set', () => {
jest.mocked(getPackageJson).mockReturnValue({
dependencies: {
'expo-updates': '1.2.3',
},
});
const exp: ExpoConfig = {
name: 'blah',
slug: 'blah',
updates: { url: 'http://sokal.dev/todo' },
};
expect(projectUtils.isEASUpdateConfigured('/path/to/project', exp)).toBe(true);
});
});
});
@@ -1,17 +1,17 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { vol } from 'memfs';
import { resolveWorkflowAsync } from '../workflow';
import { resolveWorkflowAsync, resolveWorkflowPerPlatformAsync } from '../workflow';
jest.mock('fs');
const projectDir = '/app';
describe(resolveWorkflowAsync, () => {
beforeEach(() => {
vol.reset();
});
const projectDir = '/app';
test('bare workflow for both platforms', async () => {
vol.fromJSON(
{
@@ -57,3 +57,24 @@ describe(resolveWorkflowAsync, () => {
await expect(resolveWorkflowAsync(projectDir, Platform.IOS)).resolves.toBe(Workflow.MANAGED);
});
});
describe(resolveWorkflowPerPlatformAsync, () => {
beforeEach(() => {
vol.reset();
});
test('returns the correct combined value', async () => {
vol.fromJSON(
{
'./ios/helloworld.xcodeproj/project.pbxproj': 'fake',
'./android/app/src/main/AndroidManifest.xml': 'fake',
},
projectDir
);
await expect(resolveWorkflowPerPlatformAsync(projectDir)).resolves.toEqual({
android: Workflow.GENERIC,
ios: Workflow.GENERIC,
});
});
});
+18 -8
View File
@@ -64,18 +64,19 @@ export function isExpoUpdatesInstalled(projectDir: string): boolean {
return !!(packageJson.dependencies && 'expo-updates' in packageJson.dependencies);
}
export function isExpoUpdatesInstalledOrAvailable(
projectDir: string,
sdkVersion?: string
): boolean {
export function isExpoUpdatesInstalledOrAvailable(projectDir: string, exp: ExpoConfig): boolean {
// before sdk 44, expo-updates was included in with the expo module
if (sdkVersion && semver.lt(sdkVersion, '44.0.0')) {
if (exp.sdkVersion && semver.lt(exp.sdkVersion, '44.0.0')) {
return true;
}
return isExpoUpdatesInstalled(projectDir);
}
export function isEASUpdateConfigured(projectDir: string, exp: ExpoConfig): boolean {
return isExpoUpdatesInstalledOrAvailable(projectDir, exp) && exp.updates?.url !== undefined;
}
export async function validateAppVersionRuntimePolicySupportAsync(
projectDir: string,
exp: ExpoConfig
@@ -97,10 +98,19 @@ export async function validateAppVersionRuntimePolicySupportAsync(
);
}
export async function installExpoUpdatesAsync(projectDir: string): Promise<void> {
export async function installExpoUpdatesAsync(
projectDir: string,
options?: { silent: boolean }
): Promise<void> {
Log.log(chalk.gray`> npx expo install expo-updates`);
await expoCommandAsync(projectDir, ['install', 'expo-updates']);
Log.newLine();
try {
await expoCommandAsync(projectDir, ['install', 'expo-updates'], { silent: options?.silent });
} catch (error: any) {
if (options?.silent) {
Log.error('stdout' in error ? error.stdout : error.message);
}
throw error;
}
}
export async function getOwnerAccountForProjectIdAsync(
+16 -9
View File
@@ -13,7 +13,7 @@ import { PublishMutation } from '../graphql/mutations/PublishMutation';
import { PresignedPost } from '../graphql/mutations/UploadSessionMutation';
import { PublishQuery } from '../graphql/queries/PublishQuery';
import { uploadWithPresignedPostWithRetryAsync } from '../uploads';
import { expoCommandAsync } from '../utils/expoCli';
import { expoCommandAsync, shouldUseVersionedExpoCLI } from '../utils/expoCli';
import chunk from '../utils/expodash/chunk';
import uniqBy from '../utils/expodash/uniqBy';
@@ -148,23 +148,30 @@ export async function buildUnsortedUpdateInfoGroupAsync(
export async function buildBundlesAsync({
projectDir,
inputDir,
exp,
}: {
projectDir: string;
inputDir: string;
exp: Pick<ExpoConfig, 'sdkVersion'>;
}): Promise<void> {
const packageJSON = JsonFile.read(path.resolve(projectDir, 'package.json'));
if (!packageJSON) {
throw new Error('Could not locate package.json');
}
await expoCommandAsync(projectDir, [
'export',
'--output-dir',
inputDir,
'--experimental-bundle',
'--non-interactive',
'--dump-sourcemap',
]);
if (shouldUseVersionedExpoCLI(projectDir, exp)) {
await expoCommandAsync(projectDir, ['export', '--output-dir', inputDir, '--dump-sourcemap']);
} else {
// Legacy global Expo CLI
await expoCommandAsync(projectDir, [
'export',
'--output-dir',
inputDir,
'--experimental-bundle',
'--non-interactive',
'--dump-sourcemap',
]);
}
}
export async function resolveInputDirectoryAsync(customInputDirectory: string): Promise<string> {
+10
View File
@@ -34,3 +34,13 @@ export async function resolveWorkflowAsync(
}
return Workflow.MANAGED;
}
export async function resolveWorkflowPerPlatformAsync(
projectDir: string
): Promise<Record<Platform, Workflow>> {
const [android, ios] = await Promise.all([
resolveWorkflowAsync(projectDir, Platform.ANDROID),
resolveWorkflowAsync(projectDir, Platform.IOS),
]);
return { android, ios };
}
+5
View File
@@ -0,0 +1,5 @@
import Log from '../../log';
export async function runAppOnAndroidEmulatorAsync(_appPath: string): Promise<void> {
Log.error('Not implemented!');
}
+37
View File
@@ -0,0 +1,37 @@
import spawnAsync from '@expo/spawn-async';
import path from 'path';
import * as simulator from './simulator';
import { validateSystemRequirementsAsync } from './systemRequirements';
export async function runAppOnIosSimulatorAsync(appPath: string): Promise<void> {
await validateSystemRequirementsAsync();
const selectedSimulator = await simulator.selectSimulatorAsync();
await simulator.ensureSimulatorBootedAsync(selectedSimulator);
await simulator.ensureSimulatorAppOpenedAsync(selectedSimulator.udid);
const bundleIdentifier = await getAppBundleIdentifierAsync(appPath);
await simulator.installAppAsync(selectedSimulator.udid, appPath);
await simulator.launchAppAsync(selectedSimulator.udid, bundleIdentifier);
}
async function getAppBundleIdentifierAsync(appPath: string): Promise<string> {
const { stdout, stderr } = await spawnAsync('xcrun', [
'plutil',
'-extract',
'CFBundleIdentifier',
'raw',
path.join(appPath, 'Info.plist'),
]);
if (!stdout) {
throw new Error(
`Could not read app bundle identifier from ${path.join(appPath, 'Info.plist')}: ${stderr}`
);
}
return stdout.trim();
}
+7
View File
@@ -0,0 +1,7 @@
import { SpawnOptions, SpawnResult } from '@expo/spawn-async';
import { xcrunAsync } from './xcrun';
export async function simctlAsync(args: string[], options?: SpawnOptions): Promise<SpawnResult> {
return xcrunAsync(['simctl', ...args], options);
}
+197
View File
@@ -0,0 +1,197 @@
import * as osascript from '@expo/osascript';
import spawnAsync from '@expo/spawn-async';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { sleepAsync } from '../../utils/promise';
import { simctlAsync } from './simctl';
interface IosSimulator {
runtime: string;
osVersion: string;
windowName: string;
osType: 'iOS';
state: 'Booted' | 'Shutdown';
isAvailable: boolean;
name: string;
udid: string;
lastBootedAt?: Date;
}
export async function selectSimulatorAsync(): Promise<IosSimulator> {
const bootedSimulator = await getFirstBootedIosSimulatorAsync();
if (bootedSimulator) {
return bootedSimulator;
}
const simulators = await getAvaliableIosSimulatorsListAsync();
Log.newLine();
const { selectedSimulator } = await promptAsync({
type: 'select',
message: `Select a simulator to run your app on`,
name: 'selectedSimulator',
choices: simulators.map(simulator => ({
title: `ios ${simulator.osVersion} ${simulator.name}`,
value: simulator,
})),
});
return selectedSimulator;
}
export async function getFirstBootedIosSimulatorAsync(): Promise<IosSimulator | undefined> {
const bootedSimulators = await getAvaliableIosSimulatorsListAsync('booted');
if (bootedSimulators.length > 0) {
return bootedSimulators[0];
}
return undefined;
}
export async function getAvaliableIosSimulatorsListAsync(query?: string): Promise<IosSimulator[]> {
const { stdout } = query
? await simctlAsync(['list', 'devices', '--json', query])
: await simctlAsync(['list', 'devices', '--json']);
const info = parseSimControlJsonResults(stdout);
const iosSimulators = [];
for (const runtime of Object.keys(info.devices)) {
// Given a string like 'com.apple.CoreSimulator.SimRuntime.tvOS-13-4'
const runtimeSuffix = runtime.split('com.apple.CoreSimulator.SimRuntime.').pop();
if (!runtimeSuffix) {
continue;
}
// Create an array [tvOS, 13, 4]
const [osType, ...osVersionComponents] = runtimeSuffix.split('-');
if (osType === 'iOS') {
// Join the end components [13, 4] -> '13.4'
const osVersion = osVersionComponents.join('.');
const sims = info.devices[runtime];
for (const device of sims) {
if (device.isAvailable) {
iosSimulators.push({
...device,
runtime,
osVersion,
windowName: `${device.name} (${osVersion})`,
osType: 'iOS' as const,
state: device.state as 'Booted' | 'Shutdown',
lastBootedAt: device.lastBootedAt ? new Date(device.lastBootedAt) : undefined,
});
}
}
}
}
return iosSimulators;
}
function parseSimControlJsonResults(input: string): any {
try {
return JSON.parse(input);
} catch (error: any) {
// Nov 15, 2020: Observed this can happen when opening the simulator and the simulator prompts the user to update the xcode command line tools.
// Unexpected token I in JSON at position 0
if (error.message.includes('Unexpected token')) {
Log.error(`Apple's simctl returned malformed JSON:\n${input}`);
}
throw error;
}
}
export async function ensureSimulatorBootedAsync(simulator: IosSimulator): Promise<void> {
if (simulator.state === 'Booted') {
return;
}
await simctlAsync(['boot', simulator.udid]);
}
export async function openSimulatorAppAsync(simulatorUdid: string): Promise<void> {
const args = ['-a', 'Simulator'];
if (simulatorUdid) {
// This has no effect if the app is already running.
args.push('--args', '-CurrentDeviceUDID', simulatorUdid);
}
await spawnAsync('open', args);
}
export async function launchAppAsync(
simulatorUdid: string,
bundleIdentifier: string
): Promise<void> {
Log.newLine();
Log.log('Launching your app...');
await simctlAsync(['launch', simulatorUdid, bundleIdentifier]);
Log.succeed('Successfully launched your app!');
}
// I think the app can be open while no simulators are booted.
async function waitForSimulatorAppToStartAsync(
maxWaitTimeMs: number,
intervalMs: number
): Promise<void> {
Log.newLine();
Log.log('Waiting for Simulator app to start...');
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTimeMs) {
if (await isSimulatorAppRunningAsync()) {
return;
}
await sleepAsync(Math.min(intervalMs, Math.max(maxWaitTimeMs - (Date.now() - startTime), 0)));
}
throw new Error('Timed out waiting for the iOS simulator to start.');
}
async function isSimulatorAppRunningAsync(): Promise<boolean> {
try {
const result = await osascript.execAsync(
'tell app "System Events" to count processes whose name is "Simulator"'
);
if (result.trim() === '0') {
return false;
}
} catch (error: any) {
if (error.message.includes('Application isn’t running')) {
return false;
}
throw error;
}
return true;
}
export async function ensureSimulatorAppOpenedAsync(simulatorUuid: string): Promise<void> {
if (await isSimulatorAppRunningAsync()) {
return;
}
await openSimulatorAppAsync(simulatorUuid);
await waitForSimulatorAppToStartAsync(60 * 1000, 1000);
}
export async function installAppAsync(deviceId: string, filePath: string): Promise<void> {
Log.newLine();
Log.log('Installing your app on the simulator...');
await simctlAsync(['install', deviceId, filePath]);
Log.succeed('Successfully installed your app on the simulator!');
}
export async function getSimulatorAppIdAsync(): Promise<string | undefined> {
try {
return (await osascript.execAsync('id of app "Simulator"')).trim();
} catch {
return undefined;
}
}
@@ -0,0 +1,102 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import semver from 'semver';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { getSimulatorAppIdAsync } from './simulator';
import * as xcode from './xcode';
import { installXcrunAsync, isXcrunInstalledAsync } from './xcrun';
function assertPlatform(): void {
if (process.platform !== 'darwin') {
Log.error('iOS simulator apps can only be run on macOS devices.');
throw Error('iOS simulator apps can only be run on macOS devices.');
}
}
async function assertCorrectXcodeVersionInstalledAsync(): Promise<void> {
const xcodeVersion = await xcode.getXcodeVersionAsync();
if (!xcodeVersion) {
const { goToAppStore } = await promptAsync({
type: 'select',
message: 'Xcode needs to be installed, would you like to continue to the App Store?',
name: 'goToAppStore',
choices: [
{ title: 'Yes', value: true },
{ title: 'No', value: false },
],
});
if (goToAppStore) {
await xcode.openAppStoreAsync(xcode.APP_STORE_ID);
}
throw Error('Please try again once Xcode is installed');
}
if (semver.lt(xcodeVersion, xcode.MIN_XCODE_VERSION)) {
throw Error(
`Xcode version ${chalk.bold(xcodeVersion)} is too old. Please upgrade to version ${chalk.bold(
xcode.MIN_XCODE_VERSION
)} or higher.`
);
}
}
async function ensureXcrunInstalledAsync(): Promise<void> {
if (!isXcrunInstalledAsync()) {
const { installXcrun } = await promptAsync({
type: 'select',
message: 'Xcode Command Line Tools need to be installed, continue?',
name: 'installXcrun',
choices: [
{ title: 'Yes', value: true },
{ title: 'No', value: false },
],
});
if (installXcrun) {
await installXcrunAsync();
return;
}
throw Error('Please try again once Xcode Command Line Tools are installed');
}
}
async function assertSimulatorAppInstalledAsync(): Promise<void> {
const simulatorAppId = await getSimulatorAppIdAsync();
if (!simulatorAppId) {
throw new Error(
`Can't determine id of Simulator app; the Simulator is most likely not installed on this machine. Run 'sudo xcode-select -s /Applications/Xcode.app'`
);
}
if (
simulatorAppId !== 'com.apple.iphonesimulator' &&
simulatorAppId !== 'com.apple.CoreSimulator.SimulatorTrampoline'
) {
throw new Error(
`Simulator is installed but is identified as '${simulatorAppId}', can't recognize what that is`
);
}
try {
// make sure we can run simctl
await spawnAsync('xcrun', ['simctl', 'help']);
} catch (error: any) {
Log.warn(`Unable to run simctl:\n${error.toString()}`);
throw new Error(
'xcrun is not configured correctly. Ensure `sudo xcode-select --reset` works before running this command again.'
);
}
}
export async function validateSystemRequirementsAsync(): Promise<void> {
assertPlatform();
await assertCorrectXcodeVersionInstalledAsync();
await ensureXcrunInstalledAsync();
await assertSimulatorAppInstalledAsync();
}
+46
View File
@@ -0,0 +1,46 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import semver from 'semver';
import Log from '../../log';
// Based on the RN docs (Aug 2020).
export const MIN_XCODE_VERSION = '9.4.0';
export const APP_STORE_ID = '497799835';
export async function getXcodeVersionAsync(): Promise<string | undefined> {
try {
const { stdout } = await spawnAsync('xcodebuild', ['-version']);
const version = stdout.match(/Xcode (\d+\.\d+)/)?.[1];
const semverFormattedVersion = `${version}.0`;
if (!semver.valid(semverFormattedVersion)) {
Log.warn(
`Xcode version ${chalk.bold(version)} is in unknown format. Expected format is ${chalk.bold(
'12.0'
)}.`
);
return undefined;
}
return semverFormattedVersion;
} catch {
// not installed
return undefined;
}
}
export async function openAppStoreAsync(appId: string): Promise<void> {
const link = getAppStoreLink(appId);
await spawnAsync(`open`, [link]);
}
function getAppStoreLink(appId: string): string {
if (process.platform === 'darwin') {
// TODO: Is there ever a case where the macappstore isn't available on mac?
return `macappstore://itunes.apple.com/app/id${appId}`;
}
return `https://apps.apple.com/us/app/id${appId}`;
}
+77
View File
@@ -0,0 +1,77 @@
import spawnAsync, { SpawnOptions, SpawnResult } from '@expo/spawn-async';
import chalk from 'chalk';
import Log from '../../log';
import { sleepAsync } from '../../utils/promise';
export async function xcrunAsync(args: string[], options?: SpawnOptions): Promise<SpawnResult> {
try {
return await spawnAsync('xcrun', args, options);
} catch (e) {
throwXcrunError(e);
}
}
function throwXcrunError(e: any): never {
if (isLicenseOutOfDate(e.stdout) || isLicenseOutOfDate(e.stderr)) {
throw new Error('Xcode license is not accepted. Please run `sudo xcodebuild -license`.');
} else if (e.stderr?.includes('not a developer tool or in PATH')) {
throw new Error(
`You may need to run ${chalk.bold(
'sudo xcode-select -s /Applications/Xcode.app'
)} and try again.`
);
}
if (Array.isArray(e.output)) {
e.message += '\n' + e.output.join('\n').trim();
} else if (e.stderr) {
e.message += '\n' + e.stderr;
}
throw new Error(
`Some other error occurred while running xcrun command.
${e.message}`
);
}
function isLicenseOutOfDate(text: string): boolean {
if (!text) {
return false;
}
const lower = text.toLowerCase();
return lower.includes('xcode') && lower.includes('license');
}
export async function isXcrunInstalledAsync(): Promise<boolean> {
try {
await spawnAsync('xcrun', ['--version']);
return true;
} catch {
return false;
}
}
export async function installXcrunAsync(): Promise<void> {
await spawnAsync('xcode-select', ['--install']);
await waitForXcrunInstallToFinishAsync(60 * 1000, 1000);
}
async function waitForXcrunInstallToFinishAsync(
maxWaitTimeMs: number,
intervalMs: number
): Promise<void> {
Log.newLine();
Log.log('Waiting for Xcode Command Line Tools install to finish...');
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTimeMs) {
if (await isXcrunInstalledAsync()) {
return;
}
await sleepAsync(Math.min(intervalMs, Math.max(maxWaitTimeMs - (Date.now() - startTime), 0)));
}
throw new Error('Timed out waiting for Xcode Command Line Tools install to finish');
}
+21
View File
@@ -0,0 +1,21 @@
import { AppPlatform } from '../graphql/generated';
import { runAppOnAndroidEmulatorAsync } from './android/run';
import { runAppOnIosSimulatorAsync } from './ios/run';
export interface RunArchiveFlags {
latest?: boolean;
id?: string;
path?: string;
url?: string;
}
export async function runAsync(
simulatorBuildPath: string,
selectedPlatform: AppPlatform
): Promise<void> {
if (selectedPlatform === AppPlatform.Ios) {
await runAppOnIosSimulatorAsync(simulatorBuildPath);
} else {
await runAppOnAndroidEmulatorAsync(simulatorBuildPath);
}
}
@@ -17,7 +17,7 @@ import {
SubmissionAndroidTrack,
} from '../../../graphql/generated';
import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { createTestProject } from '../../../project/__tests__/utils';
import { getOwnerAccountForProjectIdAsync } from '../../../project/projectUtils';
import { createSubmissionContextAsync } from '../../context';
import { getRecentBuildsForSubmissionAsync } from '../../utils/builds';
@@ -12,7 +12,7 @@ import {
testProjectId,
} from '../../../credentials/__tests__/fixtures-constants';
import { SetUpGoogleServiceAccountKey } from '../../../credentials/android/actions/SetUpGoogleServiceAccountKey';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { createTestProject } from '../../../project/__tests__/utils';
import { getOwnerAccountForProjectIdAsync } from '../../../project/projectUtils';
import { promptAsync } from '../../../prompts';
import { createSubmissionContextAsync } from '../../context';
@@ -12,7 +12,7 @@ import {
} from '../../../credentials/__tests__/fixtures-constants';
import { getCredentialsFromUserAsync } from '../../../credentials/utils/promptForCredentials';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { createTestProject } from '../../../project/__tests__/utils';
import { promptAsync } from '../../../prompts';
import { SubmissionContext, createSubmissionContextAsync } from '../../context';
import {
@@ -12,7 +12,7 @@ import {
import { testCommonIosAppCredentialsFragment } from '../../../credentials/__tests__/fixtures-ios';
import { SetUpSubmissionCredentials } from '../../../credentials/ios/actions/SetUpSubmissionCredentials';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { createTestProject } from '../../../project/__tests__/utils';
import { getBundleIdentifierAsync } from '../../../project/ios/bundleIdentifier';
import { promptAsync } from '../../../prompts';
import { createSubmissionContextAsync } from '../../context';
@@ -10,7 +10,7 @@ import {
testProjectId,
} from '../../../credentials/__tests__/fixtures-constants';
import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { createTestProject } from '../../../project/__tests__/utils';
import { getOwnerAccountForProjectIdAsync } from '../../../project/projectUtils';
import { createSubmissionContextAsync } from '../../context';
import IosSubmitCommand from '../IosSubmitCommand';
+1 -1
View File
@@ -61,7 +61,7 @@ export function formatArchiveSourceSummary({ source, build }: Archive): ArchiveS
return summarySlice;
}
export function printSummary<T>(summary: T, keyMap: Record<keyof T, string>): void {
export function printSummary<T extends object>(summary: T, keyMap: Record<keyof T, string>): void {
const fields = [];
for (const [key, value] of Object.entries(summary)) {
const label = `${keyMap[key as keyof T]}:`;
@@ -1,29 +1,18 @@
import { truncatePublishUpdateMessage } from '../../commands/update';
import Log from '../../log';
import { getPlatformsForGroup } from '../utils';
import { getPlatformsForGroup, truncateString } from '../utils';
describe('update utility functions', () => {
describe(truncatePublishUpdateMessage.name, () => {
const warnSpy = jest.spyOn(Log, 'warn');
beforeEach(() => {
warnSpy.mockClear();
});
describe(truncateString, () => {
it('does not alter messages with less than 1024 characters', () => {
const message = 'Small message =)';
const truncatedMessage = truncatePublishUpdateMessage(message);
const truncatedMessage = truncateString(message, 1024);
expect(truncatedMessage).toEqual(message);
expect(warnSpy).not.toBeCalled();
});
it('truncates messages to a length of 1024, including ellipses', () => {
const longMessage = Array.from({ length: 2024 }, () => 'a').join('');
const truncatedMessage = truncatePublishUpdateMessage(longMessage);
const truncatedMessage = truncateString(longMessage, 1024);
expect(truncatedMessage.length).toEqual(1024);
expect(truncatedMessage.slice(-3)).toEqual('...');
expect(warnSpy).toBeCalledWith(
'Update message exceeds the allowed 1024 character limit. Truncating message...'
);
});
});
+311
View File
@@ -0,0 +1,311 @@
import { modifyConfigAsync } from '@expo/config';
import { ExpoConfig } from '@expo/config-types';
import { Platform, Workflow } from '@expo/eas-build-job';
import chalk from 'chalk';
import { getEASUpdateURL } from '../api';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { AppPlatform } from '../graphql/generated';
import Log, { learnMore } from '../log';
import { RequestedPlatform, appPlatformDisplayNames } from '../platform';
import {
installExpoUpdatesAsync,
isExpoUpdatesInstalledOrAvailable,
} from '../project/projectUtils';
import { resolveWorkflowPerPlatformAsync } from '../project/workflow';
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from './android/UpdatesModule';
import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from './ios/UpdatesModule';
export const DEFAULT_MANAGED_RUNTIME_VERSION = { policy: 'sdkVersion' } as const;
export const DEFAULT_BARE_RUNTIME_VERSION = '1.0.0' as const;
function getDefaultRuntimeVersion(workflow: Workflow): NonNullable<ExpoConfig['runtimeVersion']> {
return workflow === Workflow.GENERIC
? DEFAULT_BARE_RUNTIME_VERSION
: DEFAULT_MANAGED_RUNTIME_VERSION;
}
function isRuntimeEqual(
runtimeVersionA: NonNullable<ExpoConfig['runtimeVersion']>,
runtimeVersionB: NonNullable<ExpoConfig['runtimeVersion']>
): boolean {
if (typeof runtimeVersionA === 'string' && typeof runtimeVersionB === 'string') {
return runtimeVersionA === runtimeVersionB;
} else if (typeof runtimeVersionA === 'object' && typeof runtimeVersionB === 'object') {
return runtimeVersionA.policy === runtimeVersionB.policy;
} else {
return false;
}
}
function replaceUndefinedObjectValues(
value: Record<string, any>,
replacement: any
): Record<string, any> {
for (const key in value) {
if (value[key] === undefined) {
value[key] = replacement;
} else if (typeof value[key] === 'object') {
value[key] = replaceUndefinedObjectValues(value[key], replacement);
}
}
return value;
}
/**
* Partially merge the EAS Update config with the existing Expo config.
* This preserves and merges the nested update-related properties.
*/
function mergeExpoConfig(exp: ExpoConfig, modifyExp: Partial<ExpoConfig>): Partial<ExpoConfig> {
return {
runtimeVersion: modifyExp.runtimeVersion ?? exp.runtimeVersion,
updates: { ...exp.updates, ...modifyExp.updates },
android: {
...exp.android,
...modifyExp.android,
},
ios: {
...exp.ios,
...modifyExp.ios,
},
};
}
/**
* Make sure the `app.json` is configured to use EAS Updates.
* This does a couple of things:
* - Ensure update URL is set to the project EAS endpoint
* - Ensure runtimeVersion is defined for both or individual platforms
* - Output the changes made, or the changes required to make manually
*/
async function ensureEASUpdatesIsConfiguredInExpoConfigAsync({
exp,
projectId,
projectDir,
platform,
workflows,
}: {
exp: ExpoConfig;
projectId: string;
projectDir: string;
platform: RequestedPlatform;
workflows: Record<Platform, Workflow>;
}): Promise<{ projectChanged: boolean; exp: ExpoConfig }> {
const modifyConfig: Partial<ExpoConfig> = {};
if (exp.updates?.url !== getEASUpdateURL(projectId)) {
modifyConfig.updates = { url: getEASUpdateURL(projectId) };
}
let androidRuntimeVersion = exp.android?.runtimeVersion ?? exp.runtimeVersion;
let iosRuntimeVersion = exp.ios?.runtimeVersion ?? exp.runtimeVersion;
if (
(['all', 'android'].includes(platform) && !androidRuntimeVersion) ||
(['all', 'ios'].includes(platform) && !iosRuntimeVersion)
) {
androidRuntimeVersion = androidRuntimeVersion ?? getDefaultRuntimeVersion(workflows.android);
iosRuntimeVersion = iosRuntimeVersion ?? getDefaultRuntimeVersion(workflows.ios);
if (platform === 'all' && isRuntimeEqual(androidRuntimeVersion, iosRuntimeVersion)) {
modifyConfig.runtimeVersion = androidRuntimeVersion;
} else {
if (['all', 'android'].includes(platform)) {
modifyConfig.runtimeVersion = undefined;
modifyConfig.android = { runtimeVersion: androidRuntimeVersion };
}
if (['all', 'ios'].includes(platform)) {
modifyConfig.runtimeVersion = undefined;
modifyConfig.ios = { runtimeVersion: iosRuntimeVersion };
}
}
}
if (Object.keys(modifyConfig).length === 0) {
return { exp, projectChanged: false };
}
const mergedExp = mergeExpoConfig(exp, modifyConfig);
const result = await modifyConfigAsync(projectDir, mergedExp);
switch (result.type) {
case 'success':
logEasUpdatesAutoConfig({ exp, modifyConfig });
return {
projectChanged: true,
// TODO(cedric): fix return type of `modifyConfigAsync` to avoid `null` for type === success repsonses
exp: result.config?.expo!,
};
case 'warn':
warnEASUpdatesManualConfig({ modifyConfig, workflows });
throw new Error(result.message);
case 'fail':
throw new Error(result.message);
default:
throw new Error(
`Unexpected result type "${result.type}" received when modifying the project config.`
);
}
}
function logEasUpdatesAutoConfig({
modifyConfig,
exp,
}: {
modifyConfig: Partial<ExpoConfig>;
exp: ExpoConfig;
}): void {
if (modifyConfig.updates?.url) {
Log.withTick(
exp.updates?.url
? `Overwrote updates.url "${exp.updates.url}" with "${modifyConfig.updates.url}"`
: `Configured updates.url to "${modifyConfig.updates.url}"`
);
}
if (modifyConfig.android?.runtimeVersion ?? modifyConfig.runtimeVersion) {
Log.withTick(
`Configured runtimeVersion for ${
appPlatformDisplayNames[AppPlatform.Android]
} with "${JSON.stringify(
modifyConfig.android?.runtimeVersion ?? modifyConfig.runtimeVersion
)}"`
);
}
if (modifyConfig.ios?.runtimeVersion ?? modifyConfig.runtimeVersion) {
Log.withTick(
`Configured runtimeVersion for ${
appPlatformDisplayNames[AppPlatform.Ios]
} with "${JSON.stringify(modifyConfig.ios?.runtimeVersion ?? modifyConfig.runtimeVersion)}"`
);
}
}
function warnEASUpdatesManualConfig({
modifyConfig,
workflows,
}: {
modifyConfig: Partial<ExpoConfig>;
workflows: Record<Platform, Workflow>;
}): void {
Log.addNewLineIfNone();
Log.warn(
`It looks like you are using a dynamic configuration! ${learnMore(
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs)'
)}`
);
Log.warn(
`Finish configuring EAS Update by adding the following to the project app.config.js:\n${learnMore(
'https://expo.fyi/eas-update-config'
)}\n`
);
Log.log(
chalk.bold(
JSON.stringify(replaceUndefinedObjectValues(modifyConfig, '<remove this key>'), null, 2)
)
);
Log.addNewLineIfNone();
if (workflows.android === Workflow.GENERIC || workflows.ios === Workflow.GENERIC) {
Log.warn(
chalk`The native config files {bold Expo.plist & AndroidManifest.xml} must be updated to support EAS Update. ${learnMore(
'https://expo.fyi/eas-update-config.md#native-configuration'
)}`
);
}
Log.addNewLineIfNone();
}
/**
* Make sure that the current `app.json` configuration for EAS Updates is set natively.
*/
async function ensureEASUpdatesIsConfiguredNativelyAsync(
graphqlClient: ExpoGraphqlClient,
{
exp,
projectId,
projectDir,
platform,
workflows,
}: {
exp: ExpoConfig;
projectId: string;
projectDir: string;
platform: RequestedPlatform;
workflows: Record<Platform, Workflow>;
}
): Promise<void> {
if (['all', 'android'].includes(platform) && workflows.android === Workflow.GENERIC) {
await syncAndroidUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId);
Log.withTick(`Configured ${chalk.bold('AndroidManifest.xml')} for EAS Update`);
}
if (['all', 'ios'].includes(platform) && workflows.ios === Workflow.GENERIC) {
await syncIosUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId);
Log.withTick(`Configured ${chalk.bold('Expo.plist')} for EAS Update`);
}
}
/**
* Make sure EAS Update is fully configured in the current project.
* This goes over a checklist and performs the following checks or changes:
* - Enure the `expo-updates` package is currently installed.
* - Ensure `app.json` is configured for EAS Updates
* - Sets `runtimeVersion` if not set
* - Sets `updates.url` if not set
* - Ensure latest changes are reflected in the native config, if any
*/
export async function ensureEASUpdatesIsConfiguredAsync(
graphqlClient: ExpoGraphqlClient,
{
exp: expWithoutUpdates,
projectId,
projectDir,
platform,
}: {
exp: ExpoConfig;
projectId: string;
projectDir: string;
platform: RequestedPlatform;
}
): Promise<void> {
const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(projectDir, expWithoutUpdates);
if (!hasExpoUpdates) {
await installExpoUpdatesAsync(projectDir, { silent: !Log.isDebug });
Log.withTick('Installed expo updates');
}
const workflows = await resolveWorkflowPerPlatformAsync(projectDir);
const { projectChanged, exp: expWithUpdates } =
await ensureEASUpdatesIsConfiguredInExpoConfigAsync({
exp: expWithoutUpdates,
projectDir,
projectId,
platform,
workflows,
});
if (projectChanged || !hasExpoUpdates) {
await ensureEASUpdatesIsConfiguredNativelyAsync(graphqlClient, {
exp: expWithUpdates,
projectDir,
projectId,
platform,
workflows,
});
}
if (projectChanged) {
Log.addNewLineIfNone();
Log.warn(
`All builds of your app going forward will be eligible to receive updates published with EAS Update.`
);
Log.newLine();
}
}
+50 -52
View File
@@ -1,6 +1,5 @@
import assert from 'assert';
import chalk from 'chalk';
import CliTable from 'cli-table3';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
@@ -18,10 +17,9 @@ import {
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
import {
UPDATE_COLUMNS,
UPDATE_COLUMNS_WITH_BRANCH,
formatBranch,
formatUpdateGroup,
formatUpdateTitle,
getUpdateGroupDescriptions,
getUpdateGroupDescriptionsWithBranch,
} from './utils';
@@ -44,7 +42,7 @@ export async function listAndRenderUpdateGroupsOnAppAsync(
offset: paginatedQueryOptions.offset,
appId: projectId,
});
renderUpdateGroupsOnAppAsTable(updateGroups, paginatedQueryOptions);
renderUpdateGroupsOnApp({ updateGroups, paginatedQueryOptions });
} else {
await paginatedQueryWithConfirmPromptAsync({
limit: paginatedQueryOptions.limit ?? UPDATE_GROUPS_LIMIT,
@@ -54,7 +52,7 @@ export async function listAndRenderUpdateGroupsOnAppAsync(
promptOptions: {
title: 'Load more update groups?',
renderListItems: updateGroups =>
renderUpdateGroupsOnAppAsTable(updateGroups, paginatedQueryOptions),
renderUpdateGroupsOnApp({ updateGroups, paginatedQueryOptions }),
},
});
}
@@ -79,7 +77,7 @@ export async function listAndRenderUpdateGroupsOnBranchAsync(
appId: projectId,
branchName,
});
renderUpdateGroupsOnBranchAsTable({ updateGroups, branchName, paginatedQueryOptions });
renderUpdateGroupsOnBranch({ updateGroups, branchName, paginatedQueryOptions });
} else {
await paginatedQueryWithConfirmPromptAsync({
limit: paginatedQueryOptions.limit ?? UPDATE_GROUPS_LIMIT,
@@ -94,7 +92,7 @@ export async function listAndRenderUpdateGroupsOnBranchAsync(
promptOptions: {
title: 'Load more update groups?',
renderListItems: updateGroups =>
renderUpdateGroupsOnBranchAsTable({ updateGroups, branchName, paginatedQueryOptions }),
renderUpdateGroupsOnBranch({ updateGroups, branchName, paginatedQueryOptions }),
},
});
}
@@ -154,15 +152,16 @@ async function queryUpdateGroupsOnAppAsync(
return await UpdateQuery.viewUpdateGroupsOnAppAsync(graphqlClient, args);
}
function renderUpdateGroupsOnBranchAsTable({
updateGroups,
function renderUpdateGroupsOnBranch({
branchName,
updateGroups,
paginatedQueryOptions: { json },
}: {
updateGroups: UpdateFragment[][];
branchName: string;
updateGroups: UpdateFragment[][];
paginatedQueryOptions: PaginatedQueryOptions;
}): void {
// Ensure all updates are from the same branch
const branchNames = updateGroups.flatMap(updateGroup =>
updateGroup.map(update => update.branch.name)
);
@@ -171,59 +170,58 @@ function renderUpdateGroupsOnBranchAsTable({
'Each update must belong to the same branch.'
);
const updateGroupDescriptions = getUpdateGroupDescriptionsWithBranch(updateGroups);
const branch = {
name: branchName,
id: updateGroups[0]?.[0].branch.id ?? 'N/A',
};
const updateGroupDescriptions = getUpdateGroupDescriptions(updateGroups);
if (json) {
printJsonOnlyOutput({ ...branch, currentPage: updateGroupDescriptions });
} else {
const updateGroupsTable = new CliTable({
head: UPDATE_COLUMNS,
wordWrap: true,
});
updateGroupDescriptions.forEach(({ message, runtimeVersion, group, platforms }) => {
updateGroupsTable.push([message, runtimeVersion, group, platforms]);
});
Log.addNewLineIfNone();
Log.log(chalk.bold('Branch:'));
Log.log(
formatFields([
{ label: 'Name', value: branch.name },
{ label: 'ID', value: branch.id },
])
);
Log.addNewLineIfNone();
Log.log(updateGroupsTable.toString());
return printJsonOnlyOutput({ ...branch, currentPage: updateGroupDescriptions });
}
Log.addNewLineIfNone();
Log.log(chalk.bold('Branch:'));
Log.log(
formatFields([
{ label: 'Name', value: branch.name },
{ label: 'ID', value: branch.id },
])
);
Log.newLine();
Log.log(chalk.bold('Recent update groups:'));
Log.newLine();
Log.log(
updateGroupDescriptions
.map(description => formatUpdateGroup(description))
.join(`\n\n${chalk.dim('———')}\n\n`)
);
}
function renderUpdateGroupsOnAppAsTable(
updateGroups: UpdateFragment[][],
{ json }: PaginatedQueryOptions
): void {
function renderUpdateGroupsOnApp({
updateGroups,
paginatedQueryOptions: { json },
}: {
updateGroups: UpdateFragment[][];
paginatedQueryOptions: PaginatedQueryOptions;
}): void {
const updateGroupDescriptions = getUpdateGroupDescriptionsWithBranch(updateGroups);
if (json) {
printJsonOnlyOutput({ currentPage: updateGroupDescriptions });
} else {
const updateGroupsTable = new CliTable({
head: UPDATE_COLUMNS_WITH_BRANCH,
wordWrap: true,
});
updateGroupDescriptions.forEach(({ branch, message, runtimeVersion, group, platforms }) => {
updateGroupsTable.push([branch, message, runtimeVersion, group, platforms]);
});
Log.addNewLineIfNone();
Log.log(chalk.bold('Recent update groups:'));
Log.addNewLineIfNone();
Log.log(updateGroupsTable.toString());
}
Log.addNewLineIfNone();
Log.log(chalk.bold('Recent update groups:'));
Log.newLine();
Log.log(
updateGroupDescriptions
.map(({ branch, ...update }) =>
formatBranch({
branch,
update,
})
)
.join(`\n\n${chalk.dim('———')}\n\n`)
);
}
+53 -1
View File
@@ -4,7 +4,14 @@ import chalk from 'chalk';
import dateFormat from 'dateformat';
import { getEASUpdateURL } from '../api';
import { Maybe, Robot, Update, UpdateFragment, User } from '../graphql/generated';
import {
Maybe,
Robot,
Update,
UpdateBranchFragment,
UpdateFragment,
User,
} from '../graphql/generated';
import Log, { learnMore } from '../log';
import { RequestedPlatform } from '../platform';
import { confirmAsync } from '../prompts';
@@ -31,6 +38,12 @@ export type FormattedUpdateGroupDescription = {
runtimeVersion: string;
};
export type FormattedBranchDescription = {
branch: string;
branchRolloutPercentage?: number;
update?: FormattedUpdateGroupDescription;
};
export type FormattedUpdateGroupDescriptionWithBranch = FormattedUpdateGroupDescription & {
branch: string;
};
@@ -53,6 +66,25 @@ export function formatUpdateGroup(update: FormattedUpdateGroupDescription): stri
]);
}
export function formatBranch({
branch,
branchRolloutPercentage,
update,
}: FormattedBranchDescription): string {
const rolloutField = branchRolloutPercentage
? [{ label: 'Rollout', value: `${branchRolloutPercentage}%` }]
: [];
return formatFields([
{ label: 'Branch', value: branch },
...rolloutField,
{ label: 'Platforms', value: update?.platforms ?? 'N/A' },
{ label: 'Runtime Version', value: update?.runtimeVersion ?? 'N/A' },
{ label: 'Message', value: update?.message ?? 'N/A' },
{ label: 'Group ID', value: update?.group ?? 'N/A' },
]);
}
export function getPlatformsForGroup({
group,
updates = [],
@@ -172,6 +204,26 @@ export function getUpdateGroupDescriptionsWithBranch(
}));
}
export function getBranchDescription(branch: UpdateBranchFragment): FormattedBranchDescription {
if (branch.updates.length === 0) {
return { branch: branch.name };
}
const latestUpdate = branch.updates[0];
return {
branch: branch.name,
update: {
message: formatUpdateMessage(latestUpdate),
runtimeVersion: latestUpdate.runtimeVersion,
group: latestUpdate.group,
platforms: getPlatformsForGroup({
group: latestUpdate.group,
updates: branch.updates,
}),
},
};
}
export async function checkEASUpdateURLIsSetAsync(
exp: ExpoConfig,
projectId: string
@@ -142,11 +142,11 @@ describe(SessionManager, () => {
await sessionManager['loginAsync']({ username: 'USERNAME', password: 'PASSWORD' });
expect(await fs.readFile(getStateJsonPath(), 'utf8')).toMatchInlineSnapshot(`
"{
\\"auth\\": {
\\"sessionSecret\\": \\"SESSION_SECRET\\",
\\"userId\\": \\"USER_ID\\",
\\"username\\": \\"USERNAME\\",
\\"currentConnection\\": \\"Username-Password-Authentication\\"
"auth": {
"sessionSecret": "SESSION_SECRET",
"userId": "USER_ID",
"username": "USERNAME",
"currentConnection": "Username-Password-Authentication"
}
}
"
Loaded 100 of 120 files, more files were not shown because too many files have changed in this diff. Show more