Compare commits

..
45 Commits
Author SHA1 Message Date
Brent Vatne bb98e6de03 v0.34.1 2021-11-02 10:15:45 -07:00
Dominik Sokal 2604228233 add clean script 2021-11-02 13:41:47 +01:00
Dominik Sokal a7035d3469 clean up package.json 2021-11-02 13:37:00 +01:00
Dominik Sokal f6dce0e17c update CHANGELOG.md 2021-11-02 13:31:56 +01:00
Wojciech KozyraandDominik Sokal ccfa6f9fc3 [eas-cli] check if commit is required after dev client install (#722)
* check if commit is required after dev client install

* update CHANGELOG.md

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-02 12:51:49 +01:00
Dominik Sokal 0ee8e2f967 Update CONTRIBUTING.md 2021-11-02 11:29:27 +01:00
Quinlan Jung 94dc4c6e93 [eas-cli] asc api key graphql type and print support (#717)
* [eas-cli] asc api key graphql type and print support

* gql

* gql

* update CHANGELOG.md
2021-11-01 14:56:07 -07:00
Brent Vatne 4144e387d6 [eas-cli] Add missing period 2021-11-01 11:39:15 -07:00
Expo CI de4e05e1e0 update CHANGELOG.md 2021-11-01 18:30:08 +00:00
Brent Vatne 36118b700f v0.34.0 2021-11-01 11:23:56 -07:00
Dominik Sokal 2fb947b47a rename useDevelopmentClient to developmentClient (#711)
* generate graphql code

* rename useDevelopmentClient to developmentClient

* update CHANGELOG.md

* upgrade @expo/eas-build-job
2021-10-31 18:51:26 +01:00
gregfentonandWojciech Kozyra 0f1156e951 [eas-cli] correct warning message text (#715) (#716)
* [eas-cli] correct warning message text (#715)

* (minor) Incorrect instructions in warning message
Fixes #715

* Update packages/eas-cli/src/build/validate.ts

Co-authored-by: Wojciech Kozyra <[email protected]>
2021-10-29 10:47:14 +02:00
Quinlan Jung e9f8be92b7 [eas-cli] fix graphql (#713)
* [eas-cli] fix graphql

* update CHANGELOG.md
2021-10-28 11:51:40 -07:00
Quinlan Jung 6360ea9f8e [eas-cli] refactor credentials manager (#712)
* [eas-cli] refactor android manager

* refactor ios

* update CHANGELOG.md
2021-10-28 11:50:08 -07:00
Wojciech Kozyra ad2d8e80f0 add no commit workflow (#695) 2021-10-28 11:40:11 +02:00
Quinlan Jung bc0a5fdaa6 [eas-cli] remove unneeded parameters (#710)
* [eas-cli] remove unneeded parameters

* update CHANGELOG.md
2021-10-27 12:49:56 -07:00
Quinlan Jung d6cfc887c3 [eas-cli] enforce additionalTypenames on graphql queries (#709)
* [eas-cli] enforce additional typenames

* fix stuff

* update CHANGELOG.md
2021-10-26 11:07:26 -07:00
Quinlan Jung ce41c436aa [eas-cli] add additionalTypenames for other queries (#704)
* [eas-cli] add additionalTypenames for other queries

* update CHANGELOG.md
2021-10-25 16:50:32 -07:00
Quinlan Jung ec096eaaa2 [ENG-2143][eas-cli] add additionalTypenames for credentials (#703)
* [eas-cli] add additionalTypenames for credentials

* update CHANGELOG.md
2021-10-25 16:50:10 -07:00
Dominik Sokal 9c32dd5308 don't print logs to stderr (#708)
* don't print logs to stderr

* update CHANGELOG.md
2021-10-25 14:15:54 +02:00
Dominik Sokal 69d7ee9547 fix changelog workflow - use sed for trimming string 2021-10-25 14:03:04 +02:00
Dominik Sokal e2e4cb67ca better error message when eas.json is invalid (#707)
* better error message when eas.json is invalid

* add unit test

* update CHANGELOG.md
2021-10-25 13:19:27 +02:00
Dominik Sokal 4a08ad4997 unify eas.json and build job format (#701)
* unify eas.json and build job format

* update CHANGELOG.md
2021-10-25 09:56:30 +02:00
Expo CI 107a1c12bd update CHANGELOG.md 2021-10-22 18:16:12 +00:00
Brent Vatne 8fe5268a73 v0.33.1 2021-10-22 11:10:45 -07:00
Brent Vatne faba1ad8e3 [eas-cli] Skip validating updates scripts integration, which no longer exist in SDK 43+ (#706) 2021-10-22 11:09:50 -07:00
Dominik Sokal ea4885c710 fix typo 2021-10-21 10:10:47 +02:00
Brent Vatne 4d85002d2b [eas-cli] Update the submission prompt options 2021-10-20 11:43:55 -07:00
jkh 535b17f592 [eas-cli] use config-plugin to compute runtime version (#697) 2021-10-20 10:58:59 -07:00
Expo CI e556b45d4d update CHANGELOG.md 2021-10-20 15:49:25 +00:00
jonsamp 48130dcbdc v0.33.0 2021-10-20 11:43:29 -04:00
1419ca6325 [eas-cli] Make "production" the default profile name (#677)
* build defaults to production, falls back to release

* allow submit to use production profile

* fix typo

* lint

* use a common type

* lint

* adds changelog

* adds test for getDefaultProfilesAsync

* updates default config

* self feedback

* adds channel names

* removes channel from default eas json config

* de-nest getDefaultProfilesAsync

* remove "_" variable name

* fix changelog

* incorporate feedback

* restore readme to main

* lint

* Update CHANGELOG.md

Co-authored-by: Wojciech Kozyra <[email protected]>

* Update CHANGELOG.md

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

* Update packages/eas-cli/src/build/utils/devClient.ts

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

Co-authored-by: Wojciech Kozyra <[email protected]>
Co-authored-by: Dominik Sokal <[email protected]>
2021-10-18 12:26:08 -04:00
Dominik Sokal 8513dc0645 update CHANGELOG.md 2021-10-18 14:59:13 +02:00
Dominik Sokal 70e8f8370b unify reading app.json (#692) 2021-10-18 14:58:08 +02:00
Dominik Sokal 135c90a8b8 eas submit - skip second prompt for apple id when already signed in with apple (#691)
* eas submit - skip second prompt for apple id when already signed in with apple

* update CHANGELOG.md
2021-10-18 14:37:47 +02:00
Dominik Sokal fa588c2d24 don't capitalize credentials.json 2021-10-18 12:33:35 +02:00
Dominik Sokal b4ce7a91c2 rename generic workflow -> bare workflow (#690) 2021-10-18 10:24:24 +02:00
Dominik Sokal 8ca9d66c32 fix typos 2021-10-15 16:04:54 +02:00
Dominik Sokal a86fabde59 re-generate graphql code 2021-10-15 11:44:44 +02:00
Expo CI 40a896675f update CHANGELOG.md 2021-10-15 09:05:59 +00:00
Dominik Sokal cf7848e3c5 v0.32.0 2021-10-15 11:00:07 +02:00
Bartłomiej Klocek 3d4394a9bc [eas-cli][submit] Ability to select a build from the list (#688)
* [eas-cli][submit] Ability to select a build from the list

* Add tests

* update CHANGELOG.md
2021-10-15 10:42:42 +02:00
Quinlan Jung 05f4c08d09 [eas-cli] create/list/revoke asc api keys via baconlane (#687)
* [eas-cli] manage ascApiKeys via baconlane

* expose methods

* update CHANGELOG.md

* pr feedback

* fix tests

* grammar
2021-10-14 13:22:19 -07:00
Quinlan JungandDominik Sokal a4107a86f9 [eas-cli] support ascApiKeys in ios submission (#678)
* [eas-cli] support ascApiKeys in ios submission

* format feedback

* get values from eas.json

* more pr feedback

* changelog stuff

* fyi

* tests

* fix lint

* Update CHANGELOG.md

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

* Update packages/eas-cli/src/submit/ios/IosSubmitCommand.ts

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-10-11 14:49:26 -07:00
Quinlan JungandDominik Sokal a5c695dd88 [eas-json] add asc api key parameters to eas json (#684)
* [eas-json] add asc api key parameters to eas json

* changelog stuff

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-10-11 14:41:13 -07:00
130 changed files with 4022 additions and 3316 deletions

No files matched your search

+2 -2
View File
@@ -28,8 +28,8 @@ jobs:
echo "Invalid /changelog-entry command"
exit 1
fi
CATEGORY=$( cut -d ' ' -f 1 <<< "$PARAMS" | xargs )
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | xargs )
CATEGORY=$( cut -d ' ' -f 1 <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
yarn changelog-entry "$CATEGORY" "$MESSAGE"
env:
GITHUB_PR_AUTHOR: ${{ github.event.issue.user.login }}
+72
View File
@@ -10,10 +10,82 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- Don't show commit prompt in no-commit workflow after installing `expo-dev-client`. ([#722](https://github.com/expo/eas-cli/pull/722) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Add App Store Connect Api Key graphql type and print support. ([#717](https://github.com/expo/eas-cli/pull/717) by [@quinlanj](https://github.com/quinlanj))
## [0.34.0](https://github.com/expo/eas-cli/releases/tag/0.34.0) - 2021-11-01
### 🛠 Breaking changes
- `[EAS BUILD API]` Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701) and [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal))
- Don't print logs to `stderr`. ([#708](https://github.com/expo/eas-cli/pull/708) by [@dsokal](https://github.com/dsokal))
- Remove automatic migration for legacy `eas.json` format introduced in [v0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2). ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
- Add required `cli` field in root of `eas.json` ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Better error message when eas.json is invalid. ([#707](https://github.com/expo/eas-cli/pull/707) by [@dsokal](https://github.com/dsokal))
- Fix credentials workflow for new users: add additionalTypenames for credentials. ([#703](https://github.com/expo/eas-cli/pull/703) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Add additionalTypenames for other queries. ([#704](https://github.com/expo/eas-cli/pull/704) by [@quinlanj](https://github.com/quinlanj))
- Fix errors after pulling down most recent graphql schema changes. ([#713](https://github.com/expo/eas-cli/pull/713) by [@quinlanj](https://github.com/quinlanj))
- Refactor credentials manager. ([#712](https://github.com/expo/eas-cli/pull/712) by [@quinlanj](https://github.com/quinlanj))
- Remove unneeded parameters. ([#710](https://github.com/expo/eas-cli/pull/710) by [@quinlanj](https://github.com/quinlanj))
- Enforce additionalTypenames for graphql queries. ([#709](https://github.com/expo/eas-cli/pull/709) by [@quinlanj](https://github.com/quinlanj))
## [0.33.1](https://github.com/expo/eas-cli/releases/tag/0.33.1) - 2021-10-22
### 🎉 New features
- Compute runtime version using config-plugins. ([#697](https://github.com/expo/eas-cli/pull/697) by [@jkhales](https://github.com/jkhales))
### 🐛 Bug fixes
- [eas-cli] Skip validating updates scripts integration, which no longer exist in SDK 43+ ([#706](https://github.com/expo/eas-cli/pull/706) by [@brentvatne](https://github.com/brentvatne))
## [0.33.0](https://github.com/expo/eas-cli/releases/tag/0.33.0) - 2021-10-20
### 🎉 New features
- Make "production" the default profile for building and submitting. ([#677](https://github.com/expo/eas-cli/pull/677)) by [@jonsamp](https://github.com/jonsamp)
- Previously, the default profile when running `eas build` or `eas submit` was "release". We're changing it to a more recognizable name that is consistent with our docs, which is now "production". You can always specify a profile manually with the `--profile` flag. For this major version, if you do not have a profile named "production", and still have a profile named "release", we will fall back to the "release" profile as the default, however you'll see a warning as we're going to remove that behavior in the next major release of EAS CLI.
- To upgrade, update **eas.json** to have a "production" profile in both the `build` and `submit` objects. If you already have a project set up, this will replace the existing "release" profile. After the change, eas.json should have the following profiles:
```json
{
"build": {
"production": { ... }
},
"submit": {
"production": { ... }
}
}
```
### 🐛 Bug fixes
- Skip the second prompt for Apple ID if the user is already signed in with Apple. ([#691](https://github.com/expo/eas-cli/pull/691) by [@dsokal](https://github.com/dsokal))
- Unify reading `app.json`. ([#692](https://github.com/expo/eas-cli/pull/692) by [@dsokal](https://github.com/dsokal))
## [0.32.0](https://github.com/expo/eas-cli/releases/tag/0.32.0) - 2021-10-15
### 🎉 New features
- Add App Store Connect Api Key support to iOS submissions. ([#678](https://github.com/expo/eas-cli/pull/678) by [@quinlanj](https://github.com/quinlanj))
- Create/list/revoke App Store Connect Api keys via Apple apis. ([#687](https://github.com/expo/eas-cli/pull/687) by [@quinlanj](https://github.com/quinlanj))
- Add ability to select a build from a list in `eas submit` interactive mode. ([#688](https://github.com/expo/eas-cli/pull/688) by [@barthap](https://github.com/barthap))
### 🐛 Bug fixes
- Fix printing App Store Connect URL after submission. ([#683](https://github.com/expo/eas-cli/pull/683) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
- Add App Store Connect Api Key fields to `eas.json`. ([#684](https://github.com/expo/eas-cli/pull/684) by [@quinlanj](https://github.com/quinlanj))
- Enable no-underscore-dangle eslint rule. ([#686](https://github.com/expo/eas-cli/pull/686) by [@dsokal](https://github.com/dsokal))
## [0.31.1](https://github.com/expo/eas-cli/releases/tag/0.31.1) - 2021-10-08
+1 -1
View File
@@ -4,7 +4,7 @@
2. Wait, the `release` workflow on GitHub Actions is going to take care of the rest (including publishing packages to npm and updating the changelog).
3. Check the changelog. If any of the entries have been tagged with `[EAS BUILD API]`
- Open `package.json` and note the version of `@expo/eas-build-job` used in the eas-cli.
- Find the `Publish` commit in [expo/eas-build](https://github.com/expo/eas-build/commit/) for that version. In most cases, it will be the latest `Publish` commit.
- Find the `Publish` commit in [expo/eas-build](https://github.com/expo/eas-build/commits) for that version. In most cases, it will be the latest `Publish` commit.
- Find the `eas-cli-local-build-plugin` version published in that commit.
![example publish commit](./.gh-assets/eas-build-publish-commit.png)
- Run `npm dist-tags add eas-cli-local-build-plugin@VERSION latest` with the version from the previous step.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.31.1"
"version": "0.34.1"
}
+1 -1
View File
@@ -11,7 +11,7 @@
"lint": "eslint . --ext .ts --ext .js",
"release": "lerna version",
"test": "jest",
"clean": "lerna run clean"
"clean": "lerna run clean && rm -rf node_modules coverage"
},
"devDependencies": {
"@expo/oclif-dev-cli": "1.24.0-expo",
+31 -29
View File
@@ -73,7 +73,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/account/login.js)_
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/login.js)_
## `eas account:logout`
@@ -87,7 +87,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/account/logout.js)_
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/logout.js)_
## `eas account:view`
@@ -101,7 +101,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/account/view.js)_
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/view.js)_
## `eas analytics [STATUS]`
@@ -112,7 +112,7 @@ USAGE
$ eas analytics [STATUS]
```
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/analytics.js)_
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/analytics.js)_
## `eas build`
@@ -138,14 +138,15 @@ OPTIONS
--non-interactive Run command in non-interactive mode
--profile=PROFILE_NAME [default: release] Name of the build profile from eas.json
--profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined
in eas.json.
--skip-project-configuration Skip project configuration
--[no-]wait Wait for build(s) to complete
```
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/build/index.js)_
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/index.js)_
## `eas build:cancel [BUILD_ID]`
@@ -156,7 +157,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/build/cancel.js)_
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/cancel.js)_
## `eas build:configure`
@@ -170,7 +171,7 @@ OPTIONS
-p, --platform=(android|ios|all) Platform to configure
```
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/build/configure.js)_
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/configure.js)_
## `eas build:list`
@@ -203,7 +204,7 @@ OPTIONS
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/build/list.js)_
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/list.js)_
## `eas build:view [BUILD_ID]`
@@ -217,7 +218,7 @@ OPTIONS
--json Enable JSON output, non-JSON messages will be printed to stderr
```
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/build/view.js)_
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/view.js)_
## `eas config`
@@ -232,7 +233,7 @@ OPTIONS
--profile=profile
```
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/config.js)_
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/config.js)_
## `eas credentials`
@@ -243,7 +244,7 @@ USAGE
$ eas credentials
```
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/credentials.js)_
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/credentials.js)_
## `eas device:create`
@@ -254,7 +255,7 @@ USAGE
$ eas device:create
```
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/device/create.js)_
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/create.js)_
## `eas device:list`
@@ -268,7 +269,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/device/list.js)_
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/list.js)_
## `eas device:view [UDID]`
@@ -279,7 +280,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/device/view.js)_
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/view.js)_
## `eas diagnostics`
@@ -290,7 +291,7 @@ USAGE
$ eas diagnostics
```
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/diagnostics.js)_
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/diagnostics.js)_
## `eas help [COMMAND]`
@@ -318,7 +319,7 @@ USAGE
$ eas project:info
```
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/project/info.js)_
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/project/info.js)_
## `eas project:init`
@@ -332,7 +333,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/project/init.js)_
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/project/init.js)_
## `eas secret:create`
@@ -349,7 +350,7 @@ OPTIONS
--value=value Value of the secret
```
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/secret/create.js)_
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/create.js)_
## `eas secret:delete`
@@ -366,7 +367,7 @@ DESCRIPTION
Unsure where to find the secret's ID? Run eas secrets:list
```
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/secret/delete.js)_
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/delete.js)_
## `eas secret:list`
@@ -377,7 +378,7 @@ USAGE
$ eas secret:list
```
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/secret/list.js)_
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/list.js)_
## `eas submit`
@@ -394,7 +395,7 @@ OPTIONS
--non-interactive Run command in non-interactive mode
--path=path Path to the .apk/.aab/.ipa file
--profile=profile Name of the submit profile from eas.json. Defaults to "release" if defined in
--profile=profile Name of the submit profile from eas.json. Defaults to "production" if defined in
eas.json.
--url=url App archive url
@@ -404,13 +405,14 @@ OPTIONS
--[no-]wait Wait for submission to complete
DESCRIPTION
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
See how to configure submits with eas.json:
https://docs.expo.dev/submit/eas-json/
ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/submit.js)_
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/submit.js)_
## `eas webhook:create`
@@ -429,7 +431,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/webhook/create.js)_
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/create.js)_
## `eas webhook:delete [ID]`
@@ -443,7 +445,7 @@ ARGUMENTS
ID ID of the webhook to delete
```
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/webhook/delete.js)_
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/delete.js)_
## `eas webhook:list`
@@ -457,7 +459,7 @@ OPTIONS
--event=(BUILD) Event type that triggers the webhook
```
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/webhook/list.js)_
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/list.js)_
## `eas webhook:update`
@@ -477,7 +479,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/webhook/update.js)_
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/update.js)_
## `eas webhook:view ID`
@@ -491,5 +493,5 @@ ARGUMENTS
ID ID of the webhook to view
```
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/webhook/view.js)_
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/view.js)_
<!-- commandsstop -->
+1083 -1149
View File
File diff suppressed because it is too large. Load diff
+7 -8
View File
@@ -1,8 +1,7 @@
{
"name": "eas-cli",
"type": "module",
"description": "EAS command line tool",
"version": "0.31.1",
"version": "0.34.1",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -10,15 +9,15 @@
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.25",
"@expo/config": "6.0.2",
"@expo/config-plugins": "4.0.2",
"@expo/eas-build-job": "0.2.52",
"@expo/eas-json": "^0.31.0",
"@expo/config": "6.0.3",
"@expo/config-plugins": "4.0.3",
"@expo/eas-build-job": "0.2.57",
"@expo/eas-json": "^0.34.1",
"@expo/json-file": "8.2.33",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.14",
"@expo/plugin-warn-if-update-available": "1.7.0",
"@expo/prebuild-config": "3.0.2",
"@expo/prebuild-config": "3.0.3",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.0",
"@expo/sdk-runtime-versions": "1.0.0",
@@ -178,6 +177,6 @@
"test": "jest",
"version": "yarn oclif-dev readme && node patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
"clean": "rm -rf build node_modules"
"clean": "rm -rf dist build node_modules yarn-error.log"
}
}
+3 -9
View File
@@ -4,13 +4,7 @@ import { URL } from 'url';
import { v4 as uuidv4 } from 'uuid';
import UserSettings from './user/UserSettings';
/**
* We use require() to exclude package.json from TypeScript's analysis since it lives outside
* the src directory and would change the directory structure of the emitted files
* under the build directory
*/
const packageJSON = require('../package.json');
import { easCliVersion } from './utils/easCli';
const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
darwin: 'Mac',
@@ -97,7 +91,7 @@ export function logEvent(name: string, properties: Record<string, any> = {}): vo
ensureUserIdentified();
const { userId, deviceId } = identifyData ?? {};
const commonEventProperties = { source_version: packageJSON?.version, source: 'eas cli' };
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
rudderstackClient.track({
@@ -126,7 +120,7 @@ function getRudderStackContext(): Record<string, any> {
return {
os: { name: platform, version: os.release() },
device: { type: platform, model: platform },
app: { name: 'eas cli', version: packageJSON?.version ?? undefined },
app: { name: 'eas cli', version: easCliVersion ?? undefined },
};
}
@@ -1,6 +1,5 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig, AndroidManifest } from '@expo/config-plugins';
import fs from 'fs-extra';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
@@ -10,17 +9,6 @@ import { ensureValidVersions } from '../utils/updates';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
const updatedBuildGradleContents =
AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
projectDir,
buildGradleContents
);
await fs.writeFile(buildGradlePath, updatedBuildGradleContents);
}
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
const androidManifest = await getAndroidManifestAsync(projectDir);
@@ -39,7 +27,7 @@ export async function syncUpdatesConfigurationAsync(
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
try {
await ensureUpdatesConfiguredAsync(projectDir, exp);
await ensureUpdatesConfiguredAsync(projectDir);
} catch (error) {
Log.error(
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
@@ -62,15 +50,9 @@ export async function syncUpdatesConfigurationAsync(
}
}
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
const gradleScriptApply = AndroidConfig.Updates.formatApplyLineForBuildGradle(projectDir);
throw new Error(`Missing ${gradleScriptApply} in ${buildGradlePath}`);
}
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in AndroidManifest.xml
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
const androidManifest = await getAndroidManifestAsync(projectDir);
if (!AndroidConfig.Updates.isMainApplicationMetaDataSet(androidManifest)) {
@@ -31,7 +31,7 @@ beforeEach(async () => {
await fs.mkdirp(os.tmpdir());
});
// generic workflow
// bare workflow
describe(bumpVersionAsync, () => {
it('throws an informative error when multiple flavors are detected in the android project', async () => {
await initProjectWithGradleFileAsync(
@@ -69,7 +69,7 @@ describe(bumpVersionAsync, () => {
});
it('bumps expo.android.versionCode and buildGradle versionCode when strategy = BumpStrategy.VERSION_CODE', async () => {
const nativeVersionCode = 100; // this should be overwritten by bumpVersionAsync
const fakeExp = initGenericProject({ versionCode: nativeVersionCode });
const fakeExp = initBareWorkflowProject({ versionCode: nativeVersionCode });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
@@ -91,7 +91,7 @@ describe(bumpVersionAsync, () => {
it('bumps expo.version and gradle versionCode when strategy = BumpStrategy.SHORT_VERSION', async () => {
const nativeVersionName = '1.0.0'; // this should be overwritten by bumpVersionAsync
const fakeExp = initGenericProject({ versionName: nativeVersionName });
const fakeExp = initBareWorkflowProject({ versionName: nativeVersionName });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
@@ -112,7 +112,7 @@ describe(bumpVersionAsync, () => {
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initGenericProject();
const fakeExp = initBareWorkflowProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
@@ -185,9 +185,9 @@ describe(bumpVersionInAppJsonAsync, () => {
});
describe(maybeResolveVersionsAsync, () => {
describe('generic project', () => {
describe('bare project', () => {
it('reads the versions from native code', async () => {
const exp = initGenericProject();
const exp = initBareWorkflowProject();
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/repo',
exp,
@@ -211,7 +211,7 @@ describe(maybeResolveVersionsAsync, () => {
});
});
function initGenericProject({
function initBareWorkflowProject({
versionCode = 123,
versionName = '3.0.0',
}: {
@@ -6,10 +6,10 @@ import { AndroidBuildProfile, AndroidVersionAutoIncrement } from '@expo/eas-json
import Log from '../../log';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
warnIfAndroidPackageDefinedInAppConfigForGenericProject,
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject,
} from '../../project/android/applicationId';
import { resolveWorkflowAsync } from '../../project/workflow';
import vcs from '../../vcs';
import { getVcsClient } from '../../vcs';
import { ConfigureContext } from '../context';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
@@ -21,12 +21,12 @@ export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void
return;
}
warnIfAndroidPackageDefinedInAppConfigForGenericProject(ctx.projectDir, ctx.exp);
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
const easGradlePath = AndroidConfig.EasBuild.getEasBuildGradlePath(ctx.projectDir);
await vcs.trackFileAsync(easGradlePath);
await getVcsClient().trackFileAsync(easGradlePath);
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
@@ -17,15 +17,14 @@ export function transformJob(job: Android.Job): AndroidJobInput {
artifactPath: job.artifactPath,
username: job.username,
buildType: job.buildType && transformBuildType(job.buildType),
developmentClient: job.developmentClient,
};
}
function transformBuildType(buildType: Android.BuildType): AndroidBuildType {
if (buildType === Android.BuildType.APK) {
return AndroidBuildType.Apk;
} else if (buildType === Android.BuildType.APP_BUNDLE) {
return AndroidBuildType.AppBundle;
} else {
return AndroidBuildType.DevelopmentClient;
return AndroidBuildType.AppBundle;
}
}
@@ -6,7 +6,7 @@ import slash from 'slash';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import vcs from '../../vcs';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
interface JobData {
@@ -27,7 +27,7 @@ export async function prepareJobAsync(
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildProfile: AndroidBuildProfile = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await vcs.getRootPathAsync(), ctx.projectDir)) || '.';
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const { credentials } = jobData;
const buildCredentials = credentials
? {
@@ -70,12 +70,11 @@ export async function prepareJobAsync(
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
developmentClient: buildProfile.developmentClient,
gradleCommand: buildProfile.gradleCommand,
artifactPath: buildProfile.artifactPath,
buildType,
username,
buildType: buildProfile.developmentClient ? Android.BuildType.DEVELOPMENT_CLIENT : buildType,
};
return sanitizeJob(job);
+2 -2
View File
@@ -13,7 +13,7 @@ import { uploadAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
import vcs from '../vcs';
import { getVcsClient } from '../vcs';
import { BuildContext } from './context';
import { runLocalBuildAsync } from './local';
import { MetadataContext, collectMetadataAsync } from './metadata';
@@ -68,7 +68,7 @@ export async function prepareBuildRequestForPlatformAsync<
});
}
if (await vcs.hasUncommittedChangesAsync()) {
if (await getVcsClient().isCommitRequiredAsync()) {
Log.addNewLineIfNone();
await reviewAndCommitChangesAsync(
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
+26 -13
View File
@@ -10,7 +10,8 @@ import { RequestedPlatform } from '../platform';
import { resolveWorkflowAsync } from '../project/workflow';
import { confirmAsync, promptAsync } from '../prompts';
import { ensureLoggedInAsync } from '../user/actions';
import vcs from '../vcs';
import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs';
import { configureAndroidAsync } from './android/configure';
import { ConfigureContext } from './context';
import { configureIosAsync } from './ios/configure';
@@ -37,7 +38,7 @@ export async function ensureProjectConfiguredAsync(
projectDir,
platform: requestedPlatform,
});
if (await vcs.hasUncommittedChangesAsync()) {
if (await getVcsClient().isCommitRequiredAsync()) {
error(
'Build process requires clean working tree, please commit all your changes and run `eas build` again',
{ exit: 1 }
@@ -57,7 +58,7 @@ export async function configureAsync(options: {
platform: RequestedPlatform;
projectDir: string;
}): Promise<void> {
await vcs.ensureRepoExistsAsync();
await getVcsClient().ensureRepoExistsAsync();
await maybeBailOnRepoStatusAsync();
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
@@ -86,31 +87,39 @@ export async function configureAsync(options: {
await configureIosAsync(ctx);
}
if (await vcs.hasUncommittedChangesAsync()) {
if (await getVcsClient().isCommitRequiredAsync()) {
Log.newLine();
await reviewAndCommitChangesAsync(configureCommitMessage[options.platform]);
} else {
} else if (!(await getVcsClient().hasUncommittedChangesAsync())) {
Log.newLine();
Log.withTick('No changes were necessary, the project is already configured correctly.');
}
}
const EAS_JSON_MANAGED_DEFAULT: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
},
build: {
release: {},
development: {
developmentClient: true,
distribution: 'internal',
},
preview: {
distribution: 'internal',
},
production: {},
},
submit: {
release: {},
production: {},
},
};
const EAS_JSON_GENERIC_DEFAULT: EasJson = {
const EAS_JSON_BARE_DEFAULT: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
},
build: {
release: {},
development: {
distribution: 'internal',
android: {
@@ -120,9 +129,13 @@ const EAS_JSON_GENERIC_DEFAULT: EasJson = {
buildConfiguration: 'Debug',
},
},
preview: {
distribution: 'internal',
},
production: {},
},
submit: {
release: {},
production: {},
},
};
@@ -139,11 +152,11 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
const easJson =
ctx.hasAndroidNativeProject && ctx.hasIosNativeProject
? EAS_JSON_GENERIC_DEFAULT
? EAS_JSON_BARE_DEFAULT
: EAS_JSON_MANAGED_DEFAULT;
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
await vcs.trackFileAsync(easJsonPath);
await getVcsClient().trackFileAsync(easJsonPath);
Log.withTick('Generated eas.json');
}
@@ -177,7 +190,7 @@ async function reviewAndCommitChangesAsync(
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await vcs.showDiffAsync();
await getVcsClient().showDiffAsync();
await reviewAndCommitChangesAsync(initialCommitMessage, false);
}
}
@@ -1,30 +1,16 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import fs from 'fs-extra';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import vcs from '../../vcs';
import { getVcsClient } from '../../vcs';
import { ensureValidVersions } from '../utils/updates';
import { readPlistAsync, writePlistAsync } from './plist';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
let xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, xcodeProject)) {
xcodeProject = IOSConfig.Updates.ensureBundleReactNativePhaseContainsConfigurationScript(
projectDir,
xcodeProject
);
await fs.writeFile(
IOSConfig.Paths.getPBXProjectPath(projectDir),
// eslint-disable-next-line node/no-sync
xcodeProject.writeSync()
);
}
let expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
@@ -41,7 +27,7 @@ export async function syncUpdatesConfigurationAsync(
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
try {
await ensureUpdatesConfiguredAsync(projectDir, exp);
await ensureUpdatesConfiguredAsync(projectDir);
} catch (error) {
Log.error(
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
@@ -62,15 +48,9 @@ export async function syncUpdatesConfigurationAsync(
}
}
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
const xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, xcodeProject)) {
const script = 'expo-updates/scripts/create-manifest-ios.sh';
const buildPhase = '"Bundle React Native code and images"';
throw new Error(`Path to ${script} is missing in a ${buildPhase} build phase.`);
}
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in Expo.plist
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
const expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistConfigurationSet(expoPlist)) {
throw new Error('Missing values in Expo.plist');
@@ -88,7 +68,7 @@ async function writeExpoPlistAsync(
): Promise<void> {
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
await writePlistAsync(expoPlistPath, expoPlist);
await vcs.trackFileAsync(expoPlistPath);
await getVcsClient().trackFileAsync(expoPlistPath);
}
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
@@ -49,10 +49,10 @@ describe(evaluateTemplateString, () => {
});
});
// generic workflow
// bare workflow
describe(bumpVersionAsync, () => {
it('bumps expo.ios.buildNumber and CFBundleVersion when strategy = BumpStrategy.BUILD_NUMBER', async () => {
const fakeExp = initGenericProject();
const fakeExp = initBareWorkflowProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
@@ -74,7 +74,7 @@ describe(bumpVersionAsync, () => {
});
it('bumps expo.ios.buildNumber and CFBundleVersion for non default Info.plist location', async () => {
const fakeExp = initGenericProject({ infoPlistName: 'Info2.plist' });
const fakeExp = initBareWorkflowProject({ infoPlistName: 'Info2.plist' });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
@@ -98,7 +98,7 @@ describe(bumpVersionAsync, () => {
});
it('bumps expo.version and CFBundleShortVersionString when strategy = BumpStrategy.SHORT_VERSION', async () => {
const fakeExp = initGenericProject();
const fakeExp = initBareWorkflowProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
@@ -120,7 +120,7 @@ describe(bumpVersionAsync, () => {
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initGenericProject();
const fakeExp = initBareWorkflowProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
@@ -194,9 +194,9 @@ describe(bumpVersionInAppJsonAsync, () => {
});
describe(readBuildNumberAsync, () => {
describe('generic project', () => {
describe('bare project', () => {
it('reads the build number from native code', async () => {
const exp = initGenericProject();
const exp = initBareWorkflowProject();
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
expect(buildNumber).toBe('1');
});
@@ -212,14 +212,14 @@ describe(readBuildNumberAsync, () => {
});
describe(readShortVersionAsync, () => {
describe('generic project', () => {
describe('bare project', () => {
it('reads the short version from native code', async () => {
const exp = initGenericProject();
const exp = initBareWorkflowProject();
const appVersion = await readShortVersionAsync('/repo', exp, {});
expect(appVersion).toBe('1.0.0');
});
it('evaluates interpolated build number', async () => {
const exp = initGenericProject({
const exp = initBareWorkflowProject({
appVersion: '$(CURRENT_PROJECT_VERSION)',
});
const buildNumber = await readShortVersionAsync('/repo', exp, {
@@ -231,7 +231,7 @@ describe(readShortVersionAsync, () => {
describe('managed project', () => {
it('reads the version from app config', async () => {
const exp = initGenericProject();
const exp = initBareWorkflowProject();
const appVersion = await readShortVersionAsync('/repo', exp, {});
expect(appVersion).toBe('1.0.0');
});
@@ -286,7 +286,7 @@ describe(getInfoPlistPath, () => {
});
});
function initGenericProject({
function initBareWorkflowProject({
appVersion = '1.0.0',
version = '1',
infoPlistName = 'Info.plist',
+2 -2
View File
@@ -6,7 +6,7 @@ import type { XCBuildConfiguration } from 'xcode';
import Log from '../../log';
import {
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
warnIfBundleIdentifierDefinedInAppConfigForGenericProject,
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject,
} from '../../project/ios/bundleIdentifier';
import { resolveWorkflowAsync } from '../../project/workflow';
import { ConfigureContext } from '../context';
@@ -20,7 +20,7 @@ export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
return;
}
warnIfBundleIdentifierDefinedInAppConfigForGenericProject(ctx.projectDir, ctx.exp);
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
+3 -28
View File
@@ -1,12 +1,7 @@
import { Env, Ios } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
import {
DistributionType,
IosBuildType,
IosJobInput,
IosJobSecretsInput,
} from '../../graphql/generated';
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
import { transformProjectArchive, transformWorkflow } from '../graphql';
export function transformJob(job: Ios.Job): IosJobInput {
@@ -16,30 +11,18 @@ export function transformJob(job: Ios.Job): IosJobInput {
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
updates: job.updates,
distribution: job.distribution && transformDistributionType(job.distribution),
secrets: transformIosSecrets(job.secrets),
builderEnvironment: job.builderEnvironment,
cache: job.cache,
scheme: job.scheme,
buildConfiguration: job.buildConfiguration,
artifactPath: job.artifactPath,
buildType: job.buildType && transformBuildType(job.buildType),
username: job.username,
developmentClient: job.developmentClient,
simulator: job.simulator,
};
}
function transformDistributionType(distributionType: Ios.DistributionType): DistributionType {
if (distributionType === 'store') {
return DistributionType.Store;
} else if (distributionType === 'internal') {
return DistributionType.Internal;
} else {
return DistributionType.Simulator;
}
}
function transformIosSecrets(secrets: {
buildCredentials?: Ios.BuildCredentials;
env?: Env;
@@ -61,11 +44,3 @@ function transformIosSecrets(secrets: {
environmentSecrets: secrets.env,
};
}
function transformBuildType(buildType: Ios.BuildType): IosBuildType {
if (buildType === Ios.BuildType.DEVELOPMENT_CLIENT) {
return IosBuildType.DevelopmentClient;
} else {
return IosBuildType.Release;
}
}
+4 -8
View File
@@ -5,7 +5,7 @@ import slash from 'slash';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import vcs from '../../vcs';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
interface JobData {
@@ -25,7 +25,7 @@ export async function prepareJobAsync(
jobData: JobData
): Promise<Job> {
const projectRootDirectory =
slash(path.relative(await vcs.getRootPathAsync(), ctx.projectDir)) || '.';
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
if (jobData.credentials) {
@@ -40,7 +40,6 @@ export async function prepareJobAsync(
platform: Platform.IOS,
projectArchive: jobData.projectArchive,
projectRootDirectory,
distribution: ctx.buildProfile.simulator ? 'simulator' : ctx.buildProfile.distribution,
builderEnvironment: {
image: ctx.buildProfile.image,
node: ctx.buildProfile.node,
@@ -61,14 +60,11 @@ export async function prepareJobAsync(
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
developmentClient: ctx.buildProfile.developmentClient,
simulator: ctx.buildProfile.simulator,
scheme: jobData.buildScheme,
buildConfiguration: ctx.buildProfile.buildConfiguration,
artifactPath: ctx.buildProfile.artifactPath,
buildType: ctx.buildProfile.developmentClient
? Ios.BuildType.DEVELOPMENT_CLIENT
: Ios.BuildType.RELEASE,
username,
};
return sanitizeJob(job);
+5 -10
View File
@@ -7,7 +7,8 @@ import { GradleBuildContext } from '../project/android/gradle';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
import { getUsername } from '../project/projectUtils';
import { ensureLoggedInAsync } from '../user/actions';
import vcs from '../vcs';
import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs';
import {
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
@@ -21,13 +22,6 @@ import {
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
import { isExpoUpdatesInstalled } from './utils/updates';
/**
* We use require() to exclude package.json from TypeScript's analysis since it lives outside
* the src directory and would change the directory structure of the emitted files
* under the build directory
*/
const packageJSON = require('../../package.json');
export type MetadataContext<T extends Platform> = T extends Platform.ANDROID
? AndroidMetadataContext
: IosMetadataContext;
@@ -53,7 +47,7 @@ export async function collectMetadataAsync<T extends Platform>(
const metadata = {
trackingContext: ctx.trackingCtx,
...(await maybeResolveVersionsAsync(ctx, platformContext)),
cliVersion: packageJSON.version,
cliVersion: easCliVersion,
workflow: ctx.workflow,
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
@@ -63,7 +57,8 @@ export async function collectMetadataAsync<T extends Platform>(
appName: ctx.exp.name,
appIdentifier: await resolveAppIdentifierAsync(ctx, platformContext),
buildProfile: ctx.buildProfileName,
gitCommitHash: await vcs.getCommitHashAsync(),
gitCommitHash: await getVcsClient().getCommitHashAsync(),
isGitWorkingTreeDirty: await getVcsClient().hasUncommittedChangesAsync(),
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
+12 -3
View File
@@ -1,6 +1,6 @@
import * as ExpoConfig from '@expo/config';
import JsonFile from '@expo/json-file';
import assert from 'assert';
import fs from 'fs-extra';
export async function updateAppJsonConfigAsync(
{
@@ -15,10 +15,19 @@ export async function updateAppJsonConfigAsync(
const paths = ExpoConfig.getConfigFilePaths(projectDir);
assert(paths.staticConfigPath, "can't update dynamic config");
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
const rawStaticConfig = readAppJson(paths.staticConfigPath);
rawStaticConfig.expo = rawStaticConfig.expo ?? {};
modifyConfig(rawStaticConfig.expo);
await fs.writeJson(paths.staticConfigPath, rawStaticConfig, { spaces: 2 });
await JsonFile.writeAsync(paths.staticConfigPath, rawStaticConfig, { json5: false });
modifyConfig(exp);
}
// TODO: remove this once @expo/config exports getStaticConfig
export function readAppJson(appJsonPath: string): any {
const config = JsonFile.read(appJsonPath, { json5: true });
if (config) {
return config as any;
}
throw new Error(`Failed to read app.json`);
}
+24 -24
View File
@@ -1,5 +1,5 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import resolveFrom from 'resolve-from';
@@ -9,40 +9,38 @@ import Log, { learnMore } from '../../log';
import { appPlatformDisplayNames } from '../../platform';
import { resolveWorkflowAsync } from '../../project/workflow';
import { confirmAsync } from '../../prompts';
import { expoCommandAsync } from '../../utils/expoCommand';
import zipObject from '../../utils/expodash/zipObject';
import { expoCommandAsync } from '../../utils/expoCli';
import { ProfileData } from '../../utils/profiles';
import { getVcsClient } from '../../vcs';
import { reviewAndCommitChangesAsync } from './repository';
export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir,
platforms,
profile,
nonInteractive = false,
buildProfiles = [],
}: {
projectDir: string;
platforms: Platform[];
profile: string;
nonInteractive?: boolean;
buildProfiles?: ProfileData<BuildProfile>[];
}): Promise<void> {
if (await isExpoDevClientInstalledAsync(projectDir)) {
return;
}
const easJsonReader = new EasJsonReader(projectDir);
const devClientPerPlatformList = await Promise.all(
platforms.map(async platform => {
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, profile);
return buildProfile.developmentClient ?? false;
})
const buildProfilesWithDevelopmentClientRequired = buildProfiles.filter(
buildProfile => buildProfile.profile.developmentClient
);
const isDevClientRequired = devClientPerPlatformList.some(i => i);
if (!isDevClientRequired) {
const isDevelopmentClientRequired = buildProfilesWithDevelopmentClientRequired.some(Boolean);
if (!isDevelopmentClientRequired) {
return;
}
const devClientPerPlatform = zipObject(platforms, devClientPerPlatformList);
const platformsToCheck = platforms.filter(platform => devClientPerPlatform[platform]);
const platformsToCheck = buildProfilesWithDevelopmentClientRequired.map(
({ platform }) => platform
);
const workflowPerPlatformList = await Promise.all(
platformsToCheck.map(platform => resolveWorkflowAsync(projectDir, platform))
);
@@ -67,15 +65,15 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
if (install) {
await installExpoDevClientAsync(projectDir, { nonInteractive });
} else {
error(`Install ${chalk.bold('expo-dev-client')} on your own and come back later.`, {
error(`Install ${chalk.bold('expo-dev-client')} manually and come back later.`, {
exit: 1,
});
}
} else {
Log.warn(`Unfortunately, you need to install ${chalk.bold('expo-dev-client')} on your own.`);
Log.warn(`You'll need to install ${chalk.bold('expo-dev-client')} manually.`);
Log.warn(
learnMore('https://docs.expo.dev/clients/installation/', {
learnMoreMessage: 'See installation instructions on how to do it',
learnMoreMessage: 'See installation instructions on how to do it.',
dim: false,
})
);
@@ -110,7 +108,9 @@ async function installExpoDevClientAsync(
Log.newLine();
await expoCommandAsync(projectDir, ['install', 'expo-dev-client']);
Log.newLine();
await reviewAndCommitChangesAsync('Install expo-dev-client', {
nonInteractive,
});
if (await getVcsClient().isCommitRequiredAsync()) {
await reviewAndCommitChangesAsync('Install expo-dev-client', {
nonInteractive,
});
}
}
+10 -7
View File
@@ -10,10 +10,10 @@ import { confirmAsync, promptAsync } from '../../prompts';
import { formatBytes } from '../../utils/files';
import { getTmpDirectory } from '../../utils/paths';
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
import vcs from '../../vcs';
import { getVcsClient } from '../../vcs';
export async function maybeBailOnRepoStatusAsync(): Promise<void> {
if (!(await vcs.hasUncommittedChangesAsync())) {
if (!(await getVcsClient().isCommitRequiredAsync())) {
return;
}
Log.addNewLineIfNone();
@@ -33,7 +33,7 @@ export async function maybeBailOnRepoStatusAsync(): Promise<void> {
}
export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<void> {
if (!(await vcs.hasUncommittedChangesAsync())) {
if (!(await getVcsClient().isCommitRequiredAsync())) {
return;
}
Log.addNewLineIfNone();
@@ -70,7 +70,7 @@ export async function commitPromptAsync({
initial: initialCommitMessage,
validate: (input: string) => input !== '',
});
await vcs.commitAsync({ commitAllFiles, commitMessage: message });
await getVcsClient().commitAsync({ commitAllFiles, commitMessage: message });
}
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
@@ -95,7 +95,7 @@ export async function makeProjectTarballAsync(): Promise<{ path: string; size: n
startTimer(compressTimerLabel);
try {
await vcs.makeShallowCopyAsync(shallowClonePath);
await getVcsClient().makeShallowCopyAsync(shallowClonePath);
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
'.',
]);
@@ -133,7 +133,10 @@ export async function reviewAndCommitChangesAsync(
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
): Promise<void> {
if (process.env.EAS_BUILD_AUTOCOMMIT) {
await vcs.commitAsync({ commitMessage: initialCommitMessage, commitAllFiles: false });
await getVcsClient().commitAsync({
commitMessage: initialCommitMessage,
commitAllFiles: false,
});
Log.withTick('Committed changes.');
return;
}
@@ -166,7 +169,7 @@ export async function reviewAndCommitChangesAsync(
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes.');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await vcs.showDiffAsync();
await getVcsClient().showDiffAsync();
await reviewAndCommitChangesAsync(initialCommitMessage, {
nonInteractive,
askedFirstTime: false,
+4 -4
View File
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
import path from 'path';
import Log, { learnMore } from '../log';
import vcs from '../vcs';
import { getVcsClient } from '../vcs';
import { BuildContext } from './context';
export function checkNodeEnvVariable(ctx: BuildContext<Platform>): void {
@@ -25,12 +25,12 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
if (!googleServicesFilePath) {
return;
}
const rootDir = path.normalize(await vcs.getRootPathAsync());
const rootDir = path.normalize(await getVcsClient().getRootPathAsync());
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
if (
(await fs.pathExists(absGoogleServicesFilePath)) &&
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
(await vcs.isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
(await getVcsClient().isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
) {
Log.warn(
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
@@ -41,7 +41,7 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
)}`
);
Log.warn(
'If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting GOOGLE_SERVICES_FILE in your build profile in eas.json to any non-falsy value.'
`If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting your build profile's env.GOOGLE_SERVICES_FILE in eas.json to any non-empty string.`
);
Log.newLine();
}
@@ -1,4 +1,6 @@
import { EasJsonReader } from '@expo/eas-json';
import { Command } from '@oclif/command';
import semver from 'semver';
import {
AnalyticsEvent,
@@ -6,8 +8,13 @@ import {
initAsync as initAnalyticsAsync,
logEvent,
} from '../analytics';
import Log from '../log';
import { findProjectRootAsync } from '../project/projectUtils';
import { getUserAsync } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { setVcsClient } from '../vcs';
import GitClient from '../vcs/clients/git';
export default abstract class EasCommand extends Command {
/**
@@ -20,7 +27,10 @@ export default abstract class EasCommand extends Command {
// eslint-disable-next-line async-protect/async-suffix
async run(): Promise<any> {
EasJsonReader.setLog(Log);
await initAnalyticsAsync();
await this.applyCliConfigAsync();
if (this.requiresAuthentication) {
const { flags } = this.parse();
@@ -42,4 +52,18 @@ export default abstract class EasCommand extends Command {
await flushAnalyticsAsync();
return super.finally(err);
}
private async applyCliConfigAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const easJsonReader = new EasJsonReader(projectDir);
const config = await easJsonReader.getCliConfigAsync();
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
throw new Error(
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
);
}
if (config?.requireCommit) {
setVcsClient(new GitClient());
}
}
}
@@ -17,7 +17,7 @@ import {
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import vcs from '../../vcs';
import { getVcsClient } from '../../vcs';
export async function createUpdateBranchOnAppAsync({
appId,
@@ -90,7 +90,8 @@ export default class BranchCreate extends EasCommand {
name: 'name',
message: 'Please name the branch:',
initial:
(await vcs.getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`,
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substr(2, 4)}`,
validate: value => (value ? true : validationMessage),
}));
}
@@ -43,7 +43,8 @@ async function getBranchInfoAsync({
{
appId,
name,
}
},
{ additionalTypenames: ['UpdateBranch'] }
)
.toPromise()
);
+2 -1
View File
@@ -44,7 +44,8 @@ export async function listBranchesAsync({
{
appId: projectId,
limit: BRANCHES_LIMIT,
}
},
{ additionalTypenames: ['UpdateBranch'] }
)
.toPromise()
);
+26 -55
View File
@@ -1,5 +1,6 @@
import { ExpoConfig, getConfig, getDefaultTarget } from '@expo/config';
import { getRuntimeVersionForSDKVersion } from '@expo/sdk-runtime-versions';
import { ExpoConfig, getConfig } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { flags } from '@oclif/command';
import assert from 'assert';
import chalk from 'chalk';
@@ -28,11 +29,12 @@ import {
collectAssetsAsync,
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync, selectAsync } from '../../prompts';
import { formatUpdate } from '../../update/utils';
import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
import vcs from '../../vcs';
import { getVcsClient } from '../../vcs';
import { createUpdateBranchOnAppAsync } from './create';
import { listBranchesAsync } from './list';
import { viewUpdateBranchAsync } from './view';
@@ -60,7 +62,8 @@ async function getUpdateGroupAsync({
`,
{
group,
}
},
{ additionalTypenames: ['Update'] }
)
.toPromise()
);
@@ -168,13 +171,14 @@ export default class BranchPublish extends EasCommand {
isPublicConfig: true,
});
const runtimeVersions = getRuntimeVersionObject(exp, platformFlag, projectDir);
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
const projectId = await getProjectIdAsync(exp);
if (!branchName && autoFlag) {
branchName =
(await vcs.getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`;
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substr(2, 4)}`;
}
if (!branchName) {
@@ -297,7 +301,7 @@ export default class BranchPublish extends EasCommand {
}
if (!message && autoFlag) {
message = (await vcs.getLastCommitMessageAsync())?.trim();
message = (await getVcsClient().getLastCommitMessageAsync())?.trim();
}
if (!message) {
@@ -311,7 +315,7 @@ export default class BranchPublish extends EasCommand {
message: `Please enter a publication message.`,
initial: republish
? `Republish "${oldMessage!}" - group: ${group}`
: (await vcs.getLastCommitMessageAsync())?.trim(),
: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
validate: value => (value ? true : validationMessage),
}));
}
@@ -388,59 +392,26 @@ export default class BranchPublish extends EasCommand {
}
}
function getRuntimeVersionObject(
async function getRuntimeVersionObjectAsync(
exp: ExpoConfig,
platformFlag: string,
platformFlag: PlatformFlag,
projectDir: string
): Record<string, string> {
let { runtimeVersion: defaultRuntimeVersion, sdkVersion } = exp;
): Promise<Record<string, string>> {
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
// When a SDK version is supplied instead of a runtime version and we're in the managed workflow
// construct the runtimeVersion with special meaning indicating that the runtime is an
// Expo SDK preset runtime that can be launched in Expo Go.
const isManagedProject = getDefaultTarget(projectDir) === 'managed';
if (!defaultRuntimeVersion && sdkVersion && isManagedProject) {
Log.withTick('Generating runtime version from sdk version');
defaultRuntimeVersion = getRuntimeVersionForSDKVersion(sdkVersion);
}
const iOSRuntimeVersion = (exp as any).ios?.runtimeVersion; // TODO-JJ remove cast to any
const androidRuntimeVersion = (exp as any).android?.runtimeVersion; // TODO-JJ remove cast to any
let runtimeVersions: Record<string, string>;
switch (platformFlag) {
case 'ios': {
runtimeVersions = {
ios: iOSRuntimeVersion ?? defaultRuntimeVersion,
};
break;
for (const platform of platforms) {
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
if (isPolicy) {
const isManaged = (await resolveWorkflowAsync(projectDir, platform)) === Workflow.MANAGED;
if (!isManaged) {
throw new Error('Runtime version policies are only supported in the managed workflow.');
}
}
case 'android': {
runtimeVersions = {
android: androidRuntimeVersion ?? defaultRuntimeVersion,
};
break;
}
case 'all': {
runtimeVersions = {
ios: iOSRuntimeVersion ?? defaultRuntimeVersion,
android: androidRuntimeVersion ?? defaultRuntimeVersion,
};
break;
}
default:
throw new Error('Platform flag must be "ios", "android", or "all"');
}
if (Object.values(runtimeVersions).some(runtime => !runtime)) {
throw new Error(
"Couldn't find a 'runtimeVersion' for every platform. Please specify it under the 'expo' key in 'app.json'"
);
}
if (Object.values(runtimeVersions).some(runtime => typeof runtime !== 'string')) {
throw new Error(
`Please ensure that all of the runtime versions defined in the app.json are strings.`
);
}
return runtimeVersions;
return Object.fromEntries(
platforms.map(platform => [platform, Updates.getRuntimeVersion(exp, platform)])
);
}
function formatUpdateTitle(
+2 -1
View File
@@ -58,7 +58,8 @@ export async function viewUpdateBranchAsync({
appId,
name,
limit: PAGE_LIMIT,
}
},
{ additionalTypenames: ['UpdateBranch', 'Update'] }
)
.toPromise()
);
+102 -72
View File
@@ -1,13 +1,10 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import {
EasJsonReader,
hasMismatchedExtendsAsync,
isUsingDeprecatedFormatAsync,
migrateAsync,
} from '@expo/eas-json';
import { BuildProfile, EasJsonReader } from '@expo/eas-json';
import { flags } from '@oclif/command';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import figures from 'figures';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { prepareAndroidBuildAsync } from '../../build/android/build';
@@ -27,7 +24,7 @@ import {
SubmissionFragment,
} from '../../graphql/generated';
import { toAppPlatform, toPlatform } from '../../graphql/types/AppPlatform';
import Log from '../../log';
import Log, { learnMore, link } from '../../log';
import {
RequestedPlatform,
appPlatformDisplayNames,
@@ -41,21 +38,24 @@ import {
} from '../../project/isEasEnabledForProject';
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
import { findProjectRootAsync } from '../../project/projectUtils';
import { confirmAsync } from '../../prompts';
import { selectAsync } from '../../prompts';
import { createSubmissionContextAsync } from '../../submit/context';
import {
submitAsync,
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../../submit/submit';
import { printSubmissionDetailsUrls } from '../../submit/utils/urls';
import { easCliVersion } from '../../utils/easCli';
import { enableJsonOutput } from '../../utils/json';
import vcs from '../../vcs';
import { ProfileData, getProfilesAsync } from '../../utils/profiles';
import { getVcsClient, setVcsClient } from '../../vcs';
import GitClient from '../../vcs/clients/git';
interface RawBuildFlags {
platform?: string;
'skip-credentials-check': boolean;
'skip-project-configuration': boolean;
profile: string;
profile?: string;
'non-interactive': boolean;
local: boolean;
wait: boolean;
@@ -68,7 +68,7 @@ interface RawBuildFlags {
interface BuildFlags {
requestedPlatform: RequestedPlatform;
skipProjectConfiguration: boolean;
profile: string;
profile: string | null;
nonInteractive: boolean;
local: boolean;
wait: boolean;
@@ -99,8 +99,8 @@ export default class Build extends EasCommand {
description: 'Skip project configuration',
}),
profile: flags.string({
default: 'release',
description: 'Name of the build profile from eas.json',
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
}),
'non-interactive': flags.boolean({
@@ -146,50 +146,66 @@ export default class Build extends EasCommand {
const projectDir = await findProjectRootAsync();
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await vcs.ensureRepoExistsAsync();
await getVcsClient().ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
const platforms = toPlatforms(requestedPlatform);
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
const easJsonReader = new EasJsonReader(projectDir);
const buildProfiles = await getProfilesAsync({
platforms,
projectDir,
profile: flags.profile,
nonInteractive: flags.nonInteractive,
profileName: flags.profile,
async readProfileAsync(platform, profileName) {
return await easJsonReader.readBuildProfileAsync(platform, profileName);
},
});
const startedBuilds: BuildFragment[] = [];
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir,
nonInteractive: flags.nonInteractive,
buildProfiles,
});
const startedBuilds: {
build: BuildFragment;
buildProfile: ProfileData<BuildProfile>;
}[] = [];
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
for (const platform of platforms) {
for (const buildProfile of buildProfiles) {
const { build: maybeBuild, buildCtx } = await this.prepareAndStartBuildAsync({
projectDir,
platform,
flags,
moreBuilds: platforms.length > 1,
buildProfile,
});
if (maybeBuild) {
startedBuilds.push(maybeBuild);
startedBuilds.push({ build: maybeBuild, buildProfile });
}
buildCtxByPlatform[toAppPlatform(platform)] = buildCtx;
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
}
if (flags.local) {
return;
}
Log.newLine();
printLogsUrls(startedBuilds);
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
Log.newLine();
const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) {
for (const build of startedBuilds) {
for (const startedBuild of startedBuilds) {
const submission = await this.prepareAndStartSubmissionAsync({
build,
credentialsCtx: nullthrows(buildCtxByPlatform[build.platform]?.credentialsCtx),
build: startedBuild.build,
credentialsCtx: nullthrows(
buildCtxByPlatform[startedBuild.build.platform]?.credentialsCtx
),
flags,
moreBuilds: startedBuilds.length > 1,
projectDir,
buildProfile: startedBuild.buildProfile,
});
submissions.push(submission);
}
@@ -203,7 +219,7 @@ export default class Build extends EasCommand {
return;
}
const builds = await waitForBuildEndAsync(startedBuilds.map(build => build.id));
const builds = await waitForBuildEndAsync(startedBuilds.map(({ build }) => build.id));
printBuildResults(builds, flags.json);
const haveAllBuildsFailedOrCanceled = builds.every(
@@ -252,7 +268,7 @@ export default class Build extends EasCommand {
return {
requestedPlatform,
skipProjectConfiguration: flags['skip-project-configuration'],
profile,
profile: profile ?? null,
nonInteractive,
local: flags['local'],
wait: flags['wait'],
@@ -263,34 +279,31 @@ export default class Build extends EasCommand {
};
}
private async prepareAndStartBuildAsync<T extends Platform>({
private async prepareAndStartBuildAsync({
projectDir,
platform,
flags,
moreBuilds,
buildProfile,
}: {
projectDir: string;
platform: T;
flags: BuildFlags;
moreBuilds: boolean;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<T> }> {
const easJsonReader = new EasJsonReader(projectDir);
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, flags.profile);
buildProfile: ProfileData<BuildProfile>;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({
buildProfileName: flags.profile,
buildProfileName: buildProfile.profileName,
clearCache: flags.clearCache,
buildProfile,
buildProfile: buildProfile.profile,
local: flags.local,
nonInteractive: flags.nonInteractive,
platform,
platform: buildProfile.platform,
projectDir,
skipProjectConfiguration: flags.skipProjectConfiguration,
});
if (moreBuilds) {
Log.newLine();
const appPlatform = toAppPlatform(platform);
const appPlatform = toAppPlatform(buildProfile.platform);
Log.log(
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
`${appPlatformDisplayNames[appPlatform]} build`
@@ -330,16 +343,17 @@ export default class Build extends EasCommand {
flags,
moreBuilds,
projectDir,
buildProfile,
}: {
build: BuildFragment;
credentialsCtx: CredentialsContext;
flags: BuildFlags;
moreBuilds: boolean;
projectDir: string;
buildProfile: ProfileData<BuildProfile>;
}): Promise<SubmissionFragment> {
const easJsonReader = new EasJsonReader(projectDir);
const platform = toPlatform(build.platform);
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, flags.profile);
const submitProfile = await easJsonReader.readSubmitProfileAsync(platform, flags.submitProfile);
const submissionCtx = await createSubmissionContextAsync({
platform,
@@ -348,7 +362,7 @@ export default class Build extends EasCommand {
profile: submitProfile,
archiveFlags: { id: build.id },
nonInteractive: flags.nonInteractive,
env: buildProfile.env,
env: buildProfile.profile.env,
credentialsCtx,
});
@@ -378,36 +392,52 @@ export async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
if (await isUsingDeprecatedFormatAsync(projectDir)) {
const hasMismatchedExtendsKeys = await hasMismatchedExtendsAsync(projectDir);
if (nonInteractive) {
Log.error('We detected that your eas.json is using a deprecated format.');
Log.error(
'We will convert it automatically if run this command without --non-interactive flag. Alternatively, you can update eas.json manually according to https://docs.expo.dev/build/eas-json'
);
error('Unsupported eas.json format', { exit: 1 });
}
if (!(await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir)))) {
return;
}
const easJsonReader = new EasJsonReader(projectDir);
const rawEasJson = await easJsonReader.readRawAsync();
if (rawEasJson?.cli) {
return;
}
const confirm = await confirmAsync({
message:
'We detected that your eas.json is using a deprecated format, do you want to migrate to the new format?',
});
if (confirm) {
await migrateAsync(projectDir);
if (hasMismatchedExtendsKeys) {
Log.warn(
'"extends" keyword can only be migrated automatically to the new format if both Android and iOS profiles extend base profiles with the same names for both platforms'
);
Log.warn(
'Migration was successful, but you need to manually adjust the extend rules for your profiles'
);
error('Fix eas.json manually', { exit: 1 });
}
} else {
Log.error(
"Aborting, update your eas.json according to https://docs.expo.dev/build/eas-json and run 'eas build' again"
);
error('Unsupported eas.json format', { exit: 1 });
}
if (nonInteractive) {
Log.warn(
`${
figures.warning
} Action required: the default behavior of EAS CLI has changed and your eas.json must be updated to remove ambiguity around which Git integration workflow to use. Refer to ${link(
'https://expo.fyi/eas-vcs-workflow'
)} for more information.`
);
Log.warn(
'This warning will become an error in an upcoming EAS CLI release. For now, we will proceed with the old default behavior to avoid disruption of your builds.'
);
setVcsClient(new GitClient());
return;
}
Log.log(
`${chalk.bold(
'eas-cli@>=0.34.0 no longer requires that you commit changes to Git before starting a build.'
)} ${learnMore('https://expo.fyi/eas-vcs-workflow')}`
);
Log.log(
`If you want to continue using the Git integration, you can opt in with ${chalk.bold(
'cli.requireCommit'
)} in ${chalk.bold('eas.json')} or with the following prompt.`
);
Log.newLine();
const mode = await selectAsync('Select your preferred Git integration', [
{ title: 'Require changes to be committed in Git (old default)', value: 'requireCommit' },
{ title: 'Allow builds with dirty Git working tree (new default)', value: 'noCommit' },
]);
rawEasJson.cli =
mode === 'requireCommit'
? { version: `>= ${easCliVersion}`, requireCommit: true }
: { version: `>= ${easCliVersion}` };
await fs.writeJSON(EasJsonReader.formatEasJsonPath(projectDir), rawEasJson, { spaces: 2 });
if (mode === 'requireCommit') {
setVcsClient(new GitClient());
}
}
@@ -49,7 +49,8 @@ async function getChannelByNameForAppAsync({
}
}
`,
{ appId, channelName }
{ appId, channelName },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch'] }
)
.toPromise()
);
@@ -58,7 +58,8 @@ async function getAllUpdateChannelForAppAsync({
}
}
`,
{ appId, offset: 0, limit: CHANNEL_LIMIT }
{ appId, offset: 0, limit: CHANNEL_LIMIT },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
)
.toPromise()
);
@@ -133,7 +133,8 @@ export async function getUpdateChannelByNameForAppAsync({
}
}
`,
{ appId, channelName }
{ appId, channelName },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
)
.toPromise()
);
+2 -3
View File
@@ -2,8 +2,7 @@ import envinfo from 'envinfo';
import EasCommand from '../commandUtils/EasCommand';
import Log from '../log';
const packageJSON = require('../../package.json');
import { easCliVersion } from '../utils/easCli';
export default class Diagnostics extends EasCommand {
static description = 'log environment info to the console';
@@ -30,7 +29,7 @@ export default class Diagnostics extends EasCommand {
npmGlobalPackages: ['eas-cli', 'expo-cli'],
},
{
title: `EAS CLI ${packageJSON.version} environment info`,
title: `EAS CLI ${easCliVersion} environment info`,
}
);
Log.log(info);
@@ -22,7 +22,8 @@ async function projectInfoByIdAsync(appId: string): Promise<AppInfoQuery> {
}
}
`,
{ appId }
{ appId },
{ additionalTypenames: ['App'] }
)
.toPromise()
);
+16 -8
View File
@@ -20,6 +20,7 @@ import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils
import { SubmitArchiveFlags, createSubmissionContextAsync } from '../submit/context';
import { submitAsync, waitToCompleteAsync } from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { getProfilesAsync } from '../utils/profiles';
interface RawFlags {
platform?: string;
@@ -54,7 +55,7 @@ See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit
}),
profile: flags.string({
description:
'Name of the submit profile from eas.json. Defaults to "release" if defined in eas.json.',
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
latest: flags.boolean({
description: 'Submit the latest build for specified platform',
@@ -101,23 +102,30 @@ See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit
return;
}
const easJsonReader = new EasJsonReader(projectDir);
const platforms = toPlatforms(flags.requestedPlatform);
const easJsonReader = new EasJsonReader(projectDir);
const submissionProfiles = await getProfilesAsync({
platforms,
profileName: flags.profile,
async readProfileAsync(platform, profileName) {
return await easJsonReader.readSubmitProfileAsync(platform, profileName);
},
});
const submissions: SubmissionFragment[] = [];
for (const platform of platforms) {
const profile = await easJsonReader.readSubmitProfileAsync(platform, flags.profile);
for (const submissionProfile of submissionProfiles) {
const ctx = await createSubmissionContextAsync({
platform,
platform: submissionProfile.platform,
projectDir,
projectId,
profile,
profile: submissionProfile.profile,
archiveFlags: flags.archiveFlags,
nonInteractive: flags.nonInteractive,
});
if (platforms.length > 1) {
if (submissionProfiles.length > 1) {
Log.newLine();
const appPlatform = toAppPlatform(platform);
const appPlatform = toAppPlatform(submissionProfile.platform);
Log.log(
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
`${appPlatformDisplayNames[appPlatform]} submission`
+2 -1
View File
@@ -39,7 +39,8 @@ export async function viewUpdateAsync({
`,
{
groupId,
}
},
{ additionalTypenames: ['Update'] }
)
.toPromise()
);
@@ -29,6 +29,9 @@ export const GoogleServiceAccountKeyQuery = {
`,
{
accountName,
},
{
additionalTypenames: ['GoogleServiceAccountKey'],
}
)
.toPromise()
@@ -6,8 +6,8 @@ import { AndroidAppBuildCredentialsFragment, IosDistributionType } from '../../g
import Log from '../../log';
import { findApplicationTarget, findTargetByName } from '../../project/ios/target';
import zipObject from '../../utils/expodash/zipObject';
import vcs from '../../vcs';
import GitClient from '../../vcs/git';
import { getVcsClient } from '../../vcs';
import GitClient from '../../vcs/clients/git';
import { CredentialsContext } from '../context';
import { App, Target, TargetCredentials } from '../ios/types';
import { readRawAsync } from './read';
@@ -294,8 +294,9 @@ async function updateFileAsync(
}
async function isFileUntrackedAsync(path: string): Promise<boolean> {
if (vcs instanceof GitClient) {
return await vcs.isFileUntrackedAsync(path);
const vcsClient = getVcsClient();
if (vcsClient instanceof GitClient) {
return await vcsClient.isFileUntrackedAsync(path);
}
return false;
}
@@ -5,3 +5,9 @@ export class MissingCredentialsNonInteractiveError extends Error {
);
}
}
export class MissingCredentialsError extends Error {
constructor(message?: string) {
super(message ?? 'Credentials are not set up.');
}
}
@@ -93,7 +93,7 @@ export default class IosCredentialsProvider {
}
const confirmSetup = await confirmAsync({
message: `Would you like to setup Push Notifications for your project?`,
message: `Would you like to set up Push Notifications for your project?`,
});
if (!confirmSetup) {
return null;
@@ -0,0 +1,71 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import * as uuid from 'uuid';
import Log, { learnMore } from '../../../log';
import { promptAsync } from '../../../prompts';
import { getCredentialsFromUserAsync } from '../../utils/promptForCredentials';
import { AscApiKey } from '../appstore/Credentials.types';
import { AscApiKeyPath, MinimalAscApiKey, ascApiKeyMetadataSchema } from '../credentials';
export async function promptForAscApiKeyAsync(): Promise<AscApiKeyPath> {
Log.log(
chalk.bold('An App Store Connect Api key is required to upload your app to the Apple App Store')
);
Log.log(
`If you're not sure what this is or how to create one, ${learnMore(
'https://expo.fyi/creating-asc-api-key'
)}`
);
const { keyP8Path } = await promptAsync({
type: 'text',
name: 'keyP8Path',
message: 'Path to App Store Connect Api Key:',
initial: 'AuthKey_ABCD.p8',
// eslint-disable-next-line async-protect/async-suffix
validate: async (filePath: string) => {
try {
const stats = await fs.stat(filePath);
if (stats.isFile()) {
return true;
}
return 'Input is not a file.';
} catch {
return 'File does not exist.';
}
},
});
const regex = /^AuthKey_(?<keyId>\w+)\.p8$/; // Common ASC Api file name downloaded from Apple
const bestEffortKeyId = path.basename(keyP8Path).match(regex)?.groups?.keyId;
const ascApiKeyMetadata = await getCredentialsFromUserAsync(ascApiKeyMetadataSchema, {
keyId: bestEffortKeyId,
});
return { ...ascApiKeyMetadata, keyP8Path };
}
export async function promptForIssuerIdAsync(): Promise<string> {
Log.log(chalk.bold('An App Store Connect Issuer ID is required'));
Log.log(
`If you're not sure what this is or how to find yours, ${learnMore(
'https://expo.fyi/asc-issuer-id'
)}`
);
const { issuerId } = await promptAsync({
type: 'text',
name: 'issuerId',
message: 'App Store Connect Issuer ID:',
validate: (input: string) => uuid.validate(input),
});
return issuerId;
}
export async function getMinimalAscApiKeyAsync(ascApiKey: AscApiKey): Promise<MinimalAscApiKey> {
return {
...ascApiKey,
issuerId: ascApiKey.issuerId ?? (await promptForIssuerIdAsync()),
};
}
@@ -10,7 +10,7 @@ export class CreateDistributionCertificate {
public async runAsync(
ctx: CredentialsContext
): Promise<AppleDistributionCertificateMutationResult> {
const distCert = await provideOrGenerateDistributionCertificateAsync(ctx, this.account.name);
const distCert = await provideOrGenerateDistributionCertificateAsync(ctx);
const result = await ctx.ios.createDistributionCertificateAsync(this.account, distCert);
Log.succeed('Created distribution certificate');
return result;
@@ -12,7 +12,7 @@ export class CreatePushKey {
throw new Error(`A new push key cannot be created in non-interactive mode.`);
}
const pushKey = await provideOrGeneratePushKeyAsync(ctx, this.account.name);
const pushKey = await provideOrGeneratePushKeyAsync(ctx);
const result = await ctx.ios.createPushKeyAsync(this.account, pushKey);
Log.succeed('Created push key');
return result;
@@ -152,8 +152,7 @@ Please remember that Apple Distribution Certificates are not application specifi
`;
export async function provideOrGenerateDistributionCertificateAsync(
ctx: CredentialsContext,
accountName: string
ctx: CredentialsContext
): Promise<DistributionCertificate> {
if (!ctx.nonInteractive) {
const userProvided = await promptForDistCertAsync(ctx);
@@ -168,13 +167,11 @@ export async function provideOrGenerateDistributionCertificateAsync(
if (!isValid) {
Log.warn("Provided Distribution Certificate is no longer valid on Apple's server");
}
return isValid
? userProvided
: await provideOrGenerateDistributionCertificateAsync(ctx, accountName);
return isValid ? userProvided : await provideOrGenerateDistributionCertificateAsync(ctx);
}
}
}
return await generateDistributionCertificateAsync(ctx, accountName);
return await generateDistributionCertificateAsync(ctx);
}
async function promptForDistCertAsync(
@@ -200,8 +197,7 @@ async function promptForDistCertAsync(
}
async function generateDistributionCertificateAsync(
ctx: CredentialsContext,
accountName: string
ctx: CredentialsContext
): Promise<DistributionCertificate> {
await ctx.appStore.ensureAuthenticatedAsync();
try {
@@ -246,7 +242,7 @@ async function generateDistributionCertificateAsync(
throw e;
}
}
return await generateDistributionCertificateAsync(ctx, accountName);
return await generateDistributionCertificateAsync(ctx);
}
function formatDistributionCertificateFromApple(
@@ -14,10 +14,7 @@ import { pushKeySchema } from '../credentials';
import { isPushKeyValidAndTrackedAsync } from '../validators/validatePushKey';
import { formatAppleTeam } from './AppleTeamUtils';
export async function provideOrGeneratePushKeyAsync(
ctx: CredentialsContext,
accountName: string
): Promise<PushKey> {
export async function provideOrGeneratePushKeyAsync(ctx: CredentialsContext): Promise<PushKey> {
if (!ctx.nonInteractive) {
const userProvided = await promptForPushKeyAsync(ctx);
if (userProvided) {
@@ -36,11 +33,11 @@ export async function provideOrGeneratePushKeyAsync(
if (useUserProvided) {
return userProvided;
}
return await provideOrGeneratePushKeyAsync(ctx, accountName);
return await provideOrGeneratePushKeyAsync(ctx);
}
}
}
return await generatePushKeyAsync(ctx, accountName);
return await generatePushKeyAsync(ctx);
}
async function promptForPushKeyAsync(ctx: CredentialsContext): Promise<PushKey | null> {
@@ -63,10 +60,7 @@ async function promptForPushKeyAsync(ctx: CredentialsContext): Promise<PushKey |
return userProvided;
}
async function generatePushKeyAsync(
ctx: CredentialsContext,
accountName: string
): Promise<PushKey> {
async function generatePushKeyAsync(ctx: CredentialsContext): Promise<PushKey> {
await ctx.appStore.ensureAuthenticatedAsync();
try {
return await ctx.appStore.createPushKeyAsync();
@@ -106,7 +100,7 @@ async function generatePushKeyAsync(
throw e;
}
}
return await generatePushKeyAsync(ctx, accountName);
return await generatePushKeyAsync(ctx);
}
function formatPushKeyFromApple(pushKey: PushKeyStoreInfo): string {
@@ -39,7 +39,7 @@ export class SetupTargetBuildCredentials {
try {
return await this.setupBuildCredentialsAsync(ctx);
} catch (error) {
Log.error('Failed to setup credentials.');
Log.error('Failed to set up credentials.');
throw error;
}
}
@@ -22,7 +22,10 @@ export const AppQuery = {
}
${print(AppFragmentNode)}
`,
{ fullName }
{ fullName },
{
additionalTypenames: ['App'],
}
)
.toPromise()
);
@@ -111,7 +111,10 @@ export const AppleDeviceQuery = {
}
}
`,
{ accountName, appleTeamIdentifier }
{ accountName, appleTeamIdentifier },
{
additionalTypenames: ['AppleDevice', 'AppleTeam'],
}
)
.toPromise()
);
@@ -147,7 +150,10 @@ export const AppleDeviceQuery = {
}
}
`,
{ accountName, identifier }
{ accountName, identifier },
{
additionalTypenames: ['AppleDevice', 'AppleTeam'],
}
)
.toPromise()
);
@@ -60,7 +60,11 @@ export const AppleDistributionCertificateQuery = {
iosDistributionType,
},
{
additionalTypenames: ['IosAppCredentials', 'IosAppBuildCredentials'],
additionalTypenames: [
'AppleDistributionCertificate',
'IosAppCredentials',
'IosAppBuildCredentials',
],
}
)
.toPromise()
@@ -93,6 +97,9 @@ export const AppleDistributionCertificateQuery = {
`,
{
accountName,
},
{
additionalTypenames: ['AppleDistributionCertificate'],
}
)
.toPromise()
@@ -77,7 +77,11 @@ export const AppleProvisioningProfileQuery = {
iosDistributionType,
},
{
additionalTypenames: ['IosAppCredentials', 'IosAppBuildCredentials'],
additionalTypenames: [
'AppleProvisioningProfile',
'IosAppCredentials',
'IosAppBuildCredentials',
],
}
)
.toPromise()
@@ -26,6 +26,9 @@ export const ApplePushKeyQuery = {
`,
{
accountName,
},
{
additionalTypenames: ['ApplePushKey'],
}
)
.toPromise()
@@ -28,7 +28,10 @@ export const AppleTeamQuery = {
}
}
`,
{ accountName }
{ accountName },
{
additionalTypenames: ['AppleTeam'],
}
)
.toPromise()
);
@@ -57,6 +60,9 @@ export const AppleTeamQuery = {
{
accountId,
appleTeamIdentifier,
},
{
additionalTypenames: ['AppleTeam'],
}
)
.toPromise()
@@ -30,6 +30,21 @@ export const IosAppCredentialsQuery = {
},
__typename: 'ApplePushKey',
},
appStoreConnectApiKeyForSubmissions: {
id: 'b8fe2225-3024-4ebe-9e36-7b9022d3b13e',
appleTeam: {
id: 'ca27f9f6-c7cb-40d1-8c41-c168e8548261',
appleTeamIdentifier: '77KQ969CHE',
appleTeamName: null,
__typename: 'AppleTeam',
},
issuerIdentifier: '69a6de93-e4e4-47e3-e053-5b8c7c11a4d1',
keyIdentifier: 'NYN468XNN3',
name: 'Expo Submissions pl94k8_lnS',
roles: ['ADMIN'],
updatedAt: '2020-10-13T18:39:04.463Z',
__typename: 'AppStoreConnectApiKey',
},
iosAppBuildCredentialsList: [
{
id: '6f8d0175-0caf-4922-b09b-eae757b83144',
@@ -1,4 +1,6 @@
import {
AscApiKey,
AscApiKeyInfo,
DistributionCertificate,
DistributionCertificateStoreInfo,
ProvisioningProfile,
@@ -6,6 +8,12 @@ import {
PushKey,
PushKeyStoreInfo,
} from './Credentials.types';
import {
createAscApiKeyAsync,
getAscApiKeyAsync,
listAscApiKeysAsync,
revokeAscApiKeyAsync,
} from './ascApiKey';
import { AuthCtx, Options as AuthenticateOptions, authenticateAsync } from './authenticate';
import {
createDistributionCertificateAsync,
@@ -136,4 +144,24 @@ export default class AppStoreApi {
distCertSerialNumber
);
}
public async listAscApiKeysAsync(): Promise<AscApiKeyInfo[]> {
const ctx = await this.ensureAuthenticatedAsync();
return await listAscApiKeysAsync(ctx);
}
public async getAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
const ctx = await this.ensureAuthenticatedAsync();
return await getAscApiKeyAsync(ctx, keyId);
}
public async createAscApiKeyAsync({ nickname }: { nickname: string }): Promise<AscApiKey> {
const ctx = await this.ensureAuthenticatedAsync();
return await createAscApiKeyAsync(ctx, { nickname });
}
public async revokeAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
const ctx = await this.ensureAuthenticatedAsync();
return await revokeAscApiKeyAsync(ctx, keyId);
}
}
@@ -1,3 +1,5 @@
import { UserRole } from '@expo/apple-utils';
export interface Device {
id: string;
teamId: string;
@@ -66,3 +68,16 @@ export interface PushKey {
teamId: string;
teamName?: string;
}
export type AscApiKeyInfo = {
keyId: string;
issuerId?: string;
teamId: string;
name: string;
teamName?: string;
roles: UserRole[];
};
export type AscApiKey = AscApiKeyInfo & {
keyP8: string;
};
@@ -0,0 +1,98 @@
import { ApiKey, ApiKeyType, UserRole } from '@expo/apple-utils';
import { asMock } from '../../../../__tests__/utils';
import {
createAscApiKeyAsync,
getAscApiKeyAsync,
listAscApiKeysAsync,
revokeAscApiKeyAsync,
} from '../ascApiKey';
import { getRequestContext } from '../authenticate';
jest.mock('@expo/apple-utils');
jest.mock('../authenticate');
jest.mock('../../../../ora');
const mockTeam = {
id: 'test-id',
name: 'test-name',
};
const mockAuthCtx = {
appleId: 'test-appleId',
team: mockTeam,
};
const mockApiKey = {
type: 'apiKeys',
id: 'NL67AN9Q6Q',
attributes: {
nickname: 'ejb-apple-utils-admin',
lastUsed: '2021-06-28T12:30:41-07:00',
revokingDate: null,
isActive: true,
canDownload: false,
privateKey: null,
roles: ['ADMIN'],
allAppsVisible: true,
keyType: 'PUBLIC_API',
},
downloadAsync: jest.fn(() => 'super secret'),
revokeAsync: jest.fn(() => mockApiKey),
} as unknown as ApiKey;
const mockAscApiKeyInfo = {
issuerId: undefined,
keyId: 'NL67AN9Q6Q',
name: 'ejb-apple-utils-admin',
roles: ['ADMIN'],
teamId: 'test-id',
teamName: 'test-name',
};
beforeEach(() => {
jest.clearAllMocks();
});
test(`listAscApiKeysAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'getAsync').mockImplementation(async () => []);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await listAscApiKeysAsync(mockAuthCtx);
expect(ApiKey.getAsync).toHaveBeenLastCalledWith(mockRequestContext);
expect(result).toEqual([]);
});
test(`getAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'infoAsync').mockImplementation(async () => mockApiKey);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await getAscApiKeyAsync(mockAuthCtx, 'test-key-id');
expect(ApiKey.infoAsync).toHaveBeenLastCalledWith(mockRequestContext, { id: 'test-key-id' });
expect(result).toEqual(mockAscApiKeyInfo);
});
test(`createAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'createAsync').mockImplementation(async () => mockApiKey);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await createAscApiKeyAsync(mockAuthCtx, { nickname: 'test-name' });
expect(ApiKey.createAsync).toHaveBeenLastCalledWith(mockRequestContext, {
nickname: 'test-name',
allAppsVisible: true,
roles: [UserRole.ADMIN],
keyType: ApiKeyType.PUBLIC_API,
});
expect(result).toEqual({ ...mockAscApiKeyInfo, keyP8: 'super secret' });
});
test(`revokeAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'infoAsync').mockImplementation(async () => mockApiKey);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await revokeAscApiKeyAsync(mockAuthCtx, 'test-key-id');
expect(ApiKey.infoAsync).toHaveBeenLastCalledWith(mockRequestContext, {
id: 'test-key-id',
});
expect(mockApiKey.revokeAsync).toBeCalled();
expect(result).toEqual(mockAscApiKeyInfo);
});
@@ -0,0 +1,104 @@
import { ApiKey, ApiKeyProps, ApiKeyType, UserRole } from '@expo/apple-utils';
import Log from '../../../log';
import { ora } from '../../../ora';
import { AscApiKey, AscApiKeyInfo } from './Credentials.types';
import { AuthCtx, getRequestContext } from './authenticate';
export async function listAscApiKeysAsync(authCtx: AuthCtx): Promise<AscApiKeyInfo[]> {
const spinner = ora(`Fetching App Store Connect API Keys.`).start();
try {
const context = getRequestContext(authCtx);
const keys = await ApiKey.getAsync(context);
spinner.succeed(`Fetched App Store Connect API Keys.`);
return keys.map(key => getAscApiKeyInfo(key, authCtx));
} catch (error) {
spinner.fail(`Failed to fetch App Store Connect API Keys.`);
throw error;
}
}
export async function getAscApiKeyAsync(authCtx: AuthCtx, keyId: string): Promise<AscApiKeyInfo> {
const spinner = ora(`Fetching App Store Connect API Key.`).start();
try {
const context = getRequestContext(authCtx);
const apiKey = await ApiKey.infoAsync(context, { id: keyId });
spinner.succeed(`Fetched App Store Connect API Key (ID: ${keyId}).`);
return getAscApiKeyInfo(apiKey, authCtx);
} catch (error) {
Log.error(error);
spinner.fail(`Failed to fetch App Store Connect API Key.`);
throw error;
}
}
export async function createAscApiKeyAsync(
authCtx: AuthCtx,
{
nickname,
allAppsVisible,
roles,
keyType,
}: Partial<Pick<ApiKeyProps, 'nickname' | 'roles' | 'allAppsVisible' | 'keyType'>>
): Promise<AscApiKey> {
const spinner = ora(`Creating App Store Connect API Key.`).start();
try {
const context = getRequestContext(authCtx);
const key = await ApiKey.createAsync(context, {
nickname: nickname ?? `[expo] ${new Date().getTime()}`,
allAppsVisible: allAppsVisible ?? true,
roles: roles ?? [UserRole.ADMIN],
keyType: keyType ?? ApiKeyType.PUBLIC_API,
});
const keyP8 = await key.downloadAsync();
if (!keyP8) {
const { nickname, roles } = key.attributes;
const humanReadableKey = `App Store Connect Key '${nickname}' (${
key.id
}) with roles {${roles.join(',')}}`;
if (!key.attributes.canDownload) {
// this case would be unexpected because we just created the key
throw new Error(`${humanReadableKey} is not available for download from Apple.`);
} else if (!key.attributes.isActive) {
throw new Error(`${humanReadableKey} is inactive and could not be downloaded.`);
}
throw new Error(`Failed to download .p8 file of ${humanReadableKey}.`);
}
return {
...getAscApiKeyInfo(key, authCtx),
keyP8,
};
} catch (err: any) {
spinner.fail('Failed to create App Store Connect API Key.');
throw err;
}
}
export async function revokeAscApiKeyAsync(
authCtx: AuthCtx,
keyId: string
): Promise<AscApiKeyInfo> {
const spinner = ora(`Revoking App Store Connect API Key.`).start();
try {
const context = getRequestContext(authCtx);
const apiKey = await ApiKey.infoAsync(context, { id: keyId });
const revokedKey = await apiKey.revokeAsync();
spinner.succeed(`Revoked App Store Connect API Key.`);
return getAscApiKeyInfo(revokedKey, authCtx);
} catch (error) {
Log.error(error);
spinner.fail(`Failed to revoke App Store Connect API Key.`);
throw error;
}
}
export function getAscApiKeyInfo(apiKey: ApiKey, authCtx: AuthCtx): AscApiKeyInfo {
return {
name: apiKey.attributes.nickname,
keyId: apiKey.id,
issuerId: apiKey.attributes.provider?.id,
teamId: authCtx.team.id,
teamName: authCtx.team.name,
roles: apiKey.attributes.roles,
};
}
@@ -1,3 +1,5 @@
import { UserRole } from '@expo/apple-utils';
import { AppleDevice } from '../../graphql/generated';
import Log from '../../log';
import { CredentialSchema } from '../utils/promptForCredentials';
@@ -100,6 +102,38 @@ export const distributionCertificateSchema: CredentialSchema<DistributionCertifi
},
};
export type MinimalAscApiKey = {
keyP8: string;
keyId: string;
issuerId: string;
teamId?: string;
teamName?: string;
roles?: UserRole[];
name?: string;
};
export interface AscApiKeyPath {
keyP8Path: string;
keyId: string;
issuerId: string;
}
export const ascApiKeyMetadataSchema: CredentialSchema<Omit<MinimalAscApiKey, 'keyP8'>> = {
name: 'App Store Connect API Key',
questions: [
{
field: 'keyId',
type: 'string',
question: 'Key ID:',
},
{
field: 'issuerId',
type: 'string',
question: 'Issuer ID:',
},
],
};
export const pushKeySchema: CredentialSchema<PushKey> = {
name: 'Apple Push Notifications service key',
questions: [
@@ -2,47 +2,55 @@
exports[`print credentials prints the IosAppCredentials map 1`] = `
Array [
"iOS Credentials
Project @quinlanj/test52
Bundle Identifier com.quinlanj.test52
Apple Team test-apple-identifier
Push Key
Developer Portal ID 9839M6AY8W
Apple Team 77KQ969CHE
Updated 6 months ago
App Store Configuration
Distribution Certificate
Serial Number test-serial
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 6 months ago
Provisioning Profile
Developer Portal ID test-profile-id
Status active
Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 5 months ago
Ad Hoc Configuration
Distribution Certificate
Serial Number test-serial
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 6 months ago
Provisioning Profile
Developer Portal ID test-profile-id
Status active
Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 5 months ago
",
"iOS Credentials
Project @quinlanj/test52
Bundle Identifier com.quinlanj.test52
Apple Team test-apple-identifier
Push Key
Developer Portal ID 9839M6AY8W
Apple Team 77KQ969CHE
Updated 6 months ago
App Store Connect Api Key
Developer Portal ID NYN468XNN3
Name Expo Submissions pl94k8_lnS
Issuer ID 69a6de93-e4e4-47e3-e053-5b8c7c11a4d1
Roles ADMIN
Apple Team 77KQ969CHE
Updated 6 months ago
App Store Configuration
Distribution Certificate
Serial Number test-serial
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 6 months ago
Provisioning Profile
Developer Portal ID test-profile-id
Status active
Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 5 months ago
Ad Hoc Configuration
Distribution Certificate
Serial Number test-serial
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 6 months ago
Provisioning Profile
Developer Portal ID test-profile-id
Status active
Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 5 months ago
",
]
`;
@@ -2,6 +2,7 @@ import chalk from 'chalk';
import dateformat from 'dateformat';
import {
AppStoreConnectApiKeyFragment,
AppleDeviceFragment,
ApplePushKeyFragment,
IosAppBuildCredentialsFragment,
@@ -91,7 +92,7 @@ export function displayIosCredentials(
continue;
}
const { appleTeam, pushKey } = targetAppCredentials;
const { appleTeam, pushKey, appStoreConnectApiKeyForSubmissions } = targetAppCredentials;
if (appleTeam) {
const { appleTeamIdentifier, appleTeamName } = appleTeam;
fields.push({
@@ -105,6 +106,10 @@ export function displayIosCredentials(
displayApplePushKey(pushKey, fields);
}
if (appStoreConnectApiKeyForSubmissions) {
displayAscApiKey(appStoreConnectApiKeyForSubmissions, fields);
}
const sortedIosAppBuildCredentialsList = sortBuildCredentialsByDistributionType(
targetAppCredentials.iosAppBuildCredentialsList
);
@@ -230,6 +235,35 @@ function displayApplePushKey(
fields.push({ label: '', value: '' });
}
function displayAscApiKey(
maybeAscApiKey: AppStoreConnectApiKeyFragment | null,
fields: { label: string; value: string }[]
): void {
fields.push({ label: 'App Store Connect Api Key', value: '' });
if (maybeAscApiKey) {
const { keyIdentifier, issuerIdentifier, appleTeam, name, roles, updatedAt } = maybeAscApiKey;
fields.push({ label: 'Developer Portal ID', value: keyIdentifier });
if (name) {
fields.push({ label: 'Name', value: name });
}
fields.push({ label: 'Issuer ID', value: issuerIdentifier });
if (roles) {
fields.push({ label: 'Roles', value: roles.join(',') });
}
if (appleTeam) {
const { appleTeamIdentifier, appleTeamName } = appleTeam;
fields.push({
label: 'Apple Team',
value: `${appleTeamIdentifier} ${appleTeamName ? `(${appleTeamName})` : ''}`,
});
}
fields.push({ label: 'Updated', value: `${fromNow(new Date(updatedAt))} ago` });
} else {
fields.push({ label: '', value: 'None assigned yet' });
}
fields.push({ label: '', value: '' });
}
function formatAppleDevice(device: AppleDeviceFragment): string {
let deviceString = '';
if (device.name) {
@@ -0,0 +1,50 @@
export type ActionInfo = {
value: AndroidActionType | IosActionType;
title: string;
scope: Scope;
};
export enum Scope {
Project,
Manager,
Account,
}
export enum AndroidActionType {
ManageBuildCredentials,
ManageFcm,
ManageGoogleServiceAccountKey,
ManageCredentialsJson,
GoBackToCaller,
GoBackToHighLevelActions,
CreateKeystore,
DownloadKeystore,
RemoveKeystore,
CreateFcm,
RemoveFcm,
CreateGsaKey,
UseExistingGsaKey,
RemoveGsaKey,
SetupGsaKey,
UpdateCredentialsJson,
SetupBuildCredentialsFromCredentialsJson,
}
export enum IosActionType {
ManageCredentialsJson,
ManageBuildCredentials,
ManagePushKey,
GoBackToCaller,
GoBackToHighLevelActions,
SetupBuildCredentials,
SetupBuildCredentialsFromCredentialsJson,
UpdateCredentialsJson,
UseExistingDistributionCertificate,
RemoveProvisioningProfile,
CreateDistributionCertificate,
RemoveDistributionCertificate,
SetupPushKey,
CreatePushKey,
UseExistingPushKey,
RemovePushKey,
}
@@ -0,0 +1,116 @@
import { ActionInfo, AndroidActionType, Scope } from './Actions';
export const highLevelActions: ActionInfo[] = [
{
value: AndroidActionType.ManageBuildCredentials,
title: 'Keystore: Manage everything needed to build your project',
scope: Scope.Manager,
},
{
value: AndroidActionType.ManageFcm,
title: 'Push Notifications: Manage your FCM Api Key',
scope: Scope.Manager,
},
{
value: AndroidActionType.ManageGoogleServiceAccountKey,
title: 'Google Service Account: Manage your Service Account Key',
scope: Scope.Manager,
},
{
value: AndroidActionType.ManageCredentialsJson,
title: 'credentials.json: Upload/Download credentials between EAS servers and your local json ',
scope: Scope.Manager,
},
{
value: AndroidActionType.GoBackToCaller,
title: 'Go back',
scope: Scope.Manager,
},
];
export const credentialsJsonActions: ActionInfo[] = [
{
value: AndroidActionType.UpdateCredentialsJson,
title: 'Download credentials from EAS to credentials.json',
scope: Scope.Project,
},
{
value: AndroidActionType.SetupBuildCredentialsFromCredentialsJson,
title: 'Upload credentials from credentials.json to EAS',
scope: Scope.Project,
},
{
value: AndroidActionType.GoBackToHighLevelActions,
title: 'Go back',
scope: Scope.Manager,
},
];
export const buildCredentialsActions: ActionInfo[] = [
{
value: AndroidActionType.CreateKeystore,
title: 'Set up a new keystore',
scope: Scope.Project,
},
{
value: AndroidActionType.DownloadKeystore,
title: 'Download existing keystore',
scope: Scope.Project,
},
{
value: AndroidActionType.RemoveKeystore,
title: 'Delete your keystore',
scope: Scope.Project,
},
{
value: AndroidActionType.GoBackToHighLevelActions,
title: 'Go back',
scope: Scope.Manager,
},
];
export const fcmActions: ActionInfo[] = [
{
value: AndroidActionType.CreateFcm,
title: 'Upload an FCM Api Key',
scope: Scope.Project,
},
{
value: AndroidActionType.RemoveFcm,
title: 'Delete your FCM Api Key',
scope: Scope.Project,
},
{
value: AndroidActionType.GoBackToHighLevelActions,
title: 'Go back',
scope: Scope.Manager,
},
];
export const gsaKeyActions: ActionInfo[] = [
{
value: AndroidActionType.SetupGsaKey,
title: 'Setup a Google Service Account Key',
scope: Scope.Project,
},
{
value: AndroidActionType.CreateGsaKey,
title: 'Upload a Google Service Account Key',
scope: Scope.Project,
},
{
value: AndroidActionType.UseExistingGsaKey,
title: 'Use an existing Google Service Account Key',
scope: Scope.Project,
},
{
value: AndroidActionType.RemoveGsaKey,
title: 'Delete a Google Service Account Key',
scope: Scope.Project,
},
{
value: AndroidActionType.GoBackToHighLevelActions,
title: 'Go back',
scope: Scope.Manager,
},
];
@@ -0,0 +1,110 @@
import { CredentialsContext } from '../context';
import { ActionInfo, IosActionType, Scope } from './Actions';
export const highLevelActions: ActionInfo[] = [
{
value: IosActionType.ManageBuildCredentials,
title: 'Build Credentials: Manage everything needed to build your project',
scope: Scope.Manager,
},
{
value: IosActionType.ManagePushKey,
title: 'Push Notifications: Manage your Apple Push Notifications Key',
scope: Scope.Manager,
},
{
value: IosActionType.ManageCredentialsJson,
title: 'credentials.json: Upload/Download credentials between EAS servers and your local json ',
scope: Scope.Manager,
},
{
value: IosActionType.GoBackToCaller,
title: 'Go back',
scope: Scope.Manager,
},
];
export const credentialsJsonActions: ActionInfo[] = [
{
value: IosActionType.UpdateCredentialsJson,
title: 'Download credentials from EAS to credentials.json',
scope: Scope.Project,
},
{
value: IosActionType.SetupBuildCredentialsFromCredentialsJson,
title: 'Upload credentials from credentials.json to EAS',
scope: Scope.Project,
},
{
value: IosActionType.GoBackToHighLevelActions,
title: 'Go back',
scope: Scope.Manager,
},
];
export function getPushKeyActions(ctx: CredentialsContext): ActionInfo[] {
return [
{
value: IosActionType.SetupPushKey,
title: 'Setup your project to use Push Notifications',
scope: Scope.Project,
},
{
value: IosActionType.CreatePushKey,
title: 'Add a new push key',
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account,
},
{
value: IosActionType.UseExistingPushKey,
title: 'Use an existing push key',
scope: Scope.Project,
},
{
value: IosActionType.RemovePushKey,
title: 'Remove a push key from your account',
scope: Scope.Account,
},
{
value: IosActionType.GoBackToHighLevelActions,
title: 'Go back',
scope: Scope.Manager,
},
];
}
export function getBuildCredentialsActions(ctx: CredentialsContext): ActionInfo[] {
return [
{
// This command will be triggered during build to ensure all credentials are ready
// I'm leaving it here for now to simplify testing
value: IosActionType.SetupBuildCredentials,
title: 'All: Set up all the required credentials to build your project',
scope: Scope.Project,
},
{
value: IosActionType.UseExistingDistributionCertificate,
title: 'Distribution Certificate: Use an existing one for your project',
scope: Scope.Project,
},
{
value: IosActionType.CreateDistributionCertificate,
title: `Distribution Certificate: Add a new one to your account`,
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account,
},
{
value: IosActionType.RemoveDistributionCertificate,
title: 'Distribution Certificate: Delete one from your account',
scope: Scope.Account,
},
{
value: IosActionType.RemoveProvisioningProfile,
title: 'Provisioning Profile: Delete one from your project',
scope: Scope.Project,
},
{
value: IosActionType.GoBackToHighLevelActions,
title: 'Go back',
scope: Scope.Manager,
},
];
}
@@ -34,6 +34,14 @@ import {
displayEmptyAndroidCredentials,
} from '../android/utils/printCredentials';
import { CredentialsContext } from '../context';
import { ActionInfo, AndroidActionType, Scope } from './Actions';
import {
buildCredentialsActions,
credentialsJsonActions,
fcmActions,
gsaKeyActions,
highLevelActions,
} from './AndroidActions';
import { Action, PressAnyKeyToContinue } from './HelperActions';
import {
SelectAndroidBuildCredentials,
@@ -42,148 +50,6 @@ import {
} from './SelectAndroidBuildCredentials';
import { SelectBuildProfileFromEasJson } from './SelectBuildProfileFromEasJson';
enum ActionType {
ManageBuildCredentials,
ManageFcm,
ManageGoogleServiceAccountKey,
ManageCredentialsJson,
GoBackToCaller,
GoBackToHighLevelActions,
CreateKeystore,
DownloadKeystore,
RemoveKeystore,
CreateFcm,
RemoveFcm,
CreateGsaKey,
UseExistingGsaKey,
RemoveGsaKey,
SetupGsaKey,
UpdateCredentialsJson,
SetupBuildCredentialsFromCredentialsJson,
}
enum Scope {
Project,
Manager,
}
type ActionInfo = { value: ActionType; title: string; scope: Scope };
const highLevelActions: ActionInfo[] = [
{
value: ActionType.ManageBuildCredentials,
title: 'Keystore: Manage everything needed to build your project',
scope: Scope.Manager,
},
{
value: ActionType.ManageFcm,
title: 'Push Notifications: Manage your FCM Api Key',
scope: Scope.Manager,
},
{
value: ActionType.ManageGoogleServiceAccountKey,
title: 'Google Service Account: Manage your Service Account Key',
scope: Scope.Manager,
},
{
value: ActionType.ManageCredentialsJson,
title: 'Credentials.json: Upload/Download credentials between EAS servers and your local json ',
scope: Scope.Manager,
},
{
value: ActionType.GoBackToCaller,
title: 'Go back',
scope: Scope.Manager,
},
];
const credentialsJsonActions: ActionInfo[] = [
{
value: ActionType.UpdateCredentialsJson,
title: 'Download credentials from EAS to credentials.json',
scope: Scope.Project,
},
{
value: ActionType.SetupBuildCredentialsFromCredentialsJson,
title: 'Upload credentials from credentials.json to EAS',
scope: Scope.Project,
},
{
value: ActionType.GoBackToHighLevelActions,
title: 'Go back',
scope: Scope.Manager,
},
];
const buildCredentialsActions: ActionInfo[] = [
{
value: ActionType.CreateKeystore,
title: 'Set up a new keystore',
scope: Scope.Project,
},
{
value: ActionType.DownloadKeystore,
title: 'Download existing keystore',
scope: Scope.Project,
},
{
value: ActionType.RemoveKeystore,
title: 'Delete your keystore',
scope: Scope.Project,
},
{
value: ActionType.GoBackToHighLevelActions,
title: 'Go back',
scope: Scope.Manager,
},
];
const fcmActions: ActionInfo[] = [
{
value: ActionType.CreateFcm,
title: 'Upload an FCM Api Key',
scope: Scope.Project,
},
{
value: ActionType.RemoveFcm,
title: 'Delete your FCM Api Key',
scope: Scope.Project,
},
{
value: ActionType.GoBackToHighLevelActions,
title: 'Go back',
scope: Scope.Manager,
},
];
const gsaKeyActions: ActionInfo[] = [
{
value: ActionType.SetupGsaKey,
title: 'Setup a Google Service Account Key',
scope: Scope.Project,
},
{
value: ActionType.CreateGsaKey,
title: 'Upload a Google Service Account Key',
scope: Scope.Project,
},
{
value: ActionType.UseExistingGsaKey,
title: 'Use an existing Google Service Account Key',
scope: Scope.Project,
},
{
value: ActionType.RemoveGsaKey,
title: 'Delete a Google Service Account Key',
scope: Scope.Project,
},
{
value: ActionType.GoBackToHighLevelActions,
title: 'Go back',
scope: Scope.Manager,
},
];
export class ManageAndroid {
constructor(private callingAction: Action) {}
@@ -242,22 +108,22 @@ export class ManageAndroid {
throw new Error('Action not supported yet');
}
if (actionInfo.scope === Scope.Manager) {
if (chosenAction === ActionType.ManageBuildCredentials) {
if (chosenAction === AndroidActionType.ManageBuildCredentials) {
currentActions = buildCredentialsActions;
continue;
} else if (chosenAction === ActionType.ManageFcm) {
} else if (chosenAction === AndroidActionType.ManageFcm) {
currentActions = fcmActions;
continue;
} else if (chosenAction === ActionType.ManageGoogleServiceAccountKey) {
} else if (chosenAction === AndroidActionType.ManageGoogleServiceAccountKey) {
currentActions = gsaKeyActions;
continue;
} else if (chosenAction === ActionType.ManageCredentialsJson) {
} else if (chosenAction === AndroidActionType.ManageCredentialsJson) {
currentActions = credentialsJsonActions;
continue;
} else if (chosenAction === ActionType.GoBackToHighLevelActions) {
} else if (chosenAction === AndroidActionType.GoBackToHighLevelActions) {
currentActions = highLevelActions;
continue;
} else if (chosenAction === ActionType.GoBackToCaller) {
} else if (chosenAction === AndroidActionType.GoBackToCaller) {
return await this.callingAction.runAsync(ctx);
}
}
@@ -302,7 +168,7 @@ export class ManageAndroid {
private async runProjectSpecificActionAsync(
ctx: CredentialsContext,
action: ActionType,
action: AndroidActionType,
gradleContext?: GradleBuildContext
): Promise<void> {
assert(
@@ -310,7 +176,7 @@ export class ManageAndroid {
'You must be in your project directory in order to perform this action'
);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx, gradleContext);
if (action === ActionType.CreateKeystore) {
if (action === AndroidActionType.CreateKeystore) {
const selectBuildCredentialsResult = await new SelectAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
@@ -331,47 +197,47 @@ export class ManageAndroid {
}
);
}
} else if (action === ActionType.DownloadKeystore) {
} else if (action === AndroidActionType.DownloadKeystore) {
const buildCredentials = await new SelectExistingAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
if (buildCredentials) {
await new DownloadKeystore({ app: appLookupParams }).runAsync(ctx, buildCredentials);
}
} else if (action === ActionType.RemoveKeystore) {
} else if (action === AndroidActionType.RemoveKeystore) {
const buildCredentials = await new SelectExistingAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
if (buildCredentials) {
await new RemoveKeystore(appLookupParams).runAsync(ctx, buildCredentials);
}
} else if (action === ActionType.CreateFcm) {
} else if (action === AndroidActionType.CreateFcm) {
const fcm = await new CreateFcm(appLookupParams.account).runAsync(ctx);
await new AssignFcm(appLookupParams).runAsync(ctx, fcm);
} else if (action === ActionType.RemoveFcm) {
} else if (action === AndroidActionType.RemoveFcm) {
await new RemoveFcm(appLookupParams).runAsync(ctx);
} else if (action === ActionType.CreateGsaKey) {
} else if (action === AndroidActionType.CreateGsaKey) {
const gsaKey = await new CreateGoogleServiceAccountKey(appLookupParams.account).runAsync(ctx);
await new AssignGoogleServiceAccountKey(appLookupParams).runAsync(ctx, gsaKey);
} else if (action === ActionType.UseExistingGsaKey) {
} else if (action === AndroidActionType.UseExistingGsaKey) {
const gsaKey = await new UseExistingGoogleServiceAccountKey(appLookupParams.account).runAsync(
ctx
);
if (gsaKey) {
await new AssignGoogleServiceAccountKey(appLookupParams).runAsync(ctx, gsaKey);
}
} else if (action === ActionType.RemoveGsaKey) {
} else if (action === AndroidActionType.RemoveGsaKey) {
await new SelectAndRemoveGoogleServiceAccountKey(appLookupParams.account).runAsync(ctx);
} else if (action === ActionType.SetupGsaKey) {
} else if (action === AndroidActionType.SetupGsaKey) {
await new SetupGoogleServiceAccountKey(appLookupParams).runAsync(ctx);
} else if (action === ActionType.UpdateCredentialsJson) {
} else if (action === AndroidActionType.UpdateCredentialsJson) {
const buildCredentials = await new SelectExistingAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
if (buildCredentials) {
await new UpdateCredentialsJson().runAsync(ctx, buildCredentials);
}
} else if (action === ActionType.SetupBuildCredentialsFromCredentialsJson) {
} else if (action === AndroidActionType.SetupBuildCredentialsFromCredentialsJson) {
await new SetupBuildCredentialsFromCredentialsJson(appLookupParams).runAsync(ctx);
}
}
@@ -38,145 +38,17 @@ import { AppLookupParams } from '../ios/api/GraphqlClient';
import { getManagedEntitlementsJsonAsync } from '../ios/appstore/entitlements';
import { App, IosAppCredentialsMap, Target } from '../ios/types';
import { displayIosCredentials } from '../ios/utils/printCredentials';
import { ActionInfo, IosActionType, Scope } from './Actions';
import { Action, PressAnyKeyToContinue } from './HelperActions';
import {
credentialsJsonActions,
getBuildCredentialsActions,
getPushKeyActions,
highLevelActions,
} from './IosActions';
import { SelectBuildProfileFromEasJson } from './SelectBuildProfileFromEasJson';
import { SelectIosDistributionTypeGraphqlFromBuildProfile } from './SelectIosDistributionTypeGraphqlFromBuildProfile';
enum ActionType {
ManageCredentialsJson,
ManageBuildCredentials,
ManagePushKey,
GoBackToCaller,
GoBackToHighLevelActions,
SetupBuildCredentials,
SetupBuildCredentialsFromCredentialsJson,
UpdateCredentialsJson,
UseExistingDistributionCertificate,
RemoveProvisioningProfile,
CreateDistributionCertificate,
RemoveDistributionCertificate,
SetupPushKey,
CreatePushKey,
UseExistingPushKey,
RemovePushKey,
}
enum Scope {
Project,
Account,
Manager,
}
type ActionInfo = { value: ActionType; title: string; scope: Scope };
const highLevelActions: ActionInfo[] = [
{
value: ActionType.ManageBuildCredentials,
title: 'Build Credentials: Manage everything needed to build your project',
scope: Scope.Manager,
},
{
value: ActionType.ManagePushKey,
title: 'Push Notifications: Manage your Apple Push Notifications Key',
scope: Scope.Manager,
},
{
value: ActionType.ManageCredentialsJson,
title: 'Credentials.json: Upload/Download credentials between EAS servers and your local json ',
scope: Scope.Manager,
},
{
value: ActionType.GoBackToCaller,
title: 'Go back',
scope: Scope.Manager,
},
];
const credentialsJsonActions: ActionInfo[] = [
{
value: ActionType.UpdateCredentialsJson,
title: 'Download credentials from EAS to credentials.json',
scope: Scope.Project,
},
{
value: ActionType.SetupBuildCredentialsFromCredentialsJson,
title: 'Upload credentials from credentials.json to EAS',
scope: Scope.Project,
},
{
value: ActionType.GoBackToHighLevelActions,
title: 'Go back',
scope: Scope.Manager,
},
];
function getPushKeyActions(ctx: CredentialsContext): ActionInfo[] {
return [
{
value: ActionType.SetupPushKey,
title: 'Setup your project to use Push Notifications',
scope: Scope.Project,
},
{
value: ActionType.CreatePushKey,
title: 'Add a new push key',
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account,
},
{
value: ActionType.UseExistingPushKey,
title: 'Use an existing push key',
scope: Scope.Project,
},
{
value: ActionType.RemovePushKey,
title: 'Remove a push key from your account',
scope: Scope.Account,
},
{
value: ActionType.GoBackToHighLevelActions,
title: 'Go back',
scope: Scope.Manager,
},
];
}
function getBuildCredentialsActions(ctx: CredentialsContext): ActionInfo[] {
return [
{
// This command will be triggered during build to ensure all credentials are ready
// I'm leaving it here for now to simplify testing
value: ActionType.SetupBuildCredentials,
title: 'All: Set up all the required credentials to build your project',
scope: Scope.Project,
},
{
value: ActionType.UseExistingDistributionCertificate,
title: 'Distribution Certificate: Use an existing one for your project',
scope: Scope.Project,
},
{
value: ActionType.CreateDistributionCertificate,
title: `Distribution Certificate: Add a new one to your account`,
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account,
},
{
value: ActionType.RemoveDistributionCertificate,
title: 'Distribution Certificate: Delete one from your account',
scope: Scope.Account,
},
{
value: ActionType.RemoveProvisioningProfile,
title: 'Provisioning Profile: Delete one from your project',
scope: Scope.Project,
},
{
value: ActionType.GoBackToHighLevelActions,
title: 'Go back',
scope: Scope.Manager,
},
];
}
export class ManageIos {
constructor(private callingAction: Action) {}
@@ -226,19 +98,19 @@ export class ManageIos {
throw new Error('Action not supported yet');
}
if (actionInfo.scope === Scope.Manager) {
if (chosenAction === ActionType.ManageBuildCredentials) {
if (chosenAction === IosActionType.ManageBuildCredentials) {
currentActions = buildCredentialsActions;
continue;
} else if (chosenAction === ActionType.ManageCredentialsJson) {
} else if (chosenAction === IosActionType.ManageCredentialsJson) {
currentActions = credentialsJsonActions;
continue;
} else if (chosenAction === ActionType.ManagePushKey) {
} else if (chosenAction === IosActionType.ManagePushKey) {
currentActions = pushKeyActions;
continue;
} else if (chosenAction === ActionType.GoBackToHighLevelActions) {
} else if (chosenAction === IosActionType.GoBackToHighLevelActions) {
currentActions = highLevelActions;
continue;
} else if (chosenAction === ActionType.GoBackToCaller) {
} else if (chosenAction === IosActionType.GoBackToCaller) {
return await this.callingAction.runAsync(ctx);
}
} else if (actionInfo.scope === Scope.Project) {
@@ -315,15 +187,15 @@ export class ManageIos {
private async runAccountSpecificActionAsync(
ctx: CredentialsContext,
account: Account,
action: ActionType
action: IosActionType
): Promise<void> {
if (action === ActionType.RemoveDistributionCertificate) {
if (action === IosActionType.RemoveDistributionCertificate) {
await new SelectAndRemoveDistributionCertificate(account).runAsync(ctx);
} else if (action === ActionType.CreateDistributionCertificate) {
} else if (action === IosActionType.CreateDistributionCertificate) {
await new CreateDistributionCertificate(account).runAsync(ctx);
} else if (action === ActionType.CreatePushKey) {
} else if (action === IosActionType.CreatePushKey) {
await new CreatePushKey(account).runAsync(ctx);
} else if (action === ActionType.RemovePushKey) {
} else if (action === IosActionType.RemovePushKey) {
await new SelectAndRemovePushKey(account).runAsync(ctx);
}
}
@@ -333,9 +205,9 @@ export class ManageIos {
app: App,
targets: Target[],
buildProfile: IosBuildProfile,
action: ActionType
action: IosActionType
): Promise<void> {
if (action === ActionType.SetupBuildCredentials) {
if (action === IosActionType.SetupBuildCredentials) {
await new SetupBuildCredentials({
app,
targets,
@@ -352,12 +224,12 @@ export class ManageIos {
buildProfile
).runAsync(ctx);
if (action === ActionType.SetupBuildCredentialsFromCredentialsJson) {
if (action === IosActionType.SetupBuildCredentialsFromCredentialsJson) {
await new SetupBuildCredentialsFromCredentialsJson(app, targets, distributionType).runAsync(
ctx
);
return;
} else if (action === ActionType.UpdateCredentialsJson) {
} else if (action === IosActionType.UpdateCredentialsJson) {
await new UpdateCredentialsJson(app, targets, distributionType).runAsync(ctx);
return;
}
@@ -365,7 +237,7 @@ export class ManageIos {
const target = await this.selectTargetAsync(targets);
const appLookupParams = getAppLookupParamsFromContext(ctx, target);
switch (action) {
case ActionType.UseExistingDistributionCertificate: {
case IosActionType.UseExistingDistributionCertificate: {
const distCert = await selectValidDistributionCertificateAsync(ctx, appLookupParams);
if (!distCert) {
return;
@@ -378,7 +250,7 @@ export class ManageIos {
);
return;
}
case ActionType.CreateDistributionCertificate: {
case IosActionType.CreateDistributionCertificate: {
const distCert = await new CreateDistributionCertificate(appLookupParams.account).runAsync(
ctx
);
@@ -395,7 +267,7 @@ export class ManageIos {
}
return;
}
case ActionType.RemoveProvisioningProfile: {
case IosActionType.RemoveProvisioningProfile: {
const iosAppCredentials = await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(
appLookupParams
);
@@ -418,7 +290,7 @@ export class ManageIos {
}
return;
}
case ActionType.SetupPushKey: {
case IosActionType.SetupPushKey: {
const setupPushKeyAction = await new SetupPushKey(appLookupParams);
const isPushKeySetup = await setupPushKeyAction.isPushKeySetupAsync(ctx);
if (isPushKeySetup) {
@@ -430,7 +302,7 @@ export class ManageIos {
}
return;
}
case ActionType.CreatePushKey: {
case IosActionType.CreatePushKey: {
const pushKey = await new CreatePushKey(appLookupParams.account).runAsync(ctx);
const confirm = await confirmAsync({
message: `Do you want ${appLookupParams.projectName} to use the new Push Key?`,
@@ -440,7 +312,7 @@ export class ManageIos {
}
return;
}
case ActionType.UseExistingPushKey: {
case IosActionType.UseExistingPushKey: {
const selectedPushKey = await selectPushKeyAsync(ctx, appLookupParams.account);
if (selectedPushKey) {
await new AssignPushKey(appLookupParams).runAsync(ctx, selectedPushKey);
+16 -1
View File
@@ -1,12 +1,17 @@
import {
Client,
CombinedError as GraphqlError,
OperationContext,
OperationResult,
PromisifiedSource,
TypedDocumentNode,
cacheExchange,
createClient as createUrqlClient,
dedupExchange,
fetchExchange,
} from '@urql/core';
import { retryExchange } from '@urql/exchange-retry';
import { DocumentNode } from 'graphql';
// node-fetch is used here because @urql/core uses the fetch API under the hood
// don't use node-fetch elsewhere
// eslint-disable-next-line
@@ -57,7 +62,17 @@ export const graphqlClient = createUrqlClient({
}
return {};
},
});
}) as StricterClient;
/* Please specify additionalTypenames in your Graphql queries */
export interface StricterClient extends Client {
// eslint-disable-next-line @typescript-eslint/ban-types
query<Data = any, Variables extends object = {}>(
query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
variables: Variables | undefined,
context: Partial<OperationContext> & { additionalTypenames: string[] }
): PromisifiedSource<OperationResult<Data, Variables>>;
}
export async function withErrorHandlingAsync<T>(promise: Promise<OperationResult<T>>): Promise<T> {
const { data, error } = await promise;
+176 -165
View File
@@ -147,6 +147,11 @@ export type Actor = {
firstName?: Maybe<Scalars['String']>;
created: Scalars['DateTime'];
isExpoAdmin: Scalars['Boolean'];
/**
* Best-effort human readable name for this actor for use in user interfaces during action attribution.
* For example, when displaying a sentence indicating that actor X created a build or published an update.
*/
displayName: Scalars['String'];
/** Associated accounts */
accounts: Array<Account>;
/** Access Tokens belonging to this actor */
@@ -212,7 +217,7 @@ export type Account = {
applePushKeys: Array<ApplePushKey>;
appleProvisioningProfiles: Array<AppleProvisioningProfile>;
appleDevices: Array<AppleDevice>;
appleAppSpecificPasswords: Array<AppleAppSpecificPassword>;
appStoreConnectApiKeys: Array<AppStoreConnectApiKey>;
/** Android credentials for account */
googleServiceAccountKeys: Array<GoogleServiceAccountKey>;
/** Environment secrets for an account */
@@ -350,13 +355,16 @@ export type Offer = {
trialLength?: Maybe<Scalars['Int']>;
type: OfferType;
features?: Maybe<Array<Maybe<Feature>>>;
prerequisite?: Maybe<OfferPrerequisite>;
};
export enum OfferType {
/** Term subscription */
Subscription = 'SUBSCRIPTION',
/** Advanced Purchase of Paid Resource */
Prepaid = 'PREPAID'
Prepaid = 'PREPAID',
/** Addon, or supplementary subscription */
Addon = 'ADDON'
}
export enum Feature {
@@ -370,6 +378,12 @@ export enum Feature {
OpenSource = 'OPEN_SOURCE'
}
export type OfferPrerequisite = {
__typename?: 'OfferPrerequisite';
type: Scalars['String'];
stripeIds: Array<Scalars['String']>;
};
export type Snack = Project & {
__typename?: 'Snack';
id: Scalars['ID'];
@@ -426,6 +440,7 @@ export type App = Project & {
privacySetting: AppPrivacy;
latestReleaseId: Scalars['ID'];
pushSecurityEnabled: Scalars['Boolean'];
releaseChannels: Array<Scalars['String']>;
/** (EAS Build) Builds associated with this app */
builds: Array<Build>;
buildJobs: Array<BuildJob>;
@@ -581,6 +596,8 @@ export type AppActivityTimelineProjectActivitiesArgs = {
limit: Scalars['Int'];
createdBefore?: Maybe<Scalars['DateTime']>;
filterTypes?: Maybe<Array<ActivityTimelineProjectActivityType>>;
filterPlatforms?: Maybe<Array<AppPlatform>>;
filterReleaseChannels?: Maybe<Array<Scalars['String']>>;
};
@@ -696,6 +713,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
iosEnterpriseProvisioning?: Maybe<BuildIosEnterpriseProvisioning>;
buildProfile?: Maybe<Scalars['String']>;
gitCommitHash?: Maybe<Scalars['String']>;
isGitWorkingTreeDirty?: Maybe<Scalars['Boolean']>;
error?: Maybe<BuildError>;
submissions: Array<Submission>;
};
@@ -723,6 +741,7 @@ export type User = Actor & {
appetizeCode?: Maybe<Scalars['String']>;
emailVerified: Scalars['Boolean'];
isExpoAdmin: Scalars['Boolean'];
displayName: Scalars['String'];
isSecondFactorAuthenticationEnabled: Scalars['Boolean'];
/** Get all certified second factor authentication methods */
secondFactorDevices: Array<UserSecondFactorDevice>;
@@ -956,7 +975,7 @@ export type IosSubmissionConfig = {
__typename?: 'IosSubmissionConfig';
ascAppIdentifier: Scalars['String'];
appleIdUsername: Scalars['String'];
appleAppSpecificPasswordId?: Maybe<Scalars['String']>;
ascApiKeyId?: Maybe<Scalars['String']>;
};
export type SubmissionError = {
@@ -1103,7 +1122,7 @@ export type IosAppCredentials = {
appleAppIdentifier: AppleAppIdentifier;
iosAppBuildCredentialsList: Array<IosAppBuildCredentials>;
pushKey?: Maybe<ApplePushKey>;
appSpecificPassword?: Maybe<AppleAppSpecificPassword>;
appStoreConnectApiKeyForSubmissions?: Maybe<AppStoreConnectApiKey>;
/** @deprecated use iosAppBuildCredentialsList instead */
iosAppBuildCredentialsArray: Array<IosAppBuildCredentials>;
};
@@ -1235,16 +1254,38 @@ export type IosAppBuildCredentialsFilter = {
iosDistributionType?: Maybe<IosDistributionType>;
};
export type AppleAppSpecificPassword = {
__typename?: 'AppleAppSpecificPassword';
export type AppStoreConnectApiKey = {
__typename?: 'AppStoreConnectApiKey';
id: Scalars['ID'];
account: Account;
appleIdUsername: Scalars['String'];
passwordLabel?: Maybe<Scalars['String']>;
appleTeam?: Maybe<AppleTeam>;
issuerIdentifier: Scalars['String'];
keyIdentifier: Scalars['String'];
name?: Maybe<Scalars['String']>;
roles?: Maybe<Array<AppStoreConnectUserRole>>;
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
};
export enum AppStoreConnectUserRole {
Admin = 'ADMIN',
Finance = 'FINANCE',
Technical = 'TECHNICAL',
AccountHolder = 'ACCOUNT_HOLDER',
ReadOnly = 'READ_ONLY',
Sales = 'SALES',
Marketing = 'MARKETING',
AppManager = 'APP_MANAGER',
Developer = 'DEVELOPER',
AccessToReports = 'ACCESS_TO_REPORTS',
CustomerSupport = 'CUSTOMER_SUPPORT',
CreateApps = 'CREATE_APPS',
CloudManagedDeveloperId = 'CLOUD_MANAGED_DEVELOPER_ID',
CloudManagedAppDistribution = 'CLOUD_MANAGED_APP_DISTRIBUTION',
ImageManager = 'IMAGE_MANAGER',
Unknown = 'UNKNOWN'
}
export type AndroidAppCredentialsFilter = {
legacyOnly?: Maybe<Scalars['Boolean']>;
applicationIdentifier?: Maybe<Scalars['String']>;
@@ -1379,6 +1420,7 @@ export type UserPermission = {
export type Billing = {
__typename?: 'Billing';
id: Scalars['ID'];
payment?: Maybe<PaymentDetails>;
subscription?: Maybe<SubscriptionDetails>;
/** History of invoices */
@@ -1413,7 +1455,10 @@ export type Address = {
export type SubscriptionDetails = {
__typename?: 'SubscriptionDetails';
id: Scalars['ID'];
planId?: Maybe<Scalars['String']>;
addons: Array<AddonDetails>;
name?: Maybe<Scalars['String']>;
price: Scalars['Int'];
nextInvoice?: Maybe<Scalars['DateTime']>;
cancelledAt?: Maybe<Scalars['DateTime']>;
willCancel?: Maybe<Scalars['Boolean']>;
@@ -1422,6 +1467,15 @@ export type SubscriptionDetails = {
status?: Maybe<Scalars['String']>;
};
export type AddonDetails = {
__typename?: 'AddonDetails';
id: Scalars['ID'];
planId: Scalars['String'];
name: Scalars['String'];
nextInvoice?: Maybe<Scalars['DateTime']>;
willCancel?: Maybe<Scalars['Boolean']>;
};
export type Charge = {
__typename?: 'Charge';
id: Scalars['ID'];
@@ -1495,7 +1549,10 @@ export type AppQuery = {
/** Look up app by app id */
byId: App;
byFullName: App;
/** Public apps in the app directory */
/**
* Public apps in the app directory
* @deprecated App directory no longer supported
*/
all: Array<App>;
};
@@ -1822,8 +1879,6 @@ export type RootMutation = {
googleServiceAccountKey: GoogleServiceAccountKeyMutation;
/** Mutations that modify an Identifier for an iOS App */
appleAppIdentifier: AppleAppIdentifierMutation;
/** Mutations that modify an App Specific Password for an Apple User Account */
appleAppSpecificPassword: AppleAppSpecificPasswordMutation;
/** Mutations that modify an Apple Device */
appleDevice: AppleDeviceMutation;
/** Mutations that modify an Apple Device registration request */
@@ -1836,6 +1891,8 @@ export type RootMutation = {
applePushKey: ApplePushKeyMutation;
/** Mutations that modify an Apple Team */
appleTeam: AppleTeamMutation;
/** Mutations that modify an App Store Connect Api Key */
appStoreConnectApiKey: AppStoreConnectApiKeyMutation;
/** Mutations that modify an App */
app?: Maybe<AppMutation>;
asset: AssetMutation;
@@ -1941,6 +1998,8 @@ export type AccountMutation = {
subscribeToProduct?: Maybe<Account>;
/** Cancels the active subscription */
cancelSubscription?: Maybe<Account>;
/** Requests a refund for the specified charge. Returns true if auto-refund was possible, otherwise requests a manual refund from support and returns false. */
requestRefund?: Maybe<Scalars['Boolean']>;
/**
* Makes a one time purchase
* @deprecated Build packs are no longer supported
@@ -1986,6 +2045,12 @@ export type AccountMutationCancelSubscriptionArgs = {
};
export type AccountMutationRequestRefundArgs = {
accountID: Scalars['ID'];
chargeIdentifier: Scalars['ID'];
};
export type AccountMutationBuyProductArgs = {
accountName: Scalars['ID'];
productId: Scalars['ID'];
@@ -2213,24 +2278,6 @@ export type AppleAppIdentifierInput = {
parentAppleAppId?: Maybe<Scalars['ID']>;
};
export type AppleAppSpecificPasswordMutation = {
__typename?: 'AppleAppSpecificPasswordMutation';
/** Create an App Specific Password for an Apple User Account */
createAppleAppSpecificPassword: AppleAppSpecificPassword;
};
export type AppleAppSpecificPasswordMutationCreateAppleAppSpecificPasswordArgs = {
appleAppSpecificPasswordInput: AppleAppSpecificPasswordInput;
accountId: Scalars['ID'];
};
export type AppleAppSpecificPasswordInput = {
appleIdUsername: Scalars['String'];
passwordLabel?: Maybe<Scalars['String']>;
appSpecificPassword: Scalars['String'];
};
export type AppleDeviceMutation = {
__typename?: 'AppleDeviceMutation';
/** Create an Apple Device */
@@ -2401,6 +2448,39 @@ export type AppleTeamInput = {
appleTeamName?: Maybe<Scalars['String']>;
};
export type AppStoreConnectApiKeyMutation = {
__typename?: 'AppStoreConnectApiKeyMutation';
/** Create an App Store Connect Api Key for an Apple Team */
createAppStoreConnectApiKey: AppStoreConnectApiKey;
/** Delete an App Store Connect Api Key */
deleteAppStoreConnectApiKey: DeleteAppStoreConnectApiKeyResult;
};
export type AppStoreConnectApiKeyMutationCreateAppStoreConnectApiKeyArgs = {
appStoreConnectApiKeyInput: AppStoreConnectApiKeyInput;
accountId: Scalars['ID'];
};
export type AppStoreConnectApiKeyMutationDeleteAppStoreConnectApiKeyArgs = {
id: Scalars['ID'];
};
export type AppStoreConnectApiKeyInput = {
issuerIdentifier: Scalars['String'];
keyIdentifier: Scalars['String'];
keyP8: Scalars['String'];
name?: Maybe<Scalars['String']>;
roles?: Maybe<Array<AppStoreConnectUserRole>>;
appleTeamId?: Maybe<Scalars['ID']>;
};
export type DeleteAppStoreConnectApiKeyResult = {
__typename?: 'deleteAppStoreConnectApiKeyResult';
id: Scalars['ID'];
};
export type AppMutation = {
__typename?: 'AppMutation';
/** Create an unpublished app */
@@ -2471,22 +2551,8 @@ export type BuildMutation = {
* @deprecated Use cancelBuild instead
*/
cancel: Build;
/**
* Create an Android generic build
* @deprecated Use createAndroidBuild instead
*/
createAndroidGenericBuild: CreateBuildResult;
/**
* Create an Android managed build
* @deprecated Use createAndroidBuild instead
*/
createAndroidManagedBuild: CreateBuildResult;
/** Create an Android build */
createAndroidBuild: CreateBuildResult;
/** Create an iOS generic build */
createIosGenericBuild: CreateBuildResult;
/** Create an iOS managed build */
createIosManagedBuild: CreateBuildResult;
/** Create an iOS build */
createIosBuild: CreateBuildResult;
};
@@ -2502,20 +2568,6 @@ export type BuildMutationDeleteBuildArgs = {
};
export type BuildMutationCreateAndroidGenericBuildArgs = {
appId: Scalars['ID'];
job: AndroidGenericJobInput;
metadata?: Maybe<BuildMetadataInput>;
};
export type BuildMutationCreateAndroidManagedBuildArgs = {
appId: Scalars['ID'];
job: AndroidManagedJobInput;
metadata?: Maybe<BuildMetadataInput>;
};
export type BuildMutationCreateAndroidBuildArgs = {
appId: Scalars['ID'];
job: AndroidJobInput;
@@ -2523,38 +2575,33 @@ export type BuildMutationCreateAndroidBuildArgs = {
};
export type BuildMutationCreateIosGenericBuildArgs = {
appId: Scalars['ID'];
job: IosGenericJobInput;
metadata?: Maybe<BuildMetadataInput>;
};
export type BuildMutationCreateIosManagedBuildArgs = {
appId: Scalars['ID'];
job: IosManagedJobInput;
metadata?: Maybe<BuildMetadataInput>;
};
export type BuildMutationCreateIosBuildArgs = {
appId: Scalars['ID'];
job: IosJobInput;
metadata?: Maybe<BuildMetadataInput>;
};
export type AndroidGenericJobInput = {
export type AndroidJobInput = {
type: BuildWorkflow;
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: Maybe<Scalars['String']>;
updates?: Maybe<BuildUpdatesInput>;
developmentClient?: Maybe<Scalars['Boolean']>;
secrets?: Maybe<AndroidJobSecretsInput>;
builderEnvironment?: Maybe<AndroidBuilderEnvironmentInput>;
cache?: Maybe<BuildCacheInput>;
gradleCommand?: Maybe<Scalars['String']>;
artifactPath?: Maybe<Scalars['String']>;
buildType?: Maybe<AndroidBuildType>;
username?: Maybe<Scalars['String']>;
};
export enum BuildWorkflow {
Generic = 'GENERIC',
Managed = 'MANAGED'
}
export type ProjectArchiveSourceInput = {
type: ProjectArchiveSourceType;
bucketKey?: Maybe<Scalars['String']>;
@@ -2603,6 +2650,13 @@ export type BuildCacheInput = {
customPaths?: Maybe<Array<Maybe<Scalars['String']>>>;
};
export enum AndroidBuildType {
Apk = 'APK',
AppBundle = 'APP_BUNDLE',
/** @deprecated Use developmentClient option instead. */
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export type BuildMetadataInput = {
trackingContext?: Maybe<Scalars['JSONObject']>;
appVersion?: Maybe<Scalars['String']>;
@@ -2620,14 +2674,10 @@ export type BuildMetadataInput = {
appIdentifier?: Maybe<Scalars['String']>;
buildProfile?: Maybe<Scalars['String']>;
gitCommitHash?: Maybe<Scalars['String']>;
isGitWorkingTreeDirty?: Maybe<Scalars['Boolean']>;
username?: Maybe<Scalars['String']>;
};
export enum BuildWorkflow {
Generic = 'GENERIC',
Managed = 'MANAGED'
}
export enum BuildCredentialsSource {
Local = 'LOCAL',
Remote = 'REMOTE'
@@ -2650,58 +2700,25 @@ export enum EasBuildDeprecationInfoType {
Internal = 'INTERNAL'
}
export type AndroidManagedJobInput = {
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: Maybe<Scalars['String']>;
updates?: Maybe<BuildUpdatesInput>;
secrets?: Maybe<AndroidJobSecretsInput>;
builderEnvironment?: Maybe<AndroidBuilderEnvironmentInput>;
cache?: Maybe<BuildCacheInput>;
buildType?: Maybe<AndroidManagedBuildType>;
username?: Maybe<Scalars['String']>;
};
export enum AndroidManagedBuildType {
Apk = 'APK',
AppBundle = 'APP_BUNDLE',
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export type AndroidJobInput = {
export type IosJobInput = {
type: BuildWorkflow;
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: Maybe<Scalars['String']>;
updates?: Maybe<BuildUpdatesInput>;
secrets?: Maybe<AndroidJobSecretsInput>;
builderEnvironment?: Maybe<AndroidBuilderEnvironmentInput>;
cache?: Maybe<BuildCacheInput>;
gradleCommand?: Maybe<Scalars['String']>;
artifactPath?: Maybe<Scalars['String']>;
buildType?: Maybe<AndroidBuildType>;
username?: Maybe<Scalars['String']>;
};
export enum AndroidBuildType {
Apk = 'APK',
AppBundle = 'APP_BUNDLE',
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export type IosGenericJobInput = {
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: Maybe<Scalars['String']>;
updates?: Maybe<BuildUpdatesInput>;
/** @deprecated */
distribution?: Maybe<DistributionType>;
simulator?: Maybe<Scalars['Boolean']>;
developmentClient?: Maybe<Scalars['Boolean']>;
secrets?: Maybe<IosJobSecretsInput>;
builderEnvironment?: Maybe<IosBuilderEnvironmentInput>;
cache?: Maybe<BuildCacheInput>;
scheme: Scalars['String'];
schemeBuildConfiguration?: Maybe<IosSchemeBuildConfiguration>;
scheme?: Maybe<Scalars['String']>;
buildConfiguration?: Maybe<Scalars['String']>;
artifactPath?: Maybe<Scalars['String']>;
/** @deprecated */
buildType?: Maybe<IosBuildType>;
username?: Maybe<Scalars['String']>;
};
export type IosJobSecretsInput = {
@@ -2731,46 +2748,7 @@ export type IosBuilderEnvironmentInput = {
env?: Maybe<Scalars['JSONObject']>;
};
export enum IosSchemeBuildConfiguration {
Release = 'RELEASE',
Debug = 'DEBUG'
}
export type IosManagedJobInput = {
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: Maybe<Scalars['String']>;
updates?: Maybe<BuildUpdatesInput>;
distribution?: Maybe<DistributionType>;
secrets?: Maybe<IosJobSecretsInput>;
builderEnvironment?: Maybe<IosBuilderEnvironmentInput>;
cache?: Maybe<BuildCacheInput>;
buildType?: Maybe<IosManagedBuildType>;
username?: Maybe<Scalars['String']>;
};
export enum IosManagedBuildType {
Release = 'RELEASE',
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export type IosJobInput = {
type: BuildWorkflow;
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: Maybe<Scalars['String']>;
updates?: Maybe<BuildUpdatesInput>;
distribution?: Maybe<DistributionType>;
secrets?: Maybe<IosJobSecretsInput>;
builderEnvironment?: Maybe<IosBuilderEnvironmentInput>;
cache?: Maybe<BuildCacheInput>;
scheme?: Maybe<Scalars['String']>;
buildConfiguration?: Maybe<Scalars['String']>;
artifactPath?: Maybe<Scalars['String']>;
buildType?: Maybe<IosBuildType>;
username?: Maybe<Scalars['String']>;
};
/** @deprecated Use developmentClient option instead. */
export enum IosBuildType {
Release = 'RELEASE',
DevelopmentClient = 'DEVELOPMENT_CLIENT'
@@ -2816,8 +2794,8 @@ export type IosAppCredentialsMutation = {
createIosAppCredentials: IosAppCredentials;
/** Set the push key to be used in an iOS app */
setPushKey: IosAppCredentials;
/** Set the app-specific password to be used for an iOS app */
setAppSpecificPassword: IosAppCredentials;
/** Set the App Store Connect Api Key to be used for submitting an iOS app */
setAppStoreConnectApiKeyForSubmissions: IosAppCredentials;
};
@@ -2834,15 +2812,15 @@ export type IosAppCredentialsMutationSetPushKeyArgs = {
};
export type IosAppCredentialsMutationSetAppSpecificPasswordArgs = {
export type IosAppCredentialsMutationSetAppStoreConnectApiKeyForSubmissionsArgs = {
id: Scalars['ID'];
appSpecificPasswordId: Scalars['ID'];
ascApiKeyId: Scalars['ID'];
};
export type IosAppCredentialsInput = {
appleTeamId?: Maybe<Scalars['ID']>;
pushKeyId?: Maybe<Scalars['ID']>;
appSpecificPasswordId?: Maybe<Scalars['ID']>;
appStoreConnectApiKeyForSubmissionsId?: Maybe<Scalars['ID']>;
};
export type RobotMutation = {
@@ -2884,6 +2862,7 @@ export type Robot = Actor & {
firstName?: Maybe<Scalars['String']>;
created: Scalars['DateTime'];
isExpoAdmin: Scalars['Boolean'];
displayName: Scalars['String'];
/** Associated accounts */
accounts: Array<Account>;
/** Access Tokens belonging to this actor */
@@ -2954,13 +2933,20 @@ export type CreateIosSubmissionInput = {
};
export type IosSubmissionConfigInput = {
appleAppSpecificPasswordId?: Maybe<Scalars['String']>;
appleAppSpecificPassword?: Maybe<Scalars['String']>;
ascApiKey?: Maybe<AscApiKeyInput>;
ascApiKeyId?: Maybe<Scalars['String']>;
archiveUrl?: Maybe<Scalars['String']>;
appleIdUsername: Scalars['String'];
appleIdUsername?: Maybe<Scalars['String']>;
ascAppIdentifier: Scalars['String'];
};
export type AscApiKeyInput = {
keyP8: Scalars['String'];
keyIdentifier: Scalars['String'];
issuerIdentifier: Scalars['String'];
};
export type CreateAndroidSubmissionInput = {
appId: Scalars['ID'];
config: AndroidSubmissionConfigInput;
@@ -3512,6 +3498,17 @@ export enum StandardOffer {
Support = 'SUPPORT'
}
export enum IosSchemeBuildConfiguration {
Release = 'RELEASE',
Debug = 'DEBUG'
}
/** @deprecated Use developmentClient option instead. */
export enum IosManagedBuildType {
Release = 'RELEASE',
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export enum CacheControlScope {
Public = 'PUBLIC',
Private = 'PRIVATE'
@@ -5362,6 +5359,16 @@ export type AndroidKeystoreFragment = (
& Pick<AndroidKeystore, 'id' | 'type' | 'keystore' | 'keystorePassword' | 'keyAlias' | 'keyPassword' | 'md5CertificateFingerprint' | 'sha1CertificateFingerprint' | 'sha256CertificateFingerprint' | 'createdAt' | 'updatedAt'>
);
export type AppStoreConnectApiKeyFragment = (
{ __typename?: 'AppStoreConnectApiKey' }
& Pick<AppStoreConnectApiKey, 'id' | 'issuerIdentifier' | 'keyIdentifier' | 'name' | 'roles' | 'createdAt' | 'updatedAt'>
& { appleTeam?: Maybe<(
{ __typename?: 'AppleTeam' }
& Pick<AppleTeam, 'id'>
& AppleTeamFragment
)> }
);
export type AppleAppIdentifierFragment = (
{ __typename?: 'AppleAppIdentifier' }
& Pick<AppleAppIdentifier, 'id' | 'bundleIdentifier'>
@@ -5491,6 +5498,10 @@ export type CommonIosAppCredentialsWithoutBuildCredentialsFragment = (
{ __typename?: 'ApplePushKey' }
& Pick<ApplePushKey, 'id'>
& ApplePushKeyFragment
)>, appStoreConnectApiKeyForSubmissions?: Maybe<(
{ __typename?: 'AppStoreConnectApiKey' }
& Pick<AppStoreConnectApiKey, 'id'>
& AppStoreConnectApiKeyFragment
)> }
);
@@ -54,6 +54,7 @@ export const BuildQuery = {
{ buildId },
{
requestPolicy: useCache ? 'cache-first' : 'network-only',
additionalTypenames: ['Build'],
}
)
.toPromise()
@@ -89,7 +90,10 @@ export const BuildQuery = {
}
${print(BuildFragmentNode)}
`,
{ appId, offset, limit, filter }
{ appId, offset, limit, filter },
{
additionalTypenames: ['Build'],
}
)
.toPromise()
);
@@ -37,7 +37,10 @@ export const EnvironmentSecretsQuery = {
}
${print(EnvironmentSecretFragmentNode)}
`,
{ accountName }
{ accountName },
{
additionalTypenames: ['EnvironmentSecret'],
}
)
.toPromise()
);
@@ -62,7 +65,8 @@ export const EnvironmentSecretsQuery = {
}
${print(EnvironmentSecretFragmentNode)}
`,
{ appId }
{ appId },
{ additionalTypenames: ['EnvironmentSecret'] }
)
.toPromise()
);
@@ -19,7 +19,10 @@ export const ProjectQuery = {
}
}
`,
{ username, slug }
{ username, slug },
{
additionalTypenames: ['App', 'Snack'] /* typenames that use the Project type*/,
}
)
.toPromise()
);
@@ -21,7 +21,10 @@ export const PublishQuery = {
{
storageKeys,
},
{ requestPolicy: 'network-only' } // Since we reptitively query this to monitor the asset upload, we need to ensure it is not cached.
{
requestPolicy: 'network-only',
additionalTypenames: ['AssetMetadataResult'],
} // Since we reptitively query this to monitor the asset upload, we need to ensure it is not cached.
)
.toPromise()
);
@@ -40,7 +40,10 @@ export const SubmissionQuery = {
${print(SubmissionFragmentNode)}
`,
{ submissionId },
{ requestPolicy: useCache ? 'cache-first' : 'network-only' }
{
requestPolicy: useCache ? 'cache-first' : 'network-only',
additionalTypenames: ['Submission'],
}
)
.toPromise()
);
@@ -78,7 +81,8 @@ export const SubmissionQuery = {
}
${print(SubmissionFragmentNode)}
`,
{ appId, offset, limit, status, platform }
{ appId, offset, limit, status, platform },
{ additionalTypenames: ['Submission'] }
)
.toPromise()
);
@@ -26,7 +26,11 @@ export const UserQuery = {
isExpoAdmin
}
}
`
`,
/* variables */ undefined,
{
additionalTypenames: ['User'],
}
)
.toPromise()
);
@@ -31,7 +31,10 @@ export const WebhookQuery = {
}
${print(WebhookFragmentNode)}
`,
{ appId, webhookFilter }
{ appId, webhookFilter },
{
additionalTypenames: ['Webhook'],
}
)
.toPromise()
);
@@ -52,7 +55,10 @@ export const WebhookQuery = {
}
${print(WebhookFragmentNode)}
`,
{ webhookId }
{ webhookId },
{
additionalTypenames: ['Webhook'],
}
)
.toPromise()
);
@@ -0,0 +1,21 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { AppleTeamFragmentNode } from './AppleTeam';
export const AppStoreConnectApiKeyFragmentNode = gql`
fragment AppStoreConnectApiKeyFragment on AppStoreConnectApiKey {
id
appleTeam {
id
...AppleTeamFragment
}
issuerIdentifier
keyIdentifier
name
roles
createdAt
updatedAt
}
${print(AppleTeamFragmentNode)}
`;
@@ -1,6 +1,7 @@
import gql from 'graphql-tag';
import { AppFragmentNode } from '../App';
import { AppStoreConnectApiKeyFragmentNode } from './AppStoreConnectApiKey';
import { AppleAppIdentifierFragmentNode } from './AppleAppIdentifier';
import { ApplePushKeyFragmentNode } from './ApplePushKey';
import { AppleTeamFragmentNode } from './AppleTeam';
@@ -25,11 +26,16 @@ export const CommonIosAppCredentialsWithoutBuildCredentialsFragmentNode = gql`
id
...ApplePushKeyFragment
}
appStoreConnectApiKeyForSubmissions {
id
...AppStoreConnectApiKeyFragment
}
}
${AppFragmentNode}
${AppleTeamFragmentNode}
${AppleAppIdentifierFragmentNode}
${ApplePushKeyFragmentNode}
${AppStoreConnectApiKeyFragmentNode}
`;
export const CommonIosAppCredentialsFragmentNode = gql`
+2 -14
View File
@@ -24,11 +24,11 @@ export default class Log {
}
public static error(...args: any[]): void {
Log.consoleError(...Log.withTextColor(args, chalk.red));
Log.consoleLog(...Log.withTextColor(args, chalk.red));
}
public static warn(...args: any[]): void {
Log.consoleWarn(...Log.withTextColor(args, chalk.yellow));
Log.consoleLog(...Log.withTextColor(args, chalk.yellow));
}
public static debug(...args: any[]): void {
@@ -59,18 +59,6 @@ export default class Log {
console.log(...args);
}
private static consoleWarn(...args: any[]): void {
Log.updateIsLastLineNewLine(args);
// eslint-disable-next-line no-console
console.warn(...args);
}
private static consoleError(...args: any[]): void {
Log.updateIsLastLineNewLine(args);
// eslint-disable-next-line no-console
console.error(...args);
}
private static withTextColor(args: any[], chalkColor: Color): string[] {
return args.map(arg => chalkColor(arg));
}
@@ -12,7 +12,7 @@ describe(resolveWorkflowAsync, () => {
const projectDir = '/app';
test('generic workflow for both platforms', async () => {
test('bare workflow for both platforms', async () => {
vol.fromJSON(
{
'./ios/helloworld.xcodeproj/project.pbxproj': 'fake',
@@ -27,7 +27,7 @@ describe(resolveWorkflowAsync, () => {
await expect(resolveWorkflowAsync(projectDir, Platform.IOS)).resolves.toBe(Workflow.GENERIC);
});
test('generic workflow for single platform', async () => {
test('bare workflow for single platform', async () => {
vol.fromJSON(
{
'./ios/helloworld.xcodeproj/project.pbxproj': 'fake',
@@ -25,7 +25,7 @@ beforeEach(async () => {
});
describe(getApplicationIdAsync, () => {
describe('generic projects', () => {
describe('bare projects', () => {
it('reads applicationId from build.gradle', async () => {
vol.fromJSON(
{
@@ -21,7 +21,7 @@ beforeEach(async () => {
});
describe(resolveGradleBuildContextAsync, () => {
describe('generic projects', () => {
describe('bare projects', () => {
it('resolves to default config (no flavor)', async () => {
vol.fromJSON(
{
@@ -6,6 +6,7 @@ import chalk from 'chalk';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { readAppJson } from '../../build/utils/appJson';
import Log, { learnMore } from '../../log';
import { getProjectConfigDescription, getUsername } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
@@ -39,7 +40,7 @@ export async function getApplicationIdAsync(
): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
warnIfAndroidPackageDefinedInAppConfigForGenericProject(projectDir, exp);
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(projectDir, exp);
const errorMessage = 'Could not read application id from Android project.';
if (gradleContext) {
@@ -112,7 +113,7 @@ async function configureApplicationIdAsync(projectDir: string, exp: ExpoConfig):
validate: value => (isApplicationIdValid(value) ? true : INVALID_APPLICATION_ID_MESSAGE),
});
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
const rawStaticConfig = readAppJson(paths.staticConfigPath);
rawStaticConfig.expo = {
...rawStaticConfig.expo,
android: { ...rawStaticConfig.expo?.android, package: packageName },
@@ -129,7 +130,7 @@ function isApplicationIdValid(applicationId: string): boolean {
}
let warnPrinted = false;
export function warnIfAndroidPackageDefinedInAppConfigForGenericProject(
export function warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(
projectDir: string,
exp: ExpoConfig
): void {
@@ -137,7 +138,7 @@ export function warnIfAndroidPackageDefinedInAppConfigForGenericProject(
Log.warn(
`Specifying "android.package" in ${getProjectConfigDescription(
projectDir
)} is deprecated for generic projects.\n` +
)} is deprecated for bare workflow projects.\n` +
'EAS Build depends only on the value in the native code. Please remove the deprecated configuration.'
);
warnPrinted = true;
@@ -29,7 +29,7 @@ beforeEach(async () => {
const originalFs = jest.requireActual('fs') as typeof fs;
describe(getBundleIdentifierAsync, () => {
describe('generic projects', () => {
describe('bare projects', () => {
it('reads bundle identifier from project.', async () => {
vol.fromJSON(
{
@@ -5,6 +5,7 @@ import assert from 'assert';
import chalk from 'chalk';
import fs from 'fs-extra';
import { readAppJson } from '../../build/utils/appJson';
import Log, { learnMore } from '../../log';
import { promptAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions';
@@ -34,7 +35,7 @@ export async function getBundleIdentifierAsync(
): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
warnIfBundleIdentifierDefinedInAppConfigForGenericProject(projectDir, exp);
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(projectDir, exp);
const bundleIdentifier = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(projectDir, {
targetName,
@@ -107,7 +108,7 @@ async function configureBundleIdentifierAsync(
validate: value => (isBundleIdentifierValid(value) ? true : INVALID_BUNDLE_IDENTIFIER_MESSAGE),
});
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
const rawStaticConfig = readAppJson(paths.staticConfigPath);
rawStaticConfig.expo = {
...rawStaticConfig.expo,
ios: { ...rawStaticConfig.expo?.ios, bundleIdentifier },
@@ -124,7 +125,7 @@ function isBundleIdentifierValid(bundleIdentifier: string): boolean {
}
let warnPrinted = false;
export function warnIfBundleIdentifierDefinedInAppConfigForGenericProject(
export function warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(
projectDir: string,
exp: ExpoConfig
): void {
@@ -132,7 +133,7 @@ export function warnIfBundleIdentifierDefinedInAppConfigForGenericProject(
Log.warn(
`Specifying "ios.bundleIdentifier" in ${getProjectConfigDescription(
projectDir
)} is deprecated for generic projects.\n` +
)} is deprecated for bare workflow projects.\n` +
'EAS Build depends only on the value in the native code. Please remove the deprecated configuration.'
);
warnPrinted = true;
+2 -1
View File
@@ -231,7 +231,8 @@ export async function getBranchByNameAsync({
{
appId,
name,
}
},
{ additionalTypenames: ['UpdateBranch'] }
)
.toPromise()
);
+1 -1
View File
@@ -12,7 +12,7 @@ import { PresignedPost } from '../graphql/mutations/UploadSessionMutation';
import { PublishQuery } from '../graphql/queries/PublishQuery';
import Log from '../log';
import { uploadWithPresignedPostAsync } from '../uploads';
import { expoCommandAsync } from '../utils/expoCommand';
import { expoCommandAsync } from '../utils/expoCli';
import uniqBy from '../utils/expodash/uniqBy';
export const TIMEOUT_LIMIT = 60_000; // 1 minute
+4 -2
View File
@@ -3,7 +3,7 @@ import { Platform, Workflow } from '@expo/eas-build-job';
import fs from 'fs-extra';
import path from 'path';
import vcs from '../vcs';
import { getVcsClient } from '../vcs';
export async function resolveWorkflowAsync(
projectDir: string,
@@ -20,7 +20,9 @@ export async function resolveWorkflowAsync(
}
if (await fs.pathExists(platformWorkflowMarker)) {
return (await vcs.isFileIgnoredAsync(path.relative(projectDir, platformWorkflowMarker)))
return (await getVcsClient().isFileIgnoredAsync(
path.relative(projectDir, platformWorkflowMarker)
))
? Workflow.MANAGED
: Workflow.GENERIC;
} else {
+133 -11
View File
@@ -1,22 +1,26 @@
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import prompts from 'prompts';
import { URL } from 'url';
import * as uuid from 'uuid';
import { BuildFragment } from '../graphql/generated';
import { AppPlatform, BuildFragment } from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log, { learnMore } from '../log';
import { appPlatformDisplayNames } from '../platform';
import { confirmAsync, promptAsync } from '../prompts';
import { getLatestBuildForSubmissionAsync } from './utils/builds';
import { getRecentBuildsForSubmissionAsync } from './utils/builds';
import { isExistingFileAsync, uploadAppArchiveAsync } from './utils/files';
export const BUILD_LIST_ITEM_COUNT = 4;
export enum ArchiveSourceType {
url,
latest,
path,
buildId,
buildList,
prompt,
}
@@ -46,6 +50,10 @@ interface ArchiveBuildIdSource extends ArchiveSourceBase {
id: string;
}
interface ArchiveBuildListSource extends ArchiveSourceBase {
sourceType: ArchiveSourceType.buildList;
}
interface ArchivePromptSource extends ArchiveSourceBase {
sourceType: ArchiveSourceType.prompt;
}
@@ -61,6 +69,7 @@ export type ArchiveSource =
| ArchiveLatestSource
| ArchivePathSource
| ArchiveBuildIdSource
| ArchiveBuildListSource
| ArchivePromptSource;
export async function getArchiveAsync(source: ArchiveSource): Promise<Archive> {
@@ -80,6 +89,9 @@ export async function getArchiveAsync(source: ArchiveSource): Promise<Archive> {
case ArchiveSourceType.buildId: {
return await handleBuildIdSourceAsync(source);
}
case ArchiveSourceType.buildList: {
return await handleBuildListSourceAsync(source);
}
}
}
@@ -113,7 +125,7 @@ async function handleUrlSourceAsync(source: ArchiveUrlSource): Promise<Archive>
async function handleLatestSourceAsync(source: ArchiveLatestSource): Promise<Archive> {
try {
const latestBuild = await getLatestBuildForSubmissionAsync(
const [latestBuild] = await getRecentBuildsForSubmissionAsync(
toAppPlatform(source.platform),
source.projectId
);
@@ -197,6 +209,116 @@ async function handleBuildIdSourceAsync(source: ArchiveBuildIdSource): Promise<A
}
}
async function handleBuildListSourceAsync(source: ArchiveBuildListSource): Promise<Archive> {
try {
const appPlatform = toAppPlatform(source.platform);
const expiryDate = new Date(); // artifacts expire after 30 days
expiryDate.setDate(expiryDate.getDate() - 30);
const recentBuilds = await getRecentBuildsForSubmissionAsync(appPlatform, source.projectId, {
limit: BUILD_LIST_ITEM_COUNT,
});
if (recentBuilds.length < 1) {
Log.error(
chalk.bold(
`Couldn't find any ${appPlatformDisplayNames[appPlatform]} builds for this project on EAS servers. ` +
"It looks like you haven't run 'eas build' yet."
)
);
return getArchiveAsync({
...source,
sourceType: ArchiveSourceType.prompt,
});
}
if (recentBuilds.every(it => new Date(it.updatedAt) < expiryDate)) {
Log.error(
chalk.bold(
'It looks like all of your build artifacts have expired. ' +
'EAS keeps your build artifacts only for 30 days.'
)
);
return getArchiveAsync({
...source,
sourceType: ArchiveSourceType.prompt,
});
}
const choices = recentBuilds.map(build => formatBuildChoice(build, expiryDate));
choices.push({
title: 'None of the above (select another option)',
value: null,
});
const { selectedBuild } = await promptAsync({
name: 'selectedBuild',
type: 'select',
message: 'Which build would you like to submit?',
choices: choices.map(choice => ({ ...choice, title: `- ${choice.title}` })),
// @ts-expect-error field documented in npm, but not defined in typescript
warn: 'This artifact has expired',
});
if (selectedBuild == null) {
return getArchiveAsync({
...source,
sourceType: ArchiveSourceType.prompt,
});
}
return {
build: selectedBuild,
source,
};
} catch (err) {
Log.error(err);
throw err;
}
}
function formatBuildChoice(build: BuildFragment, expiryDate: Date): prompts.Choice {
const {
id,
platform,
updatedAt,
appVersion,
sdkVersion,
runtimeVersion,
buildProfile,
appBuildVersion,
releaseChannel,
} = build;
const formatValue = (field?: string | null): string =>
field ? chalk.bold(field) : chalk.dim('Unknown');
const buildDate = new Date(updatedAt);
const maybeRuntimeVersion = runtimeVersion ? `Runtime: ${formatValue(runtimeVersion)}` : null;
const maybeSdkVersion = sdkVersion ? `SDK: ${formatValue(sdkVersion)}` : null;
const appBuildVersionString = `${
platform === AppPlatform.Android ? 'Version code' : 'Build number'
}: ${formatValue(appBuildVersion)}`;
const title = [
`ID: ${chalk.dim(id)}, Finished at: ${chalk.bold(buildDate.toLocaleString())}`,
[
`\tApp version: ${formatValue(appVersion)}, ${appBuildVersionString}`,
maybeRuntimeVersion,
maybeSdkVersion,
]
.filter(it => it != null)
.join(', '),
`\tProfile: ${formatValue(buildProfile)}, Release channel: ${formatValue(releaseChannel)}`,
].join('\n');
return {
title,
value: build,
disabled: buildDate < expiryDate,
};
}
async function handlePromptSourceAsync(source: ArchivePromptSource): Promise<Archive> {
const { sourceType: sourceTypeRaw } = await promptAsync({
name: 'sourceType',
@@ -204,16 +326,16 @@ async function handlePromptSourceAsync(source: ArchivePromptSource): Promise<Arc
message: 'What would you like to submit?',
choices: [
{
title: 'Latest finished build from EAS',
value: ArchiveSourceType.latest,
title: 'Select a build from EAS',
value: ArchiveSourceType.buildList,
},
{ title: 'I have a url to the app archive', value: ArchiveSourceType.url },
{ title: 'Provide a URL to the app archive', value: ArchiveSourceType.url },
{
title: 'Local app binary file',
title: 'Provide a path to a local app binary file',
value: ArchiveSourceType.path,
},
{
title: 'A build identified by a build ID',
title: 'Provide a build ID to identify a build on EAS',
value: ArchiveSourceType.buildId,
},
],
@@ -236,10 +358,10 @@ async function handlePromptSourceAsync(source: ArchivePromptSource): Promise<Arc
path,
});
}
case ArchiveSourceType.latest: {
case ArchiveSourceType.buildList: {
return getArchiveAsync({
...source,
sourceType: ArchiveSourceType.latest,
sourceType: ArchiveSourceType.buildList,
});
}
case ArchiveSourceType.buildId: {
@@ -250,7 +372,7 @@ async function handlePromptSourceAsync(source: ArchivePromptSource): Promise<Arc
id,
});
}
case ArchiveSourceType.prompt:
default:
throw new Error('This should never happen');
}
}
@@ -9,8 +9,13 @@ import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { toAppPlatform } from '../../graphql/types/AppPlatform';
import { confirmAsync, promptAsync } from '../../prompts';
import { uploadAsync } from '../../uploads';
import { Archive, ArchiveSourceType, getArchiveAsync } from '../ArchiveSource';
import { getLatestBuildForSubmissionAsync } from '../utils/builds';
import {
Archive,
ArchiveSourceType,
BUILD_LIST_ITEM_COUNT,
getArchiveAsync,
} from '../ArchiveSource';
import { getRecentBuildsForSubmissionAsync } from '../utils/builds';
jest.mock('fs');
jest.mock('../../log');
@@ -32,6 +37,7 @@ const MOCK_BUILD_FRAGMENT: Partial<BuildFragment> = {
},
appVersion: '1.2.3',
platform: AppPlatform.Android,
updatedAt: Date.now(),
};
const SOURCE_STUB_INPUT = {
@@ -140,7 +146,7 @@ describe(getArchiveAsync, () => {
it('handles latest build source', async () => {
const projectId = uuidv4();
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(MOCK_BUILD_FRAGMENT);
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([MOCK_BUILD_FRAGMENT]);
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
@@ -148,7 +154,7 @@ describe(getArchiveAsync, () => {
sourceType: ArchiveSourceType.latest,
});
expect(getLatestBuildForSubmissionAsync).toBeCalledWith(
expect(getRecentBuildsForSubmissionAsync).toBeCalledWith(
toAppPlatform(SOURCE_STUB_INPUT.platform),
projectId
);
@@ -156,7 +162,7 @@ describe(getArchiveAsync, () => {
});
it('prompts again if no builds exists when selected latest', async () => {
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(null);
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([]);
asMock(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
@@ -169,6 +175,59 @@ describe(getArchiveAsync, () => {
assertArchiveResult(archive, ArchiveSourceType.url);
});
it('handles build-list-select source', async () => {
const projectId = uuidv4();
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([MOCK_BUILD_FRAGMENT]);
asMock(promptAsync).mockResolvedValueOnce({ selectedBuild: MOCK_BUILD_FRAGMENT });
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
projectId,
sourceType: ArchiveSourceType.buildList,
});
expect(getRecentBuildsForSubmissionAsync).toBeCalledWith(
toAppPlatform(SOURCE_STUB_INPUT.platform),
projectId,
{ limit: BUILD_LIST_ITEM_COUNT }
);
assertArchiveResult(archive, ArchiveSourceType.buildList);
});
it('prompts again if all builds have expired', async () => {
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([
{
...MOCK_BUILD_FRAGMENT,
updatedAt: new Date(Date.now() - 31 * 24 * 3600 * 1000),
},
]);
asMock(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.buildList,
});
assertArchiveResult(archive, ArchiveSourceType.url);
});
it('falls back to prompt if user selected "None of the above"', async () => {
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([MOCK_BUILD_FRAGMENT]);
asMock(promptAsync)
.mockResolvedValueOnce({ selectedBuild: null })
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.buildList,
});
assertArchiveResult(archive, ArchiveSourceType.url);
});
it('handles path source', async () => {
const path = '/archive.apk';
await fs.writeFile(path, 'some content');
@@ -15,7 +15,7 @@ import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutatio
import { createTestProject } from '../../../project/__tests__/project-utils';
import { getProjectIdAsync } from '../../../project/projectUtils';
import { createSubmissionContextAsync } from '../../context';
import { getLatestBuildForSubmissionAsync } from '../../utils/builds';
import { getRecentBuildsForSubmissionAsync } from '../../utils/builds';
import AndroidSubmitCommand from '../AndroidSubmitCommand';
jest.mock('fs');
@@ -137,7 +137,7 @@ describe(AndroidSubmitCommand, () => {
it('assigns the build ID to submission', async () => {
const projectId = uuidv4();
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(fakeBuildFragment);
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([fakeBuildFragment]);
const ctx = await createSubmissionContextAsync({
platform: Platform.ANDROID,
@@ -0,0 +1,78 @@
import fs from 'fs-extra';
import { promptForAscApiKeyAsync } from '../../credentials/ios/actions/AscApiKeyUtils';
import { AscApiKeyPath, MinimalAscApiKey } from '../../credentials/ios/credentials';
import Log from '../../log';
import { isExistingFileAsync } from '../utils/files';
export enum AscApiKeySourceType {
path,
prompt,
}
interface AscApiKeySourceBase {
sourceType: AscApiKeySourceType;
}
interface AscApiKeyPromptSource extends AscApiKeySourceBase {
sourceType: AscApiKeySourceType.prompt;
}
interface AscApiKeyEnvVarSource extends AscApiKeySourceBase {
sourceType: AscApiKeySourceType.path;
path: AscApiKeyPath;
}
export type AscApiKeySource = AscApiKeyEnvVarSource | AscApiKeyPromptSource;
type AscApiKeySummary = {
source: 'local' | 'EAS servers';
path?: string;
keyId: string;
};
export type AscApiKeyResult = {
result: MinimalAscApiKey;
summary: AscApiKeySummary;
};
export async function getAscApiKeyLocallyAsync(source: AscApiKeySource): Promise<AscApiKeyResult> {
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
const { keyP8Path, keyId, issuerId } = ascApiKeyPath;
const keyP8 = await fs.readFile(keyP8Path, 'utf-8');
return {
result: { keyP8, keyId, issuerId },
summary: {
source: 'local',
path: keyP8Path,
keyId,
},
};
}
export async function getAscApiKeyPathAsync(source: AscApiKeySource): Promise<AscApiKeyPath> {
switch (source.sourceType) {
case AscApiKeySourceType.path:
return await handlePathSourceAsync(source);
case AscApiKeySourceType.prompt:
return await handlePromptSourceAsync(source);
}
}
async function handlePathSourceAsync(source: AscApiKeyEnvVarSource): Promise<AscApiKeyPath> {
const { keyP8Path } = source.path;
if (!(await isExistingFileAsync(keyP8Path))) {
Log.warn(`File ${keyP8Path} doesn't exist.`);
return await getAscApiKeyPathAsync({ sourceType: AscApiKeySourceType.prompt });
}
return source.path;
}
async function handlePromptSourceAsync(_source: AscApiKeyPromptSource): Promise<AscApiKeyPath> {
const ascApiKeyPath = await promptForAscApiKeyAsync();
return await getAscApiKeyPathAsync({
sourceType: AscApiKeySourceType.path,
path: ascApiKeyPath,
});
}
@@ -4,6 +4,7 @@ import chalk from 'chalk';
import getenv from 'getenv';
import wrapAnsi from 'wrap-ansi';
import { MissingCredentialsError } from '../../credentials/errors';
import { SubmissionFragment } from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import { promptAsync } from '../../prompts';
@@ -16,6 +17,7 @@ import {
AppSpecificPasswordSource,
AppSpecificPasswordSourceType,
} from './AppSpecificPasswordSource';
import { AscApiKeySource, AscApiKeySourceType } from './AscApiKeySource';
import IosSubmitter, { IosSubmissionOptions } from './IosSubmitter';
export default class IosSubmitCommand {
@@ -28,15 +30,37 @@ export default class IosSubmitCommand {
return await submitter.submitAsync();
}
private async resolveCredentialSubmissionOptionsAsync(): Promise<
| { appSpecificPasswordSource: Result<AppSpecificPasswordSource> }
| { ascApiKeySource: Result<AscApiKeySource> }
> {
// Fall back to app specific password if no ascApiKey defined
const ascApiKeySource = this.resolveAscApiKeySource();
const shouldUseAppSpecificPassword =
!ascApiKeySource.ok && ascApiKeySource.enforceError() instanceof MissingCredentialsError;
if (shouldUseAppSpecificPassword) {
return { appSpecificPasswordSource: this.resolveAppSpecificPasswordSource() };
} else {
return { ascApiKeySource };
}
}
private async resolveSubmissionOptionsAsync(): Promise<IosSubmissionOptions> {
const archiveSource = this.resolveArchiveSource();
const appSpecificPasswordSource = this.resolveAppSpecificPasswordSource();
const credentialsSource = await this.resolveCredentialSubmissionOptionsAsync();
const maybeAppSpecificPasswordSource =
'appSpecificPasswordSource' in credentialsSource
? credentialsSource.appSpecificPasswordSource
: null;
const maybeAscApiKeySource =
'ascApiKeySource' in credentialsSource ? credentialsSource.ascApiKeySource : null;
const ascAppIdentifier = await this.resolveAscAppIdentifierAsync();
const appleIdUsername = await this.resolveAppleIdUsernameAsync();
const errored = [
archiveSource,
appSpecificPasswordSource,
...(maybeAppSpecificPasswordSource ? [maybeAppSpecificPasswordSource] : []),
...(maybeAscApiKeySource ? [maybeAscApiKeySource] : []),
ascAppIdentifier,
appleIdUsername,
].filter(r => !r.ok);
@@ -51,7 +75,16 @@ export default class IosSubmitCommand {
appleIdUsername: appleIdUsername.enforceValue(),
ascAppIdentifier: ascAppIdentifier.enforceValue(),
archiveSource: archiveSource.enforceValue(),
appSpecificPasswordSource: appSpecificPasswordSource.enforceValue(),
...(maybeAppSpecificPasswordSource
? {
appSpecificPasswordSource: maybeAppSpecificPasswordSource.enforceValue(),
}
: null),
...(maybeAscApiKeySource
? {
ascApiKeySource: maybeAscApiKeySource.enforceValue(),
}
: null),
};
}
@@ -72,6 +105,35 @@ export default class IosSubmitCommand {
}
}
private resolveAscApiKeySource(): Result<AscApiKeySource> {
const { ascApiKeyPath, ascApiKeyIssuerId, ascApiKeyId } = this.ctx.profile;
if (ascApiKeyPath && ascApiKeyIssuerId && ascApiKeyId) {
return result({
sourceType: AscApiKeySourceType.path,
path: {
keyP8Path: ascApiKeyPath,
issuerId: ascApiKeyIssuerId,
keyId: ascApiKeyId,
},
});
}
// interpret this to mean the user had some intention of passing in ASC Api key
if (ascApiKeyPath || ascApiKeyIssuerId || ascApiKeyId) {
Log.warn(`ascApiKeyPath, ascApiKeyIssuerId and ascApiKeyId must all be defined in eas.json`);
return result({
sourceType: AscApiKeySourceType.prompt,
});
}
return result(
new MissingCredentialsError(
'Set the ascApiKeyPath, ascApiKeyIssuerId and ascApiKeyId fields in eas.json.'
)
);
}
private resolveArchiveSource(): Result<ArchiveSource> {
try {
return result(resolveArchiveSource(this.ctx, Platform.IOS));
@@ -115,6 +177,10 @@ export default class IosSubmitCommand {
return result(envAppleId);
}
if (this.ctx.credentialsCtx.appStore.authCtx?.appleId) {
return result(this.ctx.credentialsCtx.appStore.authCtx.appleId);
}
// Get the email address that was last used and set it as
// the default value for quicker authentication.
const lastAppleId = await UserSettings.getAsync('appleId', null);
+64 -10
View File
@@ -1,7 +1,9 @@
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import { IosSubmissionConfigInput, SubmissionFragment } from '../../graphql/generated';
import { SubmissionMutation } from '../../graphql/mutations/SubmissionMutation';
import formatFields from '../../utils/formatFields';
import { Archive, ArchiveSource, getArchiveAsync } from '../ArchiveSource';
import BaseSubmitter, { SubmissionInput } from '../BaseSubmitter';
import {
@@ -13,17 +15,20 @@ import {
AppSpecificPasswordSource,
getAppSpecificPasswordAsync,
} from './AppSpecificPasswordSource';
import { AscApiKeyResult, AscApiKeySource, getAscApiKeyLocallyAsync } from './AscApiKeySource';
export interface IosSubmissionOptions
extends Pick<IosSubmissionConfigInput, 'appleIdUsername' | 'ascAppIdentifier'> {
projectId: string;
archiveSource: ArchiveSource;
appSpecificPasswordSource: AppSpecificPasswordSource;
appSpecificPasswordSource?: AppSpecificPasswordSource;
ascApiKeySource?: AscApiKeySource;
}
interface ResolvedSourceOptions {
archive: Archive;
appSpecificPassword: string;
appSpecificPassword?: string;
ascApiKeyResult?: AscApiKeyResult;
}
export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmissionOptions> {
@@ -60,18 +65,22 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
private async resolveSourceOptionsAsync(): Promise<ResolvedSourceOptions> {
const archive = await getArchiveAsync(this.options.archiveSource);
const appSpecificPassword = await getAppSpecificPasswordAsync(
this.options.appSpecificPasswordSource
);
const maybeAppSpecificPassword = this.options.appSpecificPasswordSource
? await getAppSpecificPasswordAsync(this.options.appSpecificPasswordSource)
: null;
const maybeAppStoreConnectApiKey = this.options.ascApiKeySource
? await getAscApiKeyLocallyAsync(this.options.ascApiKeySource)
: null;
return {
archive,
appSpecificPassword,
...(maybeAppSpecificPassword ? { appSpecificPassword: maybeAppSpecificPassword } : null),
...(maybeAppStoreConnectApiKey ? { ascApiKeyResult: maybeAppStoreConnectApiKey } : null),
};
}
private async formatSubmissionConfigAsync(
options: IosSubmissionOptions,
{ archive, appSpecificPassword }: ResolvedSourceOptions
{ archive, appSpecificPassword, ascApiKeyResult }: ResolvedSourceOptions
): Promise<IosSubmissionConfigInput> {
const { appleIdUsername, ascAppIdentifier } = options;
return {
@@ -79,20 +88,32 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
appleIdUsername,
archiveUrl: archive.url,
appleAppSpecificPassword: appSpecificPassword,
...(ascApiKeyResult?.result
? {
ascApiKey: {
keyP8: ascApiKeyResult?.result.keyP8,
keyIdentifier: ascApiKeyResult?.result.keyId,
issuerIdentifier: ascApiKeyResult?.result.issuerId,
},
}
: null),
};
}
private prepareSummaryData(
options: IosSubmissionOptions,
{ archive }: ResolvedSourceOptions
{ archive, ascApiKeyResult }: ResolvedSourceOptions
): SummaryData {
const { appleIdUsername, ascAppIdentifier, projectId } = options;
// structuring order affects table rows order
return {
ascAppIdentifier,
appleIdUsername,
appleIdUsername: appleIdUsername ?? undefined,
projectId,
...(ascApiKeyResult
? { formattedAscApiKey: formatServiceAccountSummary(ascApiKeyResult) }
: null),
...formatArchiveSourceSummary(archive),
};
}
@@ -100,8 +121,9 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
type SummaryData = {
ascAppIdentifier: string;
appleIdUsername: string;
appleIdUsername?: string;
projectId: string;
formattedAscApiKey?: string;
} & ArchiveSourceSummaryFields;
const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
@@ -111,4 +133,36 @@ const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
archiveUrl: 'Archive URL',
archivePath: 'Archive Path',
formattedBuild: 'Build',
formattedAscApiKey: 'App Store Connect Api Key',
};
function formatServiceAccountSummary({ summary }: AscApiKeyResult): string {
const { source, path, keyId } = summary;
const fields = [
{
label: 'Key Source',
value: source,
},
{
label: 'Key Path',
value: path,
},
{
label: 'Key ID',
value: keyId,
},
];
const filteredFields = fields.filter(({ value }) => value !== undefined && value !== null) as {
label: string;
value: string;
}[];
return (
'\n' +
formatFields(filteredFields, {
labelFormat: label => ` ${chalk.dim(label)}:`,
})
);
}
@@ -0,0 +1,174 @@
import { vol } from 'memfs';
import { asMock } from '../../../__tests__/utils';
import { getCredentialsFromUserAsync } from '../../../credentials/utils/promptForCredentials';
import { promptAsync } from '../../../prompts';
import {
AscApiKeySource,
AscApiKeySourceType,
getAscApiKeyLocallyAsync,
getAscApiKeyPathAsync,
} from '../AscApiKeySource';
jest.mock('fs');
jest.mock('../../../prompts');
jest.mock('../../../credentials/utils/promptForCredentials');
beforeAll(() => {
vol.fromJSON({
'/asc-api-key.p8': 'super secret',
'/project_dir/subdir/asc-api-key.p8': 'super secret',
'/project_dir/another-asc-api-key.p8': 'super secret',
});
});
afterAll(() => {
vol.reset();
});
afterEach(() => {
asMock(promptAsync).mockClear();
});
describe(getAscApiKeyPathAsync, () => {
describe('when source is AscApiKeySourceType.path', () => {
it("prompts for path if the provided file doesn't exist", async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementationOnce(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/doesnt-exist.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
expect(promptAsync).toHaveBeenCalled();
expect(ascApiKeyPath).toEqual({
keyP8Path: '/asc-api-key.p8',
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
});
});
it("doesn't prompt for path if the provided file exists", async () => {
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
await getAscApiKeyPathAsync(source);
expect(promptAsync).not.toHaveBeenCalled();
});
it('returns the provided file path if the file exists', async () => {
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
expect(ascApiKeyPath).toEqual({
issuerId: 'test-issuer-id',
keyId: 'test-key-id',
keyP8Path: '/asc-api-key.p8',
});
});
});
describe('when source is AscApiKeySourceType.prompt', () => {
it('prompts for path', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementationOnce(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.prompt,
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
expect(promptAsync).toHaveBeenCalled();
expect(ascApiKeyPath).toEqual({
keyP8Path: '/asc-api-key.p8',
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
});
});
it('prompts for path until the user provides an existing file', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({
keyP8Path: '/doesnt-exist.p8',
}))
.mockImplementationOnce(() => ({
keyP8Path: '/blah.p8',
}))
.mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.prompt,
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
expect(promptAsync).toHaveBeenCalledTimes(3);
expect(ascApiKeyPath).toEqual({
keyP8Path: '/asc-api-key.p8',
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
});
});
});
});
describe(getAscApiKeyLocallyAsync, () => {
it('returns a local Asc Api Key file with a AscApiKeySourceType.path source', async () => {
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
const ascApiKeyResult = await getAscApiKeyLocallyAsync(source);
expect(ascApiKeyResult).toMatchObject({
result: {
keyP8: 'super secret',
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
},
summary: {
source: 'local',
path: '/asc-api-key.p8',
keyId: 'test-key-id',
},
});
});
it('returns a local Asc Api Key file with a AscApiKeySourceType.prompt source', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementationOnce(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.prompt,
};
const serviceAccountResult = await getAscApiKeyLocallyAsync(source);
expect(serviceAccountResult).toMatchObject({
result: {
keyP8: 'super secret',
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
},
summary: {
source: 'local',
path: '/asc-api-key.p8',
keyId: 'test-key-id',
},
});
});
});
+8 -7
View File
@@ -1,16 +1,17 @@
import { AppPlatform, BuildFragment, BuildStatus } from '../../graphql/generated';
import { AppPlatform, BuildFragment, BuildStatus, DistributionType } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
export async function getLatestBuildForSubmissionAsync(
export async function getRecentBuildsForSubmissionAsync(
platform: AppPlatform,
appId: string
): Promise<BuildFragment | null> {
const [build] = await BuildQuery.allForAppAsync(appId, {
limit: 1,
appId: string,
{ limit = 1 }: { limit?: number } = {}
): Promise<BuildFragment[]> {
return await BuildQuery.allForAppAsync(appId, {
limit,
filter: {
platform,
distribution: DistributionType.Store,
status: BuildStatus.Finished,
},
});
return build;
}
+1
View File
@@ -75,6 +75,7 @@ export async function loginAsync({
'expo-session': sessionSecret,
},
},
additionalTypenames: [] /* UserQuery has immutable fields */,
}
)
.toPromise();
@@ -0,0 +1,91 @@
import { Platform } from '@expo/eas-build-job';
import { errors } from '@expo/eas-json';
import { getProfilesAsync } from '../profiles';
describe(getProfilesAsync, () => {
it('defaults to production profile', async () => {
const readProfileAsync = jest.fn();
const result = await getProfilesAsync({
platforms: [Platform.ANDROID, Platform.IOS],
profileName: undefined,
readProfileAsync,
});
expect(result[0].profileName).toBe('production');
expect(result[1].profileName).toBe('production');
expect(readProfileAsync).toBeCalledWith(Platform.ANDROID, 'production');
expect(readProfileAsync).toBeCalledWith(Platform.IOS, 'production');
});
it('defaults to release profile when production profile is non-existent', async () => {
const readProfileAsync = jest
.fn()
.mockRejectedValueOnce(() => {
throw new Error();
})
.mockRejectedValueOnce(() => {
throw new Error();
});
const result = await getProfilesAsync({
platforms: [Platform.ANDROID, Platform.IOS],
profileName: undefined,
readProfileAsync,
});
expect(result[0].profileName).toBe('release');
expect(result[1].profileName).toBe('release');
expect(readProfileAsync).toBeCalledWith(Platform.ANDROID, 'production');
expect(readProfileAsync).toBeCalledWith(Platform.IOS, 'production');
expect(readProfileAsync).toBeCalledWith(Platform.ANDROID, 'release');
expect(readProfileAsync).toBeCalledWith(Platform.IOS, 'release');
});
it('fails when neither production or release profiles are present', async () => {
const readProfileAsync = jest
.fn()
.mockRejectedValueOnce(() => {
throw new Error();
})
.mockRejectedValueOnce(() => {
throw new Error();
});
await expect(
getProfilesAsync({
platforms: [Platform.ANDROID],
profileName: undefined,
readProfileAsync,
})
).rejects.toThrowError(/There is no profile named "production" in eas.json/);
});
it('gets a specific profile', async () => {
const readProfileAsync = jest.fn();
const result = await getProfilesAsync({
platforms: [Platform.ANDROID, Platform.IOS],
profileName: 'custom-profile',
readProfileAsync,
});
expect(result[0].profileName).toBe('custom-profile');
expect(result[1].profileName).toBe('custom-profile');
expect(readProfileAsync).toBeCalledWith(Platform.ANDROID, 'custom-profile');
expect(readProfileAsync).toBeCalledWith(Platform.IOS, 'custom-profile');
});
it('throws validation error if eas.json is invalid', async () => {
const readProfileAsync = jest.fn().mockImplementation(() => {
throw new errors.InvalidEasJsonError('eas.json is not valid');
});
await expect(
getProfilesAsync({
platforms: [Platform.ANDROID, Platform.IOS],
profileName: undefined,
readProfileAsync,
})
).rejects.toThrowError(/eas.json is not valid/);
});
});
Loaded 100 of 130 files, more files were not shown because too many files have changed in this diff. Show more