Compare commits

..
Author SHA1 Message Date
Will Schurman bcf2c62066 [eas-cli] fix env overriding in getExpoConfig 2022-11-17 13:50:00 -08:00
Wojciech Kozyra 8632b8ada7 [eas-cli] Upload project sources and submit archives to GCS (#1524) 2022-11-17 11:57:41 +01:00
Wojciech Kozyra b288879838 [eas-cli] Add SDK version to tracking context (#1529) 2022-11-17 11:34:24 +01:00
Dominik Sokal 8beb8f46da change wording: "Submission Service" -> "EAS Submit" (#1527)
* change wording: "Submission Service" -> "EAS Submit"

* update CHANGELOG.md
2022-11-17 09:53:44 +01:00
Expo CI 9120bf5777 update CHANGELOG.md 2022-11-16 14:58:36 +00:00
Szymon Dziedzic 01619d4182 v2.7.1 2022-11-16 15:49:27 +01:00
Szymon Dziedzic aa4ea4ae2a update eas-build-job version in yarn.lock 2022-11-16 15:48:54 +01:00
Szymon Dziedzic c236c2ffb5 upgrade eas-cli-local-build-plugin to 0.0.119 2022-11-16 15:45:52 +01:00
Szymon Dziedzic b5a3a513de [ENG-6892][eas-json] add Xcode 14.1 image (#1511)
* [eas-json] add Xcode 14.1 image

* bump build tools version

* update CHANGELOG.md
2022-11-16 15:30:15 +01:00
Wojciech Kozyra 6058183c45 [eas-cli] Show what prompt failed when running in non interactive shell. (#1523) 2022-11-16 09:57:22 +01:00
Wojciech Kozyra a042d27573 [eas-cli] Pass env variables when resolving config in credentials manager. (#1520) 2022-11-15 11:19:26 +01:00
Dominik Sokal 88b21e476d [scripts] run yarn install before running scripts (#1519) 2022-11-14 10:34:02 +01:00
Expo CI 3d497d4f05 update CHANGELOG.md 2022-11-11 17:26:16 +00:00
Brent Vatne 9b668e3629 v2.7.0 2022-11-11 09:16:07 -08:00
Brent Vatne a670a2a3fc upgrade eas-cli-local-build-plugin to 0.0.118 2022-11-11 09:15:44 -08:00
Brent Vatne 53a407fa78 [eas-cli] Fix test 2022-11-11 09:12:30 -08:00
Brent Vatne 5ab1f637a5 [eas-cli] Remove dependency on expo package from eas init (#1517) 2022-11-11 09:01:17 -08:00
Szymon Dziedzic f59c945347 [ENG-6162][eas-cli] Change choice formatting when selecting build to submit (#1502)
* [eas-cli] change choice formatting when selecting build to submit

* fix typo

* use description with one line choice

* use eas update channel vs old release channel

* update CHANGELOG.md
2022-11-10 17:49:41 +01:00
Evan Bacon 83e50ab546 feat: support uploading different bundle combos (#1461)
* feat: support uploading web bundles

Expo Router defaults to shipping with universal Metro support (ios, android, web) which means eas update breaks by default if it asserts that web is not supported.

This PR also fixes the inability to upload single platforms.

* ensure platform filtering works

* Update publish.ts

* add tests

* update CHANGELOG.md

* style xits from PR feedback

* attempt to fix

* fix

* Update index.ts

* Update CHANGELOG.md

* Update publish-test.ts

* Update publish-test.ts
2022-11-09 15:49:28 -06:00
Dominik Sokal 9a415fe743 update CHANGELOG.md 2022-11-07 10:34:56 +01:00
Mackenzie Salisbury e722b6601c [eas-json] fix ordering of distribution enum values (#1504) 2022-11-07 10:19:56 +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
Dominik Sokal cc99bb3326 v2.5.1 2022-10-24 12:18:07 +02:00
Dominik Sokal 08dc52c631 Revert "feat: use versioned cli for export (#1460)"
This reverts commit 2aade4c0c8.
2022-10-24 12:17:47 +02:00
Dominik Sokal 51db45a861 revert #1460 (#1472)
* Revert "feat: use versioned cli for export (#1460)"

This reverts commit 2aade4c0c8.

* revert CHANGELOG.md change

* update CHANGELOG.md
2022-10-24 12:08:06 +02:00
Dominik Sokal 790853b3b2 Update CHANGELOG.md 2022-10-24 09:52:15 +02:00
Expo CI 23479acabc update CHANGELOG.md 2022-10-24 07:49:28 +00:00
140 changed files with 7695 additions and 3890 deletions

No files matched your search

+3
View File
@@ -1,2 +1,5 @@
* text eol=lf
*.png binary
*.jpg binary
**/yarn.lock linguist-generated
+67 -1
View File
@@ -8,12 +8,78 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
### 🐛 Bug fixes
### 🧹 Chores
- Better wording in eas submit. ([#1527](https://github.com/expo/eas-cli/pull/1527) by [@dsokal](https://github.com/dsokal))
- Upload project sources for EAS Build and archives for EAS Submit to GCS. ([#1524](https://github.com/expo/eas-cli/pull/1524) by [@wkozyra95](https://github.com/wkozyra95))
## [2.7.1](https://github.com/expo/eas-cli/releases/tag/v2.7.1) - 2022-11-16
### 🐛 Bug fixes
- Use envs from the build profile to resolve credentials in `eas credentials`. ([#1520](https://github.com/expo/eas-cli/pull/1520) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Print prompt message when failing because of the non-interactive shell. ([#1523](https://github.com/expo/eas-cli/pull/1523) by [@wkozyra95](https://github.com/wkozyra95))
- Add SDK version to the analytics build events. ([#1529](https://github.com/expo/eas-cli/pull/1529) by [@wkozyra95](https://github.com/wkozyra95))
- Add Xcode 14.1 image to `eas-json`. ([#1511](https://github.com/expo/eas-cli/pull/1511) by [@szdziedzic](https://github.com/szdziedzic))
## [2.7.0](https://github.com/expo/eas-cli/releases/tag/v2.7.0) - 2022-11-11
### 🎉 New features
- Support uploading different platform combinations in `eas update`. ([#1461](https://github.com/expo/eas-cli/pull/1461) by [@EvanBacon](https://github.com/EvanBacon))
- 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
- Fixed the values of the distribution enum on the build profile schema receiving the incorrect descriptions. ([#1504](https://github.com/expo/eas-cli/pull/1504) by [@macksal](https://github.com/macksal))
- Remove hard dependency on expo package being installed to init a project. ([#1517](https://github.com/expo/eas-cli/pull/1517) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
- Remove friction from the initial EAS update setup. ([#1479](https://github.com/expo/eas-cli/pull/1479) by [@byCedric](https://github.com/byCedric))
- Improve choice formatting when selecting a build to submit in the `eas submit` command. ([#1502](https://github.com/expo/eas-cli/pull/1502) by [@szdziedzic](https://github.com/szdziedzic))
## [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))
## [2.5.0](https://github.com/expo/eas-cli/releases/tag/v2.5.0) - 2022-10-24
### 🎉 New features
- Update init command to handle slug and owner. ([#1452](https://github.com/expo/eas-cli/pull/1452) by [@wschurman](https://github.com/wschurman))
- Add `eas secret:push`. ([#1457](https://github.com/expo/eas-cli/pull/1457) by [@dsokal](https://github.com/dsokal))
### 🐛 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))
- - 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.0"
"version": "2.7.1"
}
+10 -10
View File
@@ -8,7 +8,7 @@
"build:tarballs:linux": "yarn build-for-size-check",
"build-for-size-check": "yarn build && yarn workspace eas-cli pretarball-ci && CLI_SIZE_CHECK=1 yarn workspace eas-cli oclif pack:tarballs --no-xz --targets linux-x64",
"typecheck": "lerna run typecheck",
"start": "lerna run watch --parallel",
"start": "lerna run watch --stream --parallel",
"watch": "yarn start",
"eas": "packages/eas-cli/bin/run",
"lint": "eslint .",
@@ -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",
+141 -50
View File
@@ -89,14 +89,18 @@ 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)
* [`eas project:info`](#eas-projectinfo)
* [`eas project:init`](#eas-projectinit)
* [`eas run`](#eas-run)
* [`eas run:run`](#eas-runrun)
* [`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 +129,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -142,7 +146,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -159,7 +163,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -173,7 +177,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -202,7 +206,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 +227,7 @@ DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -244,7 +248,7 @@ DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -264,7 +268,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -284,7 +288,7 @@ DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -307,7 +311,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -337,7 +341,7 @@ DESCRIPTION
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -354,7 +358,7 @@ DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -371,7 +375,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.0/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:inspect`
@@ -406,7 +410,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.0/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -443,7 +447,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:submit`
@@ -490,7 +494,7 @@ DESCRIPTION
Update version of an app.
```
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v2.5.0/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.7.1/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -509,7 +513,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.0/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.7.1/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -526,7 +530,7 @@ DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -547,7 +551,7 @@ DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -569,7 +573,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.0/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -589,7 +593,7 @@ DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:view [NAME]`
@@ -612,7 +616,7 @@ DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -631,7 +635,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -648,7 +652,7 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -662,7 +666,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.0/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -682,7 +686,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.0/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -703,7 +707,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.0/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -717,7 +721,7 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -731,7 +735,7 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -751,7 +755,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 +807,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.7.1/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
generate the local store configuration from the app stores
@@ -818,7 +840,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.0/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -835,7 +857,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.0/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas open`
@@ -849,7 +871,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.0/packages/eas-cli/src/commands/open.ts)_
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -863,7 +885,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -885,7 +907,55 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/project/init.ts)_
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/project/init.ts)_
## `eas run`
run simulator build
```
USAGE
$ eas run [--latest | --id <value> | --path <value> | --url <value>] [-p android|ios|all] [--offset
<value>] [--limit <value>]
FLAGS
-p, --platform=(android|ios|all)
--id=<value> ID of the simulator build to run
--latest Run the latest simulator build for specified platform
--limit=<value> The number of items to fetch each query. Defaults to 50 and is capped at 100.
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
--path=<value> Path to the simulator build file file
--url=<value> Simulator build archive url
DESCRIPTION
run simulator build
```
_See code: [src/commands/run/index.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/run/index.ts)_
## `eas run:run`
run simulator build
```
USAGE
$ eas run:run [--latest | --id <value> | --path <value> | --url <value>] [-p android|ios|all] [--offset
<value>] [--limit <value>]
FLAGS
-p, --platform=(android|ios|all)
--id=<value> ID of the simulator build to run
--latest Run the latest simulator build for specified platform
--limit=<value> The number of items to fetch each query. Defaults to 50 and is capped at 100.
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
--path=<value> Path to the simulator build file file
--url=<value> Simulator build archive url
DESCRIPTION
run simulator build
```
_See code: [src/commands/run/run.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/run/run.ts)_
## `eas secret:create`
@@ -908,7 +978,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.0/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -926,7 +996,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -940,7 +1010,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.0/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/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.7.1/packages/eas-cli/src/commands/secret/push.ts)_
## `eas submit`
@@ -970,7 +1060,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1000,7 +1090,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/update/index.ts)_
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1008,16 +1098,17 @@ configure the project to support EAS Update
```
USAGE
$ eas update:configure [-p android|ios|all]
$ eas update:configure [-p android|ios|all] [--non-interactive]
FLAGS
-p, --platform=(android|ios|all) [default: all] Platform to configure
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
configure the project to support EAS Update
```
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1038,7 +1129,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.0/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:list`
@@ -1060,7 +1151,7 @@ DESCRIPTION
view the recent updates
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/update/list.ts)_
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:view GROUPID`
@@ -1080,7 +1171,7 @@ DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/update/view.ts)_
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1101,7 +1192,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1121,7 +1212,7 @@ DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1138,7 +1229,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1160,7 +1251,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1177,7 +1268,7 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/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:
+607 -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",
@@ -17267,6 +17413,35 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "GitHubAppEnvironment",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "DEVELOPMENT",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "PRODUCTION",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "STAGING",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GitHubAppInstallation",
@@ -17284,7 +17459,7 @@
"name": "Int",
"ofType": null
},
"defaultValue": "1",
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
@@ -17296,7 +17471,7 @@
"name": "Int",
"ofType": null
},
"defaultValue": "50",
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
@@ -17305,17 +17480,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 +17635,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 +17899,71 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environment",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "GitHubAppEnvironment",
"ofType": null
}
},
"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": "name",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "searchRepositories",
"description": "",
@@ -17736,30 +17984,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 +18005,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 +18099,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,
@@ -17895,6 +18127,18 @@
"name": "GitHubRepositoryMetadata",
"description": "",
"fields": [
{
"name": "defaultBranch",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "githubRepoDescription",
"description": "",
@@ -17987,6 +18231,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "openGraphImageUrl",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "private",
"description": "",
@@ -18161,6 +18417,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 +19781,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 +20227,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 +20364,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",
@@ -22515,6 +22932,12 @@
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "GCS",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "S3",
"description": "",
@@ -22844,6 +23267,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 +24829,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 +26895,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 +27615,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 +28765,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 +28782,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.0",
"version": "2.7.1",
"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.97",
"@expo/eas-json": "2.7.1",
"@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": {
@@ -1,17 +0,0 @@
import { fixS3Url } from '../uploads';
describe(fixS3Url, () => {
it('fixes broken links', () => {
const input =
'https://submission-service-archives.s3.amazonaws.com/production%2Fdc98ca84-1473-4cb3-ae81-8c7b291cb27e%2F4424aa95-b985-4e2f-8755-9507b1037c1c';
const expectedOutput =
'https://submission-service-archives.s3.amazonaws.com/production/dc98ca84-1473-4cb3-ae81-8c7b291cb27e/4424aa95-b985-4e2f-8755-9507b1037c1c';
expect(fixS3Url(input)).toBe(expectedOutput);
});
it('does not change correct urls', () => {
const correctUrl =
'https://submission-service-archives.s3.amazonaws.com/production/dc98ca84-1473-4cb3-ae81-8c7b291cb27e/4424aa95-b985-4e2f-8755-9507b1037c1c';
expect(fixS3Url(correctUrl)).toBe(correctUrl);
});
});
+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);
+4 -4
View File
@@ -26,7 +26,7 @@ import {
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../platform';
import { uploadFileAtPathToS3Async } from '../uploads';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
@@ -113,7 +113,7 @@ export async function prepareBuildRequestForPlatformAsync<
path: (await makeProjectTarballAsync()).path,
} as const)
: ({
type: ArchiveSourceType.S3,
type: ArchiveSourceType.GCS,
bucketKey: await uploadProjectAsync(ctx),
} as const);
@@ -219,9 +219,9 @@ async function uploadProjectAsync<TPlatform extends Platform>(
projectTarballPath = projectTarball.path;
const { bucketKey } = await uploadFileAtPathToS3Async(
const bucketKey = await uploadFileAtPathToGCSAsync(
ctx.graphqlClient,
UploadSessionType.EasBuildProjectSources,
UploadSessionType.EasBuildGcsProjectSources,
projectTarball.path,
createProgressTracker({
total: projectTarball.size,
@@ -78,6 +78,7 @@ export async function createBuildContextAsync<T extends Platform>({
const analyticsEventProperties = {
tracking_id: uuidv4(),
platform,
...(exp.sdkVersion && { sdk_version: exp.sdkVersion }),
...(accountId && { account_id: accountId }),
project_id: projectId,
project_type: workflow,
+5
View File
@@ -16,6 +16,11 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
type: ProjectArchiveSourceType.S3,
bucketKey: archiveSource.bucketKey,
};
} else if (archiveSource.type === ArchiveSourceType.GCS) {
return {
type: ProjectArchiveSourceType.Gcs,
bucketKey: archiveSource.bucketKey,
};
} else if (archiveSource.type === ArchiveSourceType.URL) {
return {
type: ProjectArchiveSourceType.Url,
+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(
{
+1 -1
View File
@@ -6,7 +6,7 @@ import semver from 'semver';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.115';
const PLUGIN_PACKAGE_VERSION = '0.0.119';
export interface LocalBuildOptions {
enable: boolean;
+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,
+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 },
});
@@ -157,9 +158,13 @@ describe(getProjectIdAsync, () => {
expect(projectId).toEqual('2345');
expect(modifyConfigAsync).toHaveBeenCalledTimes(1);
expect(modifyConfigAsync).toHaveBeenCalledWith('/app', {
extra: { eas: { projectId: '2345' } },
});
expect(modifyConfigAsync).toHaveBeenCalledWith(
'/app',
{
extra: { eas: { projectId: '2345' } },
},
{ skipSDKVersionRequirement: true }
);
});
it('throws if writing the ID back to the config fails', async () => {
@@ -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>>;
}
@@ -25,9 +25,13 @@ export async function saveProjectIdToAppConfigAsync(
options: { env?: Env } = {}
): Promise<void> {
const exp = getExpoConfig(projectDir, options);
const result = await modifyConfigAsync(projectDir, {
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
});
const result = await modifyConfigAsync(
projectDir,
{
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
},
{ skipSDKVersionRequirement: true }
);
switch (result.type) {
case 'success':
@@ -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,
@@ -13,6 +13,7 @@ export default class Credentials extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.OptionalProjectConfig,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.Analytics,
};
@@ -21,6 +22,7 @@ export default class Credentials extends EasCommand {
const {
loggedIn: { actor, graphqlClient },
projectConfig,
getDynamicProjectConfigAsync,
analytics,
} = await this.getContextAsync(Credentials, {
nonInteractive: false,
@@ -31,6 +33,7 @@ export default class Credentials extends EasCommand {
graphqlClient,
analytics,
projectConfig ?? null,
getDynamicProjectConfigAsync,
flags.platform
).runAsync();
}
@@ -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') },
]);
}
+1 -1
View File
@@ -75,7 +75,7 @@ export default class Submit extends EasCommand {
exclusive: ['latest', 'id', 'path'],
}),
verbose: Flags.boolean({
description: 'Always print logs from Submission Service',
description: 'Always print logs from EAS Submit',
default: false,
}),
wait: Flags.boolean({
+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;
}
}
+205 -196
View File
@@ -1,4 +1,4 @@
import { ExpoConfig } from '@expo/config';
import { ExpoConfig, Platform as PublishPlatform } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Errors, Flags } from '@oclif/core';
@@ -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,108 +20,82 @@ 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 { RequestedPlatform, requestedPlatformDisplayNames } from '../../platform';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import {
getOwnerAccountForProjectIdAsync,
installExpoUpdatesAsync,
isExpoUpdatesInstalledOrAvailable,
} from '../../project/projectUtils';
import {
PublishPlatform,
ExpoCLIExportPlatformFlag,
buildBundlesAsync,
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
filterExportedPlatformsByFlag,
isUploadedAssetCountAboveWarningThreshold,
resolveInputDirectoryAsync,
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,
getManifestBodyAsync,
signManifestBody,
} from '../../utils/code-signing';
import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
import { getVcsClient } from '../../vcs';
import { createUpdateBranchOnAppAsync } from '../branch/create';
import { createUpdateChannelOnAppAsync } from '../channel/create';
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
export type PublishPlatformFlag = PublishPlatform | 'all';
export const defaultPublishPlatforms: Partial<PublishPlatform>[] = ['android', 'ios'];
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,
});
type UpdateFlags = {
auto: boolean;
platform: ExpoCLIExportPlatformFlag;
branchName?: string;
updateMessage?: string;
republish: boolean;
groupId?: string;
inputDir: string;
skipBundler: boolean;
privateKeyPath?: string;
json: boolean;
nonInteractive: boolean;
};
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;
}
function getRequestedPlatform(platform: ExpoCLIExportPlatformFlag): RequestedPlatform | null {
switch (platform) {
case 'android':
return RequestedPlatform.Android;
case 'ios':
return RequestedPlatform.Ios;
case 'web':
return null;
case 'all':
return RequestedPlatform.All;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
@@ -157,7 +130,11 @@ export default class UpdatePublish extends EasCommand {
}),
platform: Flags.enum({
char: 'p',
options: [...defaultPublishPlatforms, 'all'],
options: [
// TODO: Add web when it's fully supported
...defaultPublishPlatforms,
'all',
],
default: 'all',
required: false,
}),
@@ -179,21 +156,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 +183,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 +197,17 @@ export default class UpdatePublish extends EasCommand {
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
await ensureEASUpdatesIsConfiguredAsync(graphqlClient, {
exp: expPossiblyWithoutEasUpdateConfigured,
platform: getRequestedPlatform(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);
let realizedPlatforms: PublishPlatform[] = [];
if (!branchName) {
if (autoFlag) {
@@ -294,9 +255,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,17 +278,17 @@ 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?`
);
}
let publicationPlatformMessage: string;
if (platformFlag === 'all') {
if (updatesToRepublishFilteredByPlatform.length !== defaultPublishPlatforms.length) {
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 {
@@ -341,13 +302,16 @@ export default class UpdatePublish extends EasCommand {
unsortedUpdateInfoGroups[platform] = JSON.parse(manifestFragment);
}
realizedPlatforms = updatesToRepublishFilteredByPlatform.map(
update => update.platform as PublishPlatform
);
// 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 +320,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 +342,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),
@@ -389,60 +353,81 @@ export default class UpdatePublish extends EasCommand {
// build bundle and upload assets for a new publish
if (!skipBundler) {
const bundleSpinner = ora().start('Building bundle...');
const bundleSpinner = ora().start('Exporting...');
try {
await buildBundlesAsync({ projectDir, inputDir });
bundleSpinner.succeed('Built bundle!');
await buildBundlesAsync({ projectDir, inputDir, exp, platformFlag });
bundleSpinner.succeed('Exported bundle(s)');
} catch (e) {
bundleSpinner.fail('Failed to build bundle!');
bundleSpinner.fail('Export failed');
throw e;
}
}
const assetSpinner = ora().start('Uploading assets...');
// After possibly bundling, assert that the input directory can be found.
const distRoot = await resolveInputDirectoryAsync(inputDir, { skipBundler });
const assetSpinner = ora().start('Uploading...');
try {
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
const assets = await collectAssetsAsync({ inputDir: inputDir!, platforms });
const collectedAssets = await collectAssetsAsync(distRoot);
const assets = filterExportedPlatformsByFlag(collectedAssets, platformFlag);
realizedPlatforms = Object.keys(assets) as PublishPlatform[];
const uploadResults = await uploadAssetsAsync(
graphqlClient,
assets,
projectId,
(totalAssets, missingAssets) => {
assetSpinner.text = `Uploading assets. Finished (${
totalAssets - missingAssets
}/${totalAssets})`;
assetSpinner.text = `Uploading (${totalAssets - missingAssets}/${totalAssets})`;
}
);
uploadedAssetCount = uploadResults.uniqueUploadedAssetCount;
assetLimitPerUpdateGroup = uploadResults.assetLimitPerUpdateGroup;
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
const uploadAssetSuccessMessage = uploadedAssetCount
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'asset' : 'assets'}!`
: `Uploading assets skipped -- no new assets found!`;
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'platform' : 'platforms'}`
: `Uploaded: No changes detected`;
assetSpinner.succeed(uploadAssetSuccessMessage);
} catch (e) {
assetSpinner.fail('Failed to upload assets');
assetSpinner.fail('Failed to upload');
throw e;
}
}
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))) {
runtimeToPlatformMapping[runtime] = Object.entries(runtimeVersions)
.filter(pair => pair[1] === runtime)
.map(pair => pair[0]);
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, realizedPlatforms, projectDir);
const runtimeToPlatformMapping: { runtimeVersion: string; platforms: string[] }[] = [];
for (const runtime of runtimeVersions) {
const platforms = runtimeVersions
.filter(({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion)
.map(({ platform }) => platform);
if (!runtimeToPlatformMapping.find(item => item.runtimeVersion === runtime.runtimeVersion)) {
runtimeToPlatformMapping.push({ runtimeVersion: runtime.runtimeVersion, platforms });
}
}
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(
([runtime, platforms]) => {
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformMapping.map(
({ runtimeVersion, platforms }) => {
const localUpdateInfoGroup = Object.fromEntries(
platforms.map(platform => [
platform,
@@ -458,8 +443,9 @@ export default class UpdatePublish extends EasCommand {
return {
branchId,
updateInfoGroup: localUpdateInfoGroup,
runtimeVersion: republish ? oldRuntimeVersion : runtime,
runtimeVersion: republish ? oldRuntimeVersion : runtimeVersion,
message: truncatedMessage,
gitCommitHash,
awaitingCodeSigningInfo: !!codeSigningInfo,
};
}
@@ -528,12 +514,15 @@ export default class UpdatePublish extends EasCommand {
}
Log.addNewLineIfNone();
for (const runtime of new Set(Object.values(runtimeVersions))) {
for (const runtime of uniqBy(runtimeVersions, version => version.runtimeVersion)) {
const newUpdatesForRuntimeVersion = newUpdates.filter(
update => update.runtimeVersion === runtime
update => update.runtimeVersion === runtime.runtimeVersion
);
if (newUpdatesForRuntimeVersion.length === 0) {
throw new Error(`Publish response is missing updates with runtime ${runtime}.`);
throw new Error(
`Publish response is missing updates with runtime ${runtime.runtimeVersion}.`
);
}
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
@@ -550,7 +539,7 @@ export default class UpdatePublish extends EasCommand {
Log.log(
formatFields([
{ label: 'Branch', value: branchName },
{ label: 'Runtime version', value: runtime },
{ label: 'Runtime version', value: runtime.runtimeVersion },
{ label: 'Platform', value: platforms.join(', ') },
{ label: 'Update group ID', value: updateGroupId },
...(newAndroidUpdate
@@ -558,6 +547,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,19 +567,54 @@ 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,
};
}
}
/** Get runtime versions grouped by platform. Runtime version is always `null` on web where the platform is always backwards compatible. */
async function getRuntimeVersionObjectAsync(
exp: ExpoConfig,
platformFlag: PublishPlatformFlag,
platforms: PublishPlatform[],
projectDir: string
): Promise<Record<string, string>> {
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
): Promise<{ platform: string; runtimeVersion: string }[]> {
for (const platform of platforms) {
if (platform === 'web') {
continue;
}
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
if (isPolicy) {
const isManaged = (await resolveWorkflowAsync(projectDir, platform)) === Workflow.MANAGED;
const isManaged =
(await resolveWorkflowAsync(projectDir, platform as Platform)) === Workflow.MANAGED;
if (!isManaged) {
throw new Error(
'Runtime version policies are only supported in the managed workflow. In the bare workflow, runtime version needs to be set manually.'
@@ -598,34 +623,18 @@ async function getRuntimeVersionObjectAsync(
}
}
return Object.fromEntries(
platforms.map(platform => [
return [...new Set(platforms)].map(platform => {
if (platform === 'web') {
return { platform: 'web', runtimeVersion: 'UNVERSIONED' };
}
return {
platform,
nullthrows(
runtimeVersion: nullthrows(
Updates.getRuntimeVersion(exp, platform),
`Unable to determine runtime version for ${platform}`
`Unable to determine runtime version for ${
requestedPlatformDisplayNames[platform]
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
),
])
);
};
});
}
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",
@@ -1,4 +1,5 @@
import { Analytics } from '../../analytics/AnalyticsManager';
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import Log from '../../log';
import { pressAnyKeyToContinueAsync } from '../../prompts';
@@ -10,6 +11,7 @@ export interface Action<T = void> {
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
projectInfo: CredentialsContextProjectInfo | null;
getDynamicProjectConfigAsync: DynamicConfigContextFn;
runAsync(ctx: CredentialsContext): Promise<T>;
}
@@ -27,7 +27,7 @@ import {
displayAndroidAppCredentials,
displayEmptyAndroidCredentials,
} from '../android/utils/printCredentials';
import { CredentialsContext } from '../context';
import { CredentialsContext, CredentialsContextProjectInfo } from '../context';
import { ActionInfo, AndroidActionType, Scope } from './Actions';
import {
buildCredentialsActions,
@@ -52,9 +52,16 @@ export class ManageAndroid {
const buildProfile = hasProjectContext
? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.ANDROID).runAsync()
: null;
let projectInfo: CredentialsContextProjectInfo | null = null;
if (hasProjectContext) {
const { exp, projectId } = await this.callingAction.getDynamicProjectConfigAsync({
env: buildProfile?.env,
});
projectInfo = { exp, projectId };
}
const ctx = new CredentialsContext({
projectDir: process.cwd(),
projectInfo: this.callingAction.projectInfo,
projectInfo,
user: this.callingAction.actor,
graphqlClient: this.callingAction.graphqlClient,
analytics: this.callingAction.analytics,
@@ -15,7 +15,7 @@ import { resolveTargetsAsync } from '../../project/ios/target';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
import { ensureActorHasPrimaryAccount } from '../../user/actions';
import { CredentialsContext } from '../context';
import { CredentialsContext, CredentialsContextProjectInfo } from '../context';
import {
AppStoreApiKeyPurpose,
selectAscApiKeysFromAccountAsync,
@@ -61,9 +61,18 @@ export class ManageIos {
const buildProfile = this.callingAction.projectInfo
? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.IOS).runAsync()
: null;
let projectInfo: CredentialsContextProjectInfo | null = null;
if (this.callingAction.projectInfo) {
const { exp, projectId } = await this.callingAction.getDynamicProjectConfigAsync({
env: buildProfile?.env,
});
projectInfo = { exp, projectId };
}
const ctx = new CredentialsContext({
projectDir: process.cwd(),
projectInfo: this.callingAction.projectInfo,
projectInfo,
user: this.callingAction.actor,
graphqlClient: this.callingAction.graphqlClient,
analytics: this.callingAction.analytics,
@@ -1,4 +1,5 @@
import { Analytics } from '../../analytics/AnalyticsManager';
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { selectPlatformAsync } from '../../platform';
import { Actor } from '../../user/User';
@@ -12,6 +13,7 @@ export class SelectPlatform {
public readonly graphqlClient: ExpoGraphqlClient,
public readonly analytics: Analytics,
public readonly projectInfo: CredentialsContextProjectInfo | null,
public readonly getDynamicProjectConfigAsync: DynamicConfigContextFn,
private readonly flagPlatform?: string
) {}
@@ -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,
};
+111 -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'];
@@ -2504,9 +2523,15 @@ export type GetSignedAssetUploadSpecificationsResult = {
specifications: Array<Scalars['String']>;
};
export enum GitHubAppEnvironment {
Development = 'DEVELOPMENT',
Production = 'PRODUCTION',
Staging = 'STAGING'
}
export type GitHubAppInstallation = {
__typename?: 'GitHubAppInstallation';
accessibleRepositories: Array<GitHubAppInstallationAccessibleRepository>;
accessibleRepositories: GitHubRepositoryPaginationResult;
account: Account;
actor?: Maybe<Actor>;
id: Scalars['ID'];
@@ -2526,6 +2551,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 +2592,20 @@ export type GitHubAppQuery = {
__typename?: 'GitHubAppQuery';
appIdentifier: Scalars['String'];
clientIdentifier: Scalars['String'];
searchRepositories: Array<GitHubAppInstallationAccessibleRepository>;
environment: GitHubAppEnvironment;
installation: GitHubAppInstallation;
name: Scalars['String'];
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,16 +2616,19 @@ export type GitHubRepository = {
githubRepositoryIdentifier: Scalars['Int'];
id: Scalars['ID'];
metadata?: Maybe<GitHubRepositoryMetadata>;
nodeIdentifier: Scalars['String'];
};
export type GitHubRepositoryMetadata = {
__typename?: 'GitHubRepositoryMetadata';
defaultBranch?: Maybe<Scalars['String']>;
githubRepoDescription?: Maybe<Scalars['String']>;
githubRepoName: Scalars['String'];
githubRepoOwnerName: Scalars['String'];
githubRepoUrl: Scalars['String'];
lastPushed: Scalars['DateTime'];
lastUpdated: Scalars['DateTime'];
openGraphImageUrl?: Maybe<Scalars['String']>;
private: Scalars['Boolean'];
};
@@ -2623,6 +2658,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 +2829,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 +2844,11 @@ export type IosAppBuildCredentialsMutationCreateIosAppBuildCredentialsArgs = {
};
export type IosAppBuildCredentialsMutationDeleteIosAppBuildCredentialsArgs = {
id: Scalars['ID'];
};
export type IosAppBuildCredentialsMutationSetDistributionCertificateArgs = {
distributionCertificateId: Scalars['ID'];
id: Scalars['ID'];
@@ -2849,6 +2897,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 +2913,11 @@ export type IosAppCredentialsMutationCreateIosAppCredentialsArgs = {
};
export type IosAppCredentialsMutationDeleteIosAppCredentialsArgs = {
id: Scalars['ID'];
};
export type IosAppCredentialsMutationSetAppStoreConnectApiKeyForSubmissionsArgs = {
ascApiKeyId: Scalars['ID'];
id: Scalars['ID'];
@@ -2874,6 +2929,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',
@@ -3232,6 +3297,7 @@ export type ProjectArchiveSourceInput = {
};
export enum ProjectArchiveSourceType {
Gcs = 'GCS',
S3 = 'S3',
Url = 'URL'
}
@@ -3281,6 +3347,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 +3566,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 +3898,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 +4004,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 +4177,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 +4553,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 +4591,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 +5017,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, gitCommitMessage?: 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 +5027,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, gitCommitMessage?: 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;
@@ -4997,6 +5083,7 @@ export type CreateAndroidSubmissionMutationVariables = Exact<{
appId: Scalars['ID'];
config: AndroidSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']>;
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
}>;
@@ -5006,6 +5093,7 @@ export type CreateIosSubmissionMutationVariables = Exact<{
appId: Scalars['ID'];
config: IosSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']>;
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
}>;
@@ -5096,14 +5184,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, gitCommitMessage?: 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, gitCommitMessage?: 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 +5201,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, gitCommitMessage?: 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 +5297,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 +5318,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, gitCommitMessage?: 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, gitCommitMessage?: 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()
);
@@ -28,10 +28,16 @@ export const SubmissionMutation = {
$appId: ID!
$config: AndroidSubmissionConfigInput!
$submittedBuildId: ID
$archiveSource: SubmissionArchiveSourceInput
) {
submission {
createAndroidSubmission(
input: { appId: $appId, config: $config, submittedBuildId: $submittedBuildId }
input: {
appId: $appId
config: $config
submittedBuildId: $submittedBuildId
archiveSource: $archiveSource
}
) {
submission {
id
@@ -60,10 +66,16 @@ export const SubmissionMutation = {
$appId: ID!
$config: IosSubmissionConfigInput!
$submittedBuildId: ID
$archiveSource: SubmissionArchiveSourceInput
) {
submission {
createIosSubmission(
input: { appId: $appId, config: $config, submittedBuildId: $submittedBuildId }
input: {
appId: $appId
config: $config
submittedBuildId: $submittedBuildId
archiveSource: $archiveSource
}
) {
submission {
id
@@ -8,16 +8,17 @@ import {
UploadSessionType,
} from '../generated';
export interface PresignedPost {
export interface SignedUrl {
url: string;
fields: Record<string, string>;
headers: Record<string, string>;
bucketKey: string;
}
export const UploadSessionMutation = {
async createUploadSessionAsync(
graphqlClient: ExpoGraphqlClient,
type: UploadSessionType
): Promise<PresignedPost> {
): Promise<SignedUrl> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateUploadSessionMutation, CreateUploadSessionMutationVariables>(
@@ -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
@@ -44,6 +45,7 @@ export const BuildFragmentNode = gql`
appBuildVersion
runtimeVersion
gitCommitHash
gitCommitMessage
initialQueuePosition
queuePosition
estimatedWaitTimeLeftSeconds
+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,67 @@
import { ProjectConfig, getConfig } from '@expo/config';
import { getExpoConfig } from '../expoConfig';
jest.mock('@expo/config');
describe(getExpoConfig, () => {
it('correctly restores process.env as best as possible', () => {
process.env.BEFORE_CALL_KEY = 'BEFORE';
process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_WITHIN = 'BEFORE_OVERRIDDEN_BY_WITHIN';
process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS = 'BEFORE_OVERRIDDEN_BY_OPTS';
process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS_AND_WITHIN =
'BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS_AND_WITHIN';
jest.mocked(getConfig).mockImplementation(() => {
process.env.WITHIN_CALL_KEY = 'WITHIN';
process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_WITHIN = 'OVERRIDDEN_VALUE';
process.env.OPTS_KEY_OVERRIDDEN_BY_WITHIN = 'OVERRIDDEN_VALUE';
process.env.OPTS_KEY_OVERRIDDEN_BY_WITHIN_TO_SAME_VALUE =
'OPTS_KEY_OVERRIDDEN_BY_WITHIN_TO_SAME_VALUE';
process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS_AND_WITHIN = 'OVERRIDDEN_WITHIN';
return {} as ProjectConfig;
});
// ensure idempotent (run N times)
for (let i = 0; i < 2; i++) {
getExpoConfig('', {
env: {
OPTS_KEY: 'OPTS',
BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS: 'OVERRIDDEN_VALUE',
OPTS_KEY_OVERRIDDEN_BY_WITHIN: 'OPTS_KEY_OVERRIDDEN_BY_WITHIN',
OPTS_KEY_OVERRIDDEN_BY_WITHIN_TO_SAME_VALUE:
'OPTS_KEY_OVERRIDDEN_BY_WITHIN_TO_SAME_VALUE',
BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS_AND_WITHIN: 'OVERRIDDEN_OPTS',
},
});
// env from before the call should be restored
expect(process.env.BEFORE_CALL_KEY).toEqual('BEFORE');
// env from before the call but overridden within the call should not be restored
expect(process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_WITHIN).toEqual('OVERRIDDEN_VALUE');
// env from before the call but overridden by opts should be restored
expect(process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS).toEqual('BEFORE_OVERRIDDEN_BY_OPTS');
// env added within the call should be kept
expect(process.env.WITHIN_CALL_KEY).toEqual('WITHIN');
// env added by opts should be removed
expect(process.env.OPTS_KEY).toBeUndefined();
// env added by opts but overridden within the call should be retained and have overridden value
expect(process.env.OPTS_KEY_OVERRIDDEN_BY_WITHIN).toEqual('OVERRIDDEN_VALUE');
// env added by opts but overridden within the call to the same value will be removed
// note that this is a known bug/limitation of this approach
expect(process.env.OPTS_KEY_OVERRIDDEN_BY_WITHIN_TO_SAME_VALUE).toBeUndefined();
// env from before but overridden within the call and by opts should retain value from
// within the call
expect(process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS_AND_WITHIN).toEqual(
'OVERRIDDEN_WITHIN'
);
}
});
});
@@ -14,6 +14,7 @@ import {
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
convertAssetToUpdateInfoGroupFormatAsync,
filterExportedPlatformsByFlag,
filterOutAssetsThatAlreadyExistAsync,
getBase64URLEncoding,
getStorageKey,
@@ -91,6 +92,28 @@ describe('MetadataJoi', () => {
});
});
describe(filterExportedPlatformsByFlag, () => {
it(`returns all`, () => {
expect(filterExportedPlatformsByFlag({ web: true, ios: true, android: true }, 'all')).toEqual({
web: true,
ios: true,
android: true,
});
});
it(`selects a platform`, () => {
expect(filterExportedPlatformsByFlag({ web: true, ios: true, android: true }, 'ios')).toEqual({
ios: true,
});
});
it(`asserts selected platform missing`, () => {
expect(() =>
filterExportedPlatformsByFlag({ web: true }, 'ios')
).toThrowErrorMatchingInlineSnapshot(
`"--platform="ios" not found in metadata.json. Available platform(s): web"`
);
});
});
describe(guessContentTypeFromExtension, () => {
it('returns the correct content type for jpg', () => {
expect(guessContentTypeFromExtension('jpg')).toBe('image/jpeg');
@@ -236,30 +259,41 @@ describe(resolveInputDirectoryAsync, () => {
it('returns the correct distRoot path', async () => {
const customDirectoryName = path.resolve(uuidv4());
await fs.mkdir(customDirectoryName, { recursive: true });
expect(await resolveInputDirectoryAsync(customDirectoryName)).toBe(customDirectoryName);
expect(await resolveInputDirectoryAsync(customDirectoryName, { skipBundler: false })).toBe(
customDirectoryName
);
});
it('throws an error if the path does not exist', async () => {
const nonExistentPath = path.resolve(uuidv4());
await expect(resolveInputDirectoryAsync(nonExistentPath)).rejects
.toThrow(`The input directory "${nonExistentPath}" does not exist.
You can allow us to build it for you by not setting the --skip-bundler flag.
If you chose to build it yourself you'll need to run a command to build the JS
bundle first.
You can use '--input-dir' to specify a different input directory.`);
await expect(
resolveInputDirectoryAsync(nonExistentPath, { skipBundler: false })
).rejects.toThrow(`--input-dir="${nonExistentPath}" not found.`);
});
it('throws a more specific error if the path does not exist and the dev opted out of bundling', async () => {
const nonExistentPath = path.resolve(uuidv4());
await expect(
resolveInputDirectoryAsync(nonExistentPath, { skipBundler: true })
).rejects.toThrow(
`--input-dir="${nonExistentPath}" not found. --skip-bundler requires the project to be exported manually before uploading. Ex: npx expo export && eas update --skip-bundler`
);
});
});
describe(collectAssetsAsync, () => {
it('builds an update info group', async () => {
const fakeHash = 'md5-hash-of-jpg';
const bundles = { android: 'android-bundle-code', ios: 'ios-bundle-code' };
const inputDir = uuidv4();
const bundles = {
android: 'android-bundle-code',
ios: 'ios-bundle-code',
web: 'web-bundle-code',
};
const inputDir = path.resolve(uuidv4());
const userDefinedAssets = [
{
fileExtension: '.jpg',
contentType: 'image/jpeg',
path: path.resolve(`${inputDir}/assets/${fakeHash}`),
path: `${inputDir}/assets/${fakeHash}`,
},
];
@@ -285,16 +319,20 @@ describe(collectAssetsAsync, () => {
assets: [{ path: `assets/${fakeHash}`, ext: 'jpg' }],
bundle: 'bundles/ios.js',
},
web: {
assets: [{ path: `assets/${fakeHash}`, ext: 'jpg' }],
bundle: 'bundles/web.js',
},
},
})
);
expect(await collectAssetsAsync({ inputDir, platforms: defaultPublishPlatforms })).toEqual({
expect(await collectAssetsAsync(inputDir)).toEqual({
android: {
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: path.resolve(`${inputDir}/bundles/android.js`),
path: `${inputDir}/bundles/android.js`,
},
assets: userDefinedAssets,
},
@@ -302,18 +340,15 @@ describe(collectAssetsAsync, () => {
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: path.resolve(`${inputDir}/bundles/ios.js`),
path: `${inputDir}/bundles/ios.js`,
},
assets: userDefinedAssets,
},
});
expect(await collectAssetsAsync({ inputDir, platforms: ['ios'] })).toEqual({
ios: {
web: {
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: path.resolve(`${inputDir}/bundles/ios.js`),
path: `${inputDir}/bundles/web.js`,
},
assets: userDefinedAssets,
},
@@ -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,
});
});
});
+25 -1
View File
@@ -1,5 +1,6 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { Env } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
export interface ExpoConfigOptions {
env?: Env;
@@ -9,6 +10,11 @@ export interface ExpoConfigOptions {
export function getExpoConfig(projectDir: string, opts: ExpoConfigOptions = {}): ExpoConfig {
const originalProcessEnv: NodeJS.ProcessEnv = process.env;
const optsEnvKeys = opts.env ? Object.keys(opts.env) : [];
const originalEnvKeys = new Set(Object.keys(process.env));
const newKeysAddedToProcessEnv = new Set(optsEnvKeys.filter(k => !originalEnvKeys.has(k)));
try {
process.env = {
...process.env,
@@ -20,6 +26,24 @@ export function getExpoConfig(projectDir: string, opts: ExpoConfigOptions = {}):
});
return exp;
} finally {
process.env = originalProcessEnv;
optsEnvKeys.forEach(k => {
// if new key added, remove unless it was modified
if (newKeysAddedToProcessEnv.has(k)) {
if (k in originalProcessEnv) {
if (process.env[k] === originalProcessEnv[k]) {
delete process.env[k];
}
} else {
if (nullthrows(opts.env)[k] === process.env[k]) {
delete process.env[k];
}
}
} else {
// opts key overrode env key, but only if the value is equal to the one declared by opts
if (process.env[k] === nullthrows(opts.env)[k]) {
process.env[k] = originalProcessEnv[k];
}
}
});
}
}
+12 -3
View File
@@ -97,10 +97,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(
+80 -43
View File
@@ -10,19 +10,20 @@ import promiseLimit from 'promise-limit';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { AssetMetadataStatus, PartialManifestAsset } from '../graphql/generated';
import { PublishMutation } from '../graphql/mutations/PublishMutation';
import { PresignedPost } from '../graphql/mutations/UploadSessionMutation';
import { PublishQuery } from '../graphql/queries/PublishQuery';
import { uploadWithPresignedPostWithRetryAsync } from '../uploads';
import Log from '../log';
import { PresignedPost, uploadWithPresignedPostWithRetryAsync } from '../uploads';
import { expoCommandAsync, shouldUseVersionedExpoCLI } from '../utils/expoCli';
import chunk from '../utils/expodash/chunk';
import uniqBy from '../utils/expodash/uniqBy';
export type PublishPlatform = Extract<'android' | 'ios', Platform>;
export type ExpoCLIExportPlatformFlag = Platform | 'all';
type Metadata = {
version: number;
bundler: 'metro';
fileMetadata: {
[key in 'android' | 'ios']: { assets: { path: string; ext: string }[]; bundle: string };
[key in Platform]: { assets: { path: string; ext: string }[]; bundle: string };
};
};
export type RawAsset = {
@@ -31,7 +32,7 @@ export type RawAsset = {
path: string;
};
type CollectedAssets = {
[platform in PublishPlatform]?: {
[platform in Platform]?: {
launchAsset: RawAsset;
assets: RawAsset[];
};
@@ -47,7 +48,7 @@ type ManifestFragment = {
extra?: ManifestExtra;
};
type UpdateInfoGroup = {
[key in PublishPlatform]: ManifestFragment;
[key in Platform]: ManifestFragment;
};
const fileMetadataJoi = Joi.object({
@@ -55,13 +56,14 @@ const fileMetadataJoi = Joi.object({
.required()
.items(Joi.object({ path: Joi.string().required(), ext: Joi.string().required() })),
bundle: Joi.string().required(),
}).required();
}).optional();
export const MetadataJoi = Joi.object({
version: Joi.number().required(),
bundler: Joi.string().required(),
fileMetadata: Joi.object({
android: fileMetadataJoi,
ios: fileMetadataJoi,
web: fileMetadataJoi,
}).required(),
}).required();
@@ -129,7 +131,7 @@ export async function buildUnsortedUpdateInfoGroupAsync(
assets: CollectedAssets,
exp: ExpoConfig
): Promise<UpdateInfoGroup> {
let platform: PublishPlatform;
let platform: Platform;
const updateInfoGroup: Partial<UpdateInfoGroup> = {};
for (platform in assets) {
updateInfoGroup[platform] = {
@@ -148,17 +150,28 @@ export async function buildUnsortedUpdateInfoGroupAsync(
export async function buildBundlesAsync({
projectDir,
inputDir,
exp,
platformFlag,
}: {
projectDir: string;
inputDir: string;
exp: Pick<ExpoConfig, 'sdkVersion'>;
platformFlag: ExpoCLIExportPlatformFlag;
}): Promise<void> {
const packageJSON = JsonFile.read(path.resolve(projectDir, 'package.json'));
if (!packageJSON) {
throw new Error('Could not locate package.json');
}
if (shouldUseVersionedExpoCLI(projectDir)) {
await expoCommandAsync(projectDir, ['export', '--output-dir', inputDir, '--dump-sourcemap']);
if (shouldUseVersionedExpoCLI(projectDir, exp)) {
await expoCommandAsync(projectDir, [
'export',
'--output-dir',
inputDir,
'--dump-sourcemap',
'--platform',
platformFlag,
]);
} else {
// Legacy global Expo CLI
await expoCommandAsync(projectDir, [
@@ -168,18 +181,23 @@ export async function buildBundlesAsync({
'--experimental-bundle',
'--non-interactive',
'--dump-sourcemap',
'--platform',
platformFlag,
]);
}
}
export async function resolveInputDirectoryAsync(customInputDirectory: string): Promise<string> {
const distRoot = path.resolve(customInputDirectory);
export async function resolveInputDirectoryAsync(
inputDir: string,
{ skipBundler }: { skipBundler?: boolean }
): Promise<string> {
const distRoot = path.resolve(inputDir);
if (!(await fs.pathExists(distRoot))) {
throw new Error(`The input directory "${customInputDirectory}" does not exist.
You can allow us to build it for you by not setting the --skip-bundler flag.
If you chose to build it yourself you'll need to run a command to build the JS
bundle first.
You can use '--input-dir' to specify a different input directory.`);
let error = `--input-dir="${inputDir}" not found.`;
if (skipBundler) {
error += ` --skip-bundler requires the project to be exported manually before uploading. Ex: npx expo export && eas update --skip-bundler`;
}
throw new Error(error);
}
return distRoot;
}
@@ -198,43 +216,62 @@ export function loadMetadata(distRoot: string): Metadata {
if (metadata.bundler !== 'metro') {
throw new Error('Only bundles created with Metro are currently supported');
}
const platforms = Object.keys(metadata.fileMetadata);
if (platforms.length === 0) {
Log.warn('No updates were exported for any platform');
}
Log.debug(`Loaded ${platforms.length} platform(s): ${platforms.join(', ')}`);
return metadata;
}
export async function collectAssetsAsync({
inputDir,
platforms,
}: {
inputDir: string;
platforms: PublishPlatform[];
}): Promise<CollectedAssets> {
const distRoot = await resolveInputDirectoryAsync(inputDir);
const metadata = loadMetadata(distRoot);
export function filterExportedPlatformsByFlag<T extends Partial<Record<Platform, any>>>(
record: T,
platformFlag: ExpoCLIExportPlatformFlag
): T {
if (platformFlag === 'all') {
return record;
}
const assetsFinal: CollectedAssets = {};
for (const platform of platforms) {
assetsFinal[platform] = {
const platform = platformFlag as Platform;
if (!record[platform]) {
throw new Error(
`--platform="${platform}" not found in metadata.json. Available platform(s): ${Object.keys(
record
).join(', ')}`
);
}
return { [platform]: record[platform] } as T;
}
/** Given a directory, load the metadata.json and collect the assets for each platform. */
export async function collectAssetsAsync(dir: string): Promise<CollectedAssets> {
const metadata = loadMetadata(dir);
const collectedAssets: CollectedAssets = {};
for (const platform of Object.keys(metadata.fileMetadata) as Platform[]) {
collectedAssets[platform] = {
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: path.resolve(distRoot, metadata.fileMetadata[platform].bundle),
path: path.resolve(dir, metadata.fileMetadata[platform].bundle),
},
assets: metadata.fileMetadata[platform].assets.map(asset => {
let fileExtension;
if (asset.ext) {
// ensure the file extension has a '.' prefix
fileExtension = asset.ext.startsWith('.') ? asset.ext : `.${asset.ext}`;
}
return {
fileExtension,
contentType: guessContentTypeFromExtension(asset.ext),
path: path.join(distRoot, asset.path),
};
}),
assets: metadata.fileMetadata[platform].assets.map(asset => ({
fileExtension: asset.ext ? ensureLeadingPeriod(asset.ext) : undefined,
contentType: guessContentTypeFromExtension(asset.ext),
path: path.join(dir, asset.path),
})),
};
}
return assetsFinal;
return collectedAssets;
}
// ensure the file extension has a '.' prefix
function ensureLeadingPeriod(extension: string): string {
return extension.startsWith('.') ? extension : `.${extension}`;
}
export async function filterOutAssetsThatAlreadyExistAsync(
+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 };
}
+4 -1
View File
@@ -14,7 +14,10 @@ export async function promptAsync<T extends string = string>(
options: Options = {}
): Promise<Answers<T>> {
if (!process.stdin.isTTY && !global.test) {
throw new Error('Input is required, but stdin is not readable.');
const message = Array.isArray(questions) ? questions[0]?.message : questions.message;
throw new Error(
`Input is required, but stdin is not readable. Failed to display prompt: ${message}`
);
}
return await prompts<T>(questions, {
onCancel() {
+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);
}
}
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff. Show more