Compare commits

...
Author SHA1 Message Date
Quinlan Jung dc0b3b1237 [eas-cli] remove asp prompt 2021-11-05 16:15:33 -07:00
Quinlan Jung 0e6254453a [eas-cli] integrate ascApiKey with submissions workflow (#737)
* [eas-cli] integrate ascApiKey with submissions workflow

* fix failing stuff

* update CHANGELOG.md

* barthap pr feedback

* dominik pr feedback
2021-11-05 13:30:16 -07:00
Quinlan Jung 587a29892f fix changelog 2021-11-05 13:28:03 -07:00
Quinlan Jung db8245d954 [eas-cli] setup -> set up (#735)
* [eas-cli] setup -> set up

* fix stuff

* update CHANGELOG.md

* fix merge errors
2021-11-05 11:59:44 -07:00
Bartłomiej Klocek 0088306bca [eas-cli] Handle Apple servers maintenance in eas submit (#738)
* [eas-cli] Handle Apple servers maintenance in `eas submit`

* update CHANGELOG.md
2021-11-05 11:08:30 +01:00
Wojciech KozyraandDominik Sokal b6fc718713 [eas-cli] improve vcs migration experience (#732)
* [eas-cli] improve vcs migration experience

* update CHANGELOG.md

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-05 10:40:00 +01:00
Quinlan Jung 18e55dafd4 [eas-cli] setup asc api key (#733)
* [eas-cli] setup asc api key

* setup -> set up

* pr feedback

* update CHANGELOG.md
2021-11-04 14:52:25 -07:00
Bartłomiej Klocek 8e013ab793 [eas-cli] Use actor displayName when printing build info (#730)
* [eas-cli] Use actor `displayName` when printing build info

* update CHANGELOG.md
2021-11-04 07:50:03 +01:00
Quinlan JungandDominik Sokal b88e79f0f3 [eas-cli] cleanup up creds prompt method (#728)
* [eas-cli] cleanup up creds prompt method

* update CHANGELOG.md

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-03 13:19:10 -07:00
Ryan Blunden 35b505120d Replace eas:secrets with eas:secret (#727) 2021-11-03 09:44:19 +01:00
Brent VatneandDominik Sokal 6807d3080b [eas-cli] Bump @expo/apple-utils to better handle some error messages (#723)
* [eas-cli] Bump @expo/apple-utils to better handle some error messages

* Update CHANGELOG

* Update CHANGELOG.md

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-03 09:35:11 +01:00
Quinlan JungandDominik Sokal 0d1cfb3286 [eas-cli] support removal of asc api keys (#721)
* [eas-cli] support removal of asc api keys

* update CHANGELOG.md

* Update packages/eas-cli/src/credentials/ios/actions/AscApiKeyUtils.ts

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

* Update packages/eas-cli/src/credentials/ios/actions/RemoveAscApiKey.ts

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-02 21:03:34 -07:00
Quinlan Jung 0b9a5621fe fix changelog 2021-11-02 20:57:46 -07:00
Quinlan Jung 9bd15af02a [eas-cli] assign asc api keys to project (#719)
* [eas-cli] assign asc api keys to project

assign asc test

* fix compilation errors
2021-11-02 20:51:15 -07:00
Quinlan Jung aab3bf1330 [eas-cli] create + validate asc api key, infer issuer ids (#718)
* [eas-cli] create + validate asc api key, infer issuer ids

create asc tests

create asc key:

ascapikeyutils test

* update CHANGELOG.md

* some pr feedback

* API -> Api

* more pr feedbakc

* fix tsc
2021-11-02 20:41:17 -07:00
Expo CI 9e95218523 update CHANGELOG.md 2021-11-02 17:27:32 +00:00
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
167 changed files with 4516 additions and 3572 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 }}
+86
View File
@@ -8,6 +8,92 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Add ASC Api Key generation workflow. ([#718](https://github.com/expo/eas-cli/pull/718) by [@quinlanj](https://github.com/quinlanj))
- Add support for removal of ASC Api Keys. ([#721](https://github.com/expo/eas-cli/pull/721) by [@quinlanj](https://github.com/quinlanj))
- Allow users to assign an ASC Api Key to their project. ([#719](https://github.com/expo/eas-cli/pull/719) by [@quinlanj](https://github.com/quinlanj))
- Add setup support for ASC Api Keys. ([#733](https://github.com/expo/eas-cli/pull/733) by [@quinlanj](https://github.com/quinlanj))
- Show initiating user display name when selecting a build to submit. ([#730](https://github.com/expo/eas-cli/pull/730) by [@barthap](https://github.com/barthap))
- Handle Apple servers maintenance error in `eas submit`. ([#738](https://github.com/expo/eas-cli/pull/738) by [@barthap](https://github.com/barthap))
- Integrate ASC Api Key with submissions workflow. ([#737](https://github.com/expo/eas-cli/pull/737) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
### 🧹 Chores
- Clean up credentials prompt method. ([#728](https://github.com/expo/eas-cli/pull/728) by [@quinlanj](https://github.com/quinlanj))
- Bump @expo/apple-utils to 0.0.0-alpha.26. ([#723](https://github.com/expo/eas-cli/pull/723] by [@brentvatne](https://github.com/brentvatne))
- Grammar: replace setup with set up. ([#735](https://github.com/expo/eas-cli/pull/735) by [@quinlanj](https://github.com/quinlanj))
- Improve VCS workflow migration experience. ([#732](https://github.com/expo/eas-cli/pull/732) by [@wkozyra95](https://github.com/wkozyra95))
## [0.34.1](https://github.com/expo/eas-cli/releases/tag/0.34.1) - 2021-11-02
### 🐛 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))
+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.32.0"
"version": "0.34.1"
}
+2 -1
View File
@@ -10,7 +10,8 @@
"eas": "packages/eas-cli/bin/run",
"lint": "eslint . --ext .ts --ext .js",
"release": "lerna version",
"test": "jest"
"test": "jest",
"clean": "lerna run clean && rm -rf node_modules coverage"
},
"devDependencies": {
"@expo/oclif-dev-cli": "1.24.0-expo",
+32 -30
View File
@@ -73,7 +73,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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`
@@ -363,10 +364,10 @@ OPTIONS
--id=id ID of the secret to delete
DESCRIPTION
Unsure where to find the secret's ID? Run eas secrets:list
Unsure where to find the secret's ID? Run eas secret:list
```
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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.32.0/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 -->
+848 -1165
View File
File diff suppressed because it is too large. Load diff
+9 -9
View File
@@ -1,23 +1,23 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.32.0",
"version": "0.34.1",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"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.32.0",
"@expo/apple-utils": "0.0.0-alpha.26",
"@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",
@@ -113,7 +113,6 @@
"cli"
],
"license": "MIT",
"main": "build/index.js",
"oclif": {
"bin": "eas",
"commands": "./build/commands",
@@ -177,6 +176,7 @@
"typecheck": "tsc",
"test": "jest",
"version": "yarn oclif-dev readme && node patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml"
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
"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,
});
}
}
@@ -120,11 +120,5 @@ export function formatGraphQLBuild(build: BuildFragment): string {
}
const getActorName = (build: BuildFragment): string => {
if (!build.initiatingActor) {
return 'unknown';
} else if (build.initiatingActor.__typename === 'User') {
return build.initiatingActor.username;
} else {
return build.initiatingActor.firstName ?? 'unknown';
}
return build.initiatingActor?.displayName || 'unknown';
};
+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()
);
+112 -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';
@@ -17,7 +14,7 @@ import { BuildContext, createBuildContextAsync } from '../../build/context';
import { prepareIosBuildAsync } from '../../build/ios/build';
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from '../../build/utils/devClient';
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import { ensureRepoIsCleanAsync, reviewAndCommitChangesAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { CredentialsContext } from '../../credentials/context';
import {
@@ -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,62 @@ 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 });
}
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
if (!(await fs.pathExists(easJsonPath))) {
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 (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' },
]);
if (mode === 'requireCommit') {
setVcsClient(new GitClient());
await ensureRepoIsCleanAsync(nonInteractive);
}
rawEasJson.cli =
mode === 'requireCommit'
? { version: `>= ${easCliVersion}`, requireCommit: true }
: { version: `>= ${easCliVersion}` };
await fs.writeJSON(easJsonPath, rawEasJson, { spaces: 2 });
Log.withTick('Updated eas.json');
if (mode === 'requireCommit') {
await getVcsClient().trackFileAsync(easJsonPath);
await reviewAndCommitChangesAsync('Set cli.requireCommit to true in eas.json', {
nonInteractive,
});
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 });
}
}
}
@@ -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()
);
@@ -23,7 +23,7 @@ import { promptAsync, toggleConfirmAsync } from '../../prompts';
export default class EnvironmentSecretDelete extends EasCommand {
static description = `Delete an environment secret by ID.
Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
Unsure where to find the secret's ID? Run ${chalk.bold('eas secret:list')}`;
static flags = {
id: flags.string({
+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()
);
@@ -4,7 +4,7 @@ import { AuthCtx } from '../ios/appstore/authenticate';
export const testAuthCtx: AuthCtx = {
appleId: 'test-apple-id',
appleIdPassword: 'test-apple-password',
team: { id: 'test-team-id', name: 'test-team-name', inHouse: false },
team: { id: 'test-apple-team-identifier', name: 'test-team-name', inHouse: false },
};
export function getAppstoreMock(): AppStoreApi {
@@ -22,5 +22,9 @@ export function getAppstoreMock(): AppStoreApi {
createProvisioningProfileAsync: jest.fn(),
revokeProvisioningProfileAsync: jest.fn(),
createOrReuseAdhocProvisioningProfileAsync: jest.fn(),
createAscApiKeyAsync: jest.fn(),
getAscApiKeyAsync: jest.fn(),
revokeAscApiKeyAsync: jest.fn(),
listAscApiKeysAsync: jest.fn(),
} as any;
}
@@ -1,5 +1,9 @@
import { UserRole } from '@expo/apple-utils';
import {
AppFragment,
AppStoreConnectApiKeyFragment,
AppStoreConnectUserRole,
AppleAppIdentifierFragment,
AppleDistributionCertificateFragment,
AppleProvisioningProfileFragment,
@@ -10,12 +14,27 @@ import {
IosDistributionType,
} from '../../graphql/generated';
import * as IosGraphqlClient from '../ios/api/GraphqlClient';
import { DistributionCertificate, ProvisioningProfile } from '../ios/appstore/Credentials.types';
import {
AscApiKey,
DistributionCertificate,
ProvisioningProfile,
} from '../ios/appstore/Credentials.types';
import { Target } from '../ios/types';
import { testProvisioningProfileBase64 } from './fixtures-base64-data';
const now = new Date();
export const testAscApiKey: AscApiKey = {
keyId: 'test-keyIdentifier',
issuerId: 'test-issuer-id-from-apple',
teamId: 'test-team-id',
teamName: 'test-team-name',
name: 'test-name',
roles: [UserRole.ADMIN],
isRevoked: false,
keyP8: 'test-key-p8',
};
export const testProvisioningProfile: ProvisioningProfile = {
provisioningProfileId: 'test-id',
provisioningProfile: testProvisioningProfileBase64,
@@ -38,6 +57,17 @@ export const testAppFragment: AppFragment = {
slug: 'testapp',
};
export const testAscApiKeyFragment: AppStoreConnectApiKeyFragment = {
id: 'test-asc-api-key-id',
appleTeam: { ...testAppleTeamFragment },
issuerIdentifier: 'test-issuerIdentifier',
keyIdentifier: 'test-keyIdentifier',
name: 'test-name',
roles: [AppStoreConnectUserRole.Admin],
updatedAt: now,
createdAt: now,
};
export const testPushKey: ApplePushKeyFragment = {
id: 'test-push-key-id',
keyIdentifier: 'test-key-identifier',
@@ -112,6 +142,7 @@ export const testCommonIosAppCredentialsFragment: CommonIosAppCredentialsFragmen
appleAppIdentifier: testAppleAppIdentifierFragment,
pushKey: testPushKey,
iosAppBuildCredentialsList: [testIosAppBuildCredentialsFragment],
appStoreConnectApiKeyForSubmissions: testAscApiKeyFragment,
};
export const testAppleTeam = {
@@ -139,6 +170,9 @@ export function getNewIosApiMock(): { [key in keyof typeof IosGraphqlClient]?: a
createPushKeyAsync: jest.fn(),
getPushKeyForAppAsync: jest.fn(),
deletePushKeyAsync: jest.fn(),
createAscApiKeyAsync: jest.fn(),
deleteAscApiKeyAsync: jest.fn(),
getAscApiKeysForAccountAsync: jest.fn(() => []),
};
}
@@ -5,7 +5,7 @@ import nullthrows from 'nullthrows';
import { AndroidAppBuildCredentialsFragment } from '../../graphql/generated';
import { CredentialsContext } from '../context';
import * as credentialsJsonReader from '../credentialsJson/read';
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
import { SetUpBuildCredentials } from './actions/SetUpBuildCredentials';
import { AppLookupParams } from './api/GraphqlClient';
import { Keystore } from './credentials';
@@ -34,7 +34,7 @@ export default class AndroidCredentialsProvider {
}
private async getRemoteAsync(): Promise<AndroidCredentials> {
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: this.options.app });
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: this.options.app });
const buildCredentials = await setupBuildCredentialsAction.runAsync(this.ctx);
return this.toAndroidCredentials(buildCredentials);
}
@@ -25,7 +25,7 @@ interface Options {
* Sets up Build Credentials for Android
* @name: sets up build credentials for the specified configuration. If no name is specified, the default configuration is setup
*/
export class SetupBuildCredentials {
export class SetUpBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: CredentialsContext): Promise<AndroidAppBuildCredentialsFragment> {
@@ -14,7 +14,7 @@ import { AppLookupParams } from '../api/GraphqlClient';
import { getKeystoreWithType } from '../utils/keystoreNew';
import { BackupKeystore } from './DownloadKeystore';
export class SetupBuildCredentialsFromCredentialsJson {
export class SetUpBuildCredentialsFromCredentialsJson {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: CredentialsContext): Promise<AndroidAppBuildCredentialsFragment | null> {
@@ -13,7 +13,7 @@ import { AssignGoogleServiceAccountKey } from './AssignGoogleServiceAccountKey';
import { CreateGoogleServiceAccountKey } from './CreateGoogleServiceAccountKey';
import { UseExistingGoogleServiceAccountKey } from './UseExistingGoogleServiceAccountKey';
export class SetupGoogleServiceAccountKey {
export class SetUpGoogleServiceAccountKey {
constructor(private app: AppLookupParams) {}
public async runAsync(ctx: CredentialsContext): Promise<CommonAndroidAppCredentialsFragment> {
@@ -9,7 +9,7 @@ import { createCtxMock } from '../../../__tests__/fixtures-context';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { generateRandomKeystoreAsync } from '../../utils/keystore';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetupBuildCredentials } from '../SetupBuildCredentials';
import { SetUpBuildCredentials } from '../SetUpBuildCredentials';
jest.mock('../../../../ora');
jest.mock('../../../../project/ensureProjectExists');
@@ -17,7 +17,7 @@ jest.mock('../../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() =
jest.mock('../../../../prompts', () => ({ confirmAsync: jest.fn(() => true) }));
jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn() }));
describe('SetupBuildCredentials', () => {
describe('SetUpBuildCredentials', () => {
beforeEach(() => {
asMock(generateRandomKeystoreAsync).mockReset();
});
@@ -33,7 +33,7 @@ describe('SetupBuildCredentials', () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
await setupBuildCredentialsAction.runAsync(ctx);
// expect keystore not to be created
@@ -48,7 +48,7 @@ describe('SetupBuildCredentials', () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
await setupBuildCredentialsAction.runAsync(ctx);
// expect keystore to be created
@@ -63,7 +63,7 @@ describe('SetupBuildCredentials', () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
);
@@ -14,7 +14,7 @@ import {
SelectAndroidBuildCredentialsResultType,
} from '../../../manager/SelectAndroidBuildCredentials';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetupBuildCredentialsFromCredentialsJson } from '../SetupBuildCredentialsFromCredentialsJson';
import { SetUpBuildCredentialsFromCredentialsJson } from '../SetUpBuildCredentialsFromCredentialsJson';
jest.mock('fs');
@@ -26,7 +26,7 @@ beforeEach(() => {
vol.reset();
});
describe(SetupBuildCredentialsFromCredentialsJson, () => {
describe(SetUpBuildCredentialsFromCredentialsJson, () => {
it('sets up a new build configuration from credentials.json upon user request', async () => {
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
return {
@@ -59,7 +59,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
'keystore.jks': 'some-binary-content',
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
appLookupParams
);
await setupBuildCredentialsAction.runAsync(ctx);
@@ -109,7 +109,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
'keystore.jks': 'some-binary-content',
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
appLookupParams
);
await setupBuildCredentialsAction.runAsync(ctx);
@@ -132,7 +132,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
nonInteractive: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
appLookupParams
);
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError();
@@ -10,7 +10,7 @@ import {
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetupGoogleServiceAccountKey } from '../SetupGoogleServiceAccountKey';
import { SetUpGoogleServiceAccountKey } from '../SetUpGoogleServiceAccountKey';
jest.mock('../../../../prompts');
jest.mock('fs');
@@ -22,7 +22,7 @@ beforeEach(() => {
vol.reset();
});
describe(SetupGoogleServiceAccountKey, () => {
describe(SetUpGoogleServiceAccountKey, () => {
it('skips setup when there is a Google Service Account Key already assigned to the project', async () => {
const ctx = createCtxMock({
nonInteractive: false,
@@ -34,7 +34,7 @@ describe(SetupGoogleServiceAccountKey, () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
await setupGoogleServiceAccountKeyAction.runAsync(ctx);
expect(ctx.android.createGoogleServiceAccountKeyAsync).not.toHaveBeenCalled();
@@ -57,7 +57,7 @@ describe(SetupGoogleServiceAccountKey, () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
await setupGoogleServiceAccountKeyAction.runAsync(ctx);
expect(ctx.android.createGoogleServiceAccountKeyAsync).toHaveBeenCalledTimes(1);
@@ -68,7 +68,7 @@ describe(SetupGoogleServiceAccountKey, () => {
nonInteractive: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
await expect(setupGoogleServiceAccountKeyAction.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
);
@@ -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;
}
@@ -11,3 +11,9 @@ export class MissingCredentialsError extends Error {
super(message ?? 'Credentials are not set up.');
}
}
export class UnsupportedCredentialsChoiceError extends Error {
constructor(message: string, public choice: string) {
super(message);
}
}
@@ -9,8 +9,8 @@ import { CredentialsContext } from '../context';
import * as credentialsJsonReader from '../credentialsJson/read';
import { ensureAllTargetsAreConfigured } from '../credentialsJson/utils';
import { getAppFromContext } from './actions/BuildCredentialsUtils';
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
import { SetupPushKey } from './actions/SetupPushKey';
import { SetUpBuildCredentials } from './actions/SetUpBuildCredentials';
import { SetUpPushKey } from './actions/SetUpPushKey';
import { IosCapabilitiesOptions } from './appstore/ensureAppExists';
import { App, IosCredentials, Target } from './types';
import { isAdHocProfile } from './utils/provisioningProfile';
@@ -58,7 +58,7 @@ export default class IosCredentialsProvider {
}
private async getRemoteAsync(): Promise<IosCredentials> {
return await new SetupBuildCredentials({
return await new SetUpBuildCredentials({
app: this.options.app,
targets: this.options.targets,
distribution: this.options.distribution,
@@ -83,7 +83,7 @@ export default class IosCredentialsProvider {
parentBundleIdentifier: applicationTarget.parentBundleIdentifier,
};
const setupPushKeyAction = new SetupPushKey(appLookupParams);
const setupPushKeyAction = new SetUpPushKey(appLookupParams);
const isPushKeySetup = await setupPushKeyAction.isPushKeySetupAsync(ctx);
if (isPushKeySetup) {
Log.succeed(
@@ -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;
@@ -1,15 +1,122 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import { nanoid } from 'nanoid';
import path from 'path';
import * as uuid from 'uuid';
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
import Log, { learnMore } from '../../../log';
import { promptAsync } from '../../../prompts';
import { getCredentialsFromUserAsync } from '../../utils/promptForCredentials';
import { confirmAsync, promptAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { fromNow } from '../../../utils/date';
import { CredentialsContext } from '../../context';
import {
getCredentialsFromUserAsync,
shouldAutoGenerateCredentialsAsync,
} from '../../utils/promptForCredentials';
import { AscApiKey } from '../appstore/Credentials.types';
import { AscApiKeyPath, MinimalAscApiKey, ascApiKeyMetadataSchema } from '../credentials';
import {
AscApiKeyPath,
MinimalAscApiKey,
ascApiKeyIdSchema,
ascApiKeyIssuerIdSchema,
} from '../credentials';
import { isAscApiKeyValidAndTrackedAsync } from '../validators/validateAscApiKey';
import { formatAppleTeam } from './AppleTeamUtils';
export async function promptForAscApiKeyAsync(): Promise<AscApiKeyPath> {
export enum AppStoreApiKeyPurpose {
SUBMISSION_SERVICE = 'EAS Submit',
}
export async function promptForAscApiKeyPathAsync(ctx: CredentialsContext): Promise<AscApiKeyPath> {
const { keyId, keyP8Path } = await promptForKeyP8AndIdAsync();
const bestEffortIssuerId = await getBestEffortIssuerIdAsync(ctx, keyId);
if (bestEffortIssuerId) {
Log.log(`Detected Issuer ID: ${bestEffortIssuerId}`);
return { keyId, issuerId: bestEffortIssuerId, keyP8Path };
}
const issuerId = await promptForIssuerIdAsync();
return { keyId, issuerId, 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'
)}`
);
// Do not perform uuid validation - Apple's issuerIds are not RFC4122 compliant
const { issuerId } = await getCredentialsFromUserAsync(ascApiKeyIssuerIdSchema);
return issuerId;
}
export async function getMinimalAscApiKeyAsync(ascApiKey: AscApiKey): Promise<MinimalAscApiKey> {
return {
...ascApiKey,
issuerId: ascApiKey.issuerId ?? (await promptForIssuerIdAsync()),
};
}
export async function provideOrGenerateAscApiKeyAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<MinimalAscApiKey> {
if (ctx.nonInteractive) {
return await generateAscApiKeyAsync(ctx, purpose);
}
const userProvided = await promptForAscApiKeyAsync(ctx);
if (!userProvided) {
return await generateAscApiKeyAsync(ctx, purpose);
}
if (!ctx.appStore.authCtx) {
Log.warn('Unable to validate App Store Connect Api Key, you are not authenticated with Apple.');
return userProvided;
}
const isValidAndTracked = await isAscApiKeyValidAndTrackedAsync(ctx, userProvided);
if (isValidAndTracked) {
return userProvided;
}
const useUserProvided = await confirmAsync({
message: `App Store Connect Api Key with ID ${userProvided.keyId} is not valid on Apple's servers. Proceed anyway?`,
});
if (useUserProvided) {
return userProvided;
}
return await provideOrGenerateAscApiKeyAsync(ctx, purpose);
}
async function generateAscApiKeyAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<MinimalAscApiKey> {
await ctx.appStore.ensureAuthenticatedAsync();
const ascApiKey = await ctx.appStore.createAscApiKeyAsync({
nickname: getAscApiKeyName(purpose),
});
return await getMinimalAscApiKeyAsync(ascApiKey);
}
export function getAscApiKeyName(purpose: AppStoreApiKeyPurpose): string {
const nameParts = ['[Expo]', purpose, nanoid(10)];
return nameParts.join(' ');
}
async function promptForAscApiKeyAsync(ctx: CredentialsContext): Promise<MinimalAscApiKey | null> {
const shouldAutoGenerateCredentials = await shouldAutoGenerateCredentialsAsync(ascApiKeyIdSchema);
if (shouldAutoGenerateCredentials) {
return null;
}
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
const { keyP8Path, keyId, issuerId } = ascApiKeyPath;
return { keyP8: await fs.readFile(keyP8Path, 'utf-8'), keyId, issuerId };
}
async function promptForKeyP8AndIdAsync(): Promise<Pick<AscApiKeyPath, 'keyP8Path' | 'keyId'>> {
Log.log(
chalk.bold('An App Store Connect Api key is required to upload your app to the Apple App Store')
);
@@ -39,33 +146,108 @@ export async function promptForAscApiKeyAsync(): Promise<AscApiKeyPath> {
});
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, {
const { keyId } = await getCredentialsFromUserAsync(ascApiKeyIdSchema, {
keyId: bestEffortKeyId,
});
return { ...ascApiKeyMetadata, keyP8Path };
return { keyId, 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'
)}`
);
async function getBestEffortIssuerIdAsync(
ctx: CredentialsContext,
ascApiKeyId: string
): Promise<string | null> {
if (!ctx.appStore.authCtx) {
return null;
}
const ascApiKeyInfo = await ctx.appStore.getAscApiKeyAsync(ascApiKeyId);
return ascApiKeyInfo?.issuerId ?? null;
}
const { issuerId } = await promptAsync({
type: 'text',
name: 'issuerId',
message: 'App Store Connect Issuer ID:',
validate: (input: string) => uuid.validate(input),
export async function getAscApiKeysFromAccountAsync(
ctx: CredentialsContext,
account: Account,
{ filterDifferentAppleTeam }: { filterDifferentAppleTeam?: boolean } = {}
): Promise<AppStoreConnectApiKeyFragment[]> {
const ascApiKeysForAccount = await ctx.ios.getAscApiKeysForAccountAsync(account);
if (!filterDifferentAppleTeam) {
return ascApiKeysForAccount;
} else {
return filterKeysFromDifferentAppleTeam(ctx, ascApiKeysForAccount);
}
}
export async function selectAscApiKeysFromAccountAsync(
ctx: CredentialsContext,
account: Account,
{ filterDifferentAppleTeam }: { filterDifferentAppleTeam?: boolean } = {}
): Promise<AppStoreConnectApiKeyFragment | null> {
const ascApiKeysForAccount = await getAscApiKeysFromAccountAsync(ctx, account, {
filterDifferentAppleTeam,
});
return issuerId;
if (ascApiKeysForAccount.length === 0) {
if (filterDifferentAppleTeam) {
const maybeAppleTeamId = ctx.appStore.authCtx?.team.id;
Log.warn(
`There are no App Store Connect Api Keys in your EAS account${
maybeAppleTeamId ? ` matching Apple Team ID: ${maybeAppleTeamId}.` : '.'
}`
);
} else {
Log.warn(`There are no App Store Connect Api Keys available in your EAS account.`);
}
return null;
}
return selectAscApiKeysAsync(ascApiKeysForAccount);
}
export async function getMinimalAscApiKeyAsync(ascApiKey: AscApiKey): Promise<MinimalAscApiKey> {
return {
...ascApiKey,
issuerId: ascApiKey.issuerId ?? (await promptForIssuerIdAsync()),
};
async function selectAscApiKeysAsync(
ascApiKeys: AppStoreConnectApiKeyFragment[]
): Promise<AppStoreConnectApiKeyFragment | null> {
const sortedAscApiKeys = sortAscApiKeysByUpdatedAtDesc(ascApiKeys);
const { chosenAscApiKey } = await promptAsync({
type: 'select',
name: 'chosenAscApiKey',
message: 'Select an Api Key from the list:',
choices: sortedAscApiKeys.map(ascApiKey => ({
title: formatAscApiKey(ascApiKey),
value: ascApiKey,
})),
});
return chosenAscApiKey;
}
function filterKeysFromDifferentAppleTeam(
ctx: CredentialsContext,
keys: AppStoreConnectApiKeyFragment[]
): AppStoreConnectApiKeyFragment[] {
if (!ctx.appStore.authCtx) {
return keys;
}
const teamId = ctx.appStore.authCtx.team.id;
return keys.filter(key => !key.appleTeam || key.appleTeam?.appleTeamIdentifier === teamId);
}
export function sortAscApiKeysByUpdatedAtDesc(
keys: AppStoreConnectApiKeyFragment[]
): AppStoreConnectApiKeyFragment[] {
return keys.sort(
(keyA, keyB) => new Date(keyB.updatedAt).getTime() - new Date(keyA.updatedAt).getTime()
);
}
export function formatAscApiKey(ascApiKey: AppStoreConnectApiKeyFragment): string {
const { keyIdentifier, appleTeam, name, updatedAt } = ascApiKey;
let line: string = '';
line += `Key ID: ${keyIdentifier}`;
if (name) {
line += chalk.gray(`\n Name: ${name}`);
}
if (appleTeam) {
line += chalk.gray(`\n ${formatAppleTeam(appleTeam)}`);
}
line += chalk.gray(`\n Updated: ${fromNow(new Date(updatedAt))} ago`);
return line;
}
@@ -0,0 +1,38 @@
import {
AppStoreConnectApiKeyFragment,
CommonIosAppCredentialsFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { AppStoreApiKeyPurpose } from './AscApiKeyUtils';
export class AssignAscApiKey {
constructor(private app: AppLookupParams) {}
public async runAsync(
ctx: CredentialsContext,
ascApiKey: AppStoreConnectApiKeyFragment,
purpose: AppStoreApiKeyPurpose
): Promise<CommonIosAppCredentialsFragment> {
const appleTeam =
(await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app)) ?? ascApiKey.appleTeam ?? null;
const appCredentials = await ctx.ios.createOrGetIosAppCredentialsWithCommonFieldsAsync(
this.app,
{ appleTeam: appleTeam ?? undefined }
);
let updatedAppCredentials;
if (purpose === AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
updatedAppCredentials = await ctx.ios.updateIosAppCredentialsAsync(appCredentials, {
ascApiKeyIdForSubmissions: ascApiKey.id,
});
} else {
throw new Error(`${purpose} is not yet supported.`);
}
Log.succeed(
`App Store Connect Api Key assigned to ${this.app.projectName}: ${this.app.bundleIdentifier} for ${purpose}.`
);
return updatedAppCredentials;
}
}
@@ -0,0 +1,23 @@
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { Account } from '../../../user/Account';
import { CredentialsContext } from '../../context';
import { AppStoreApiKeyPurpose, provideOrGenerateAscApiKeyAsync } from './AscApiKeyUtils';
export class CreateAscApiKey {
constructor(private account: Account) {}
public async runAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<AppStoreConnectApiKeyFragment> {
if (ctx.nonInteractive) {
throw new Error(`A new App Store Connect Api Key cannot be created in non-interactive mode.`);
}
const ascApiKey = await provideOrGenerateAscApiKeyAsync(ctx, purpose);
const result = await ctx.ios.createAscApiKeyAsync(this.account, ascApiKey);
Log.succeed('Created App Store Connect Api Key');
return result;
}
}
@@ -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,15 +152,14 @@ 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);
if (userProvided) {
if (!ctx.appStore.authCtx) {
Log.warn(
'Unable to validate distribution certificate due to insufficient Apple Credentials'
'Unable to validate distribution certificate, you are not authenticated with Apple.'
);
return userProvided;
} else {
@@ -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,15 +14,12 @@ 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) {
if (!ctx.appStore.authCtx) {
Log.warn('Unable to validate push key due to insufficient Apple Credentials');
Log.warn('Unable to validate push key, you are not authenticated with Apple.');
return userProvided;
} else {
const isValidAndTracked = await isPushKeyValidAndTrackedAsync(ctx, 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 {
@@ -0,0 +1,53 @@
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { CredentialsContext } from '../../context';
import { selectAscApiKeysFromAccountAsync } from './AscApiKeyUtils';
export class SelectAndRemoveAscApiKey {
constructor(private account: Account) {}
async runAsync(ctx: CredentialsContext): Promise<void> {
const selected = await selectAscApiKeysFromAccountAsync(ctx, this.account);
if (selected) {
await new RemoveAscApiKey(this.account, selected).runAsync(ctx);
Log.succeed('Removed App Store Connect Api Key');
Log.newLine();
}
}
}
export class RemoveAscApiKey {
constructor(private account: Account, private ascApiKey: AppStoreConnectApiKeyFragment) {}
public async runAsync(ctx: CredentialsContext): Promise<void> {
if (ctx.nonInteractive) {
throw new Error(`Cannot remove App Store Connect Api Keys in non-interactive mode.`);
}
// TODO(quin): add an extra edge on AppStoreConnectApiKey to find the apps using it
const confirm = await confirmAsync({
message: `Deleting this Api Key may affect your projects that rely on it. Do you want to continue?`,
});
if (!confirm) {
Log.log('Aborting');
return;
}
Log.log('Removing Api Key');
await ctx.ios.deleteAscApiKeyAsync(this.ascApiKey.id);
let shouldRevoke = false;
if (!ctx.nonInteractive) {
shouldRevoke = await confirmAsync({
message: `Do you also want to revoke this Api Key on the Apple Developer Portal?`,
});
} else if (ctx.nonInteractive) {
Log.log('Skipping Api Key revocation on the Apple Developer Portal.');
}
if (shouldRevoke) {
await ctx.appStore.revokeAscApiKeyAsync(this.ascApiKey.keyIdentifier);
}
}
}
@@ -21,7 +21,7 @@ import { validateProvisioningProfileAsync } from '../validators/validateProvisio
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { assignBuildCredentialsAsync, getBuildCredentialsAsync } from './BuildCredentialsUtils';
import { chooseDevicesAsync, formatDeviceLabel } from './DeviceUtils';
import { SetupDistributionCertificate } from './SetupDistributionCertificate';
import { SetUpDistributionCertificate } from './SetUpDistributionCertificate';
enum ReuseAction {
Yes,
@@ -29,11 +29,11 @@ enum ReuseAction {
No,
}
export class SetupAdhocProvisioningProfile {
export class SetUpAdhocProvisioningProfile {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
const distCert = await new SetupDistributionCertificate(
const distCert = await new SetUpDistributionCertificate(
this.app,
IosDistributionType.AdHoc
).runAsync(ctx);
@@ -0,0 +1,148 @@
import assert from 'assert';
import nullthrows from 'nullthrows';
import {
AppStoreConnectApiKeyFragment,
CommonIosAppCredentialsFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync, promptAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import {
MissingCredentialsNonInteractiveError,
UnsupportedCredentialsChoiceError,
} from '../../errors';
import { AppLookupParams } from '../api/GraphqlClient';
import { getValidAndTrackedAscApiKeysAsync } from '../validators/validateAscApiKey';
import {
AppStoreApiKeyPurpose,
formatAscApiKey,
getAscApiKeysFromAccountAsync,
selectAscApiKeysFromAccountAsync,
sortAscApiKeysByUpdatedAtDesc,
} from './AscApiKeyUtils';
import { AssignAscApiKey } from './AssignAscApiKey';
import { CreateAscApiKey } from './CreateAscApiKey';
export enum SetupAscApiKeyChoice {
GENERATE = 'GENERATE',
USE_EXISTING = 'USE_EXISTING',
}
export class SetUpAscApiKey {
public choices: { title: string; value: string }[] = [
{
title: '[Choose an existing key]',
value: SetupAscApiKeyChoice.USE_EXISTING,
},
{ title: '[Add a new key]', value: SetupAscApiKeyChoice.GENERATE },
];
constructor(private app: AppLookupParams, private purpose: AppStoreApiKeyPurpose) {}
public async runAsync(ctx: CredentialsContext): Promise<CommonIosAppCredentialsFragment> {
const isKeySetup = await this.isAscApiKeySetupAsync(ctx, this.purpose);
if (isKeySetup) {
Log.succeed('App Store Connect Api Key already set up.');
return nullthrows(
await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(this.app),
'iosAppCredentials cannot be null if App Store Connect Api Key is already set up'
);
}
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'App Store Connect Api Keys cannot be set up in --non-interactive mode.'
);
}
const keysForAccount = await getAscApiKeysFromAccountAsync(ctx, this.app.account, {
filterDifferentAppleTeam: true,
});
const maybeAutoselectedKey = await this.doBestEffortAutoselectAsync(ctx, keysForAccount);
if (maybeAutoselectedKey) {
return await new AssignAscApiKey(this.app).runAsync(ctx, maybeAutoselectedKey, this.purpose);
}
const availableChoices =
keysForAccount.length === 0
? this.choices.filter(choice => choice.value !== SetupAscApiKeyChoice.USE_EXISTING)
: this.choices;
const ascApiKey = await this.processChoicesAsync(ctx, this.purpose, availableChoices);
return await new AssignAscApiKey(this.app).runAsync(ctx, ascApiKey, this.purpose);
}
private async doBestEffortAutoselectAsync(
ctx: CredentialsContext,
keysForAccount: AppStoreConnectApiKeyFragment[]
): Promise<AppStoreConnectApiKeyFragment | null> {
if (!ctx.appStore.authCtx) {
return null;
}
if (keysForAccount.length === 0) {
return null;
}
// only provide autoselect if we can find a key that is certainly valid
const validKeys = await getValidAndTrackedAscApiKeysAsync(ctx, keysForAccount);
if (validKeys.length === 0) {
return null;
}
const [autoselectedKey] = sortAscApiKeysByUpdatedAtDesc(validKeys);
const useAutoselected = await confirmAsync({
message: `Reuse this App Store Connect Api Key?\n${formatAscApiKey(autoselectedKey)}`,
});
if (useAutoselected) {
Log.log(`Using App Store Connect Api Key with ID ${autoselectedKey.keyIdentifier}`);
return autoselectedKey;
} else {
return null;
}
}
private async isAscApiKeySetupAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<boolean> {
const appCredentials = await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(this.app);
if (purpose !== AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
throw new Error(`App Store Connect Api Key setup is not yet supported for ${purpose}.`);
}
return !!appCredentials?.appStoreConnectApiKeyForSubmissions;
}
private async processChoicesAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose,
choices: { title: string; value: string }[]
): Promise<AppStoreConnectApiKeyFragment> {
assert(choices.length > 0, 'SetupAscApiKey: There must be at least one choice');
let choice: string;
if (choices.length === 1) {
choice = choices[0].value;
} else {
const result = await promptAsync({
type: 'select',
name: 'choice',
message: 'Select the App Store Connect Api Key to use for your project:',
choices,
});
choice = result.choice;
}
if (choice === SetupAscApiKeyChoice.GENERATE) {
return await new CreateAscApiKey(this.app.account).runAsync(ctx, purpose);
} else if (choice === SetupAscApiKeyChoice.USE_EXISTING) {
const selectedAscApiKey = await selectAscApiKeysFromAccountAsync(ctx, this.app.account, {
filterDifferentAppleTeam: true,
});
if (!selectedAscApiKey) {
return await this.processChoicesAsync(ctx, purpose, choices);
}
return selectedAscApiKey;
}
throw new UnsupportedCredentialsChoiceError(
`AscApiKey Setup does not support choice:${choice}`,
choice
);
}
}
@@ -7,7 +7,7 @@ import { CredentialsContext } from '../../context';
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
import { App, IosAppBuildCredentialsMap, IosCredentials, Target } from '../types';
import { displayProjectCredentials } from '../utils/printCredentials';
import { SetupTargetBuildCredentials } from './SetupTargetBuildCredentials';
import { SetUpTargetBuildCredentials } from './SetUpTargetBuildCredentials';
interface Options {
app: App;
@@ -17,7 +17,7 @@ interface Options {
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
export class SetupBuildCredentials {
export class SetUpBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: CredentialsContext): Promise<IosCredentials> {
@@ -49,7 +49,7 @@ export class SetupBuildCredentials {
} else {
Log.newLine();
}
const action = new SetupTargetBuildCredentials({
const action = new SetUpTargetBuildCredentials({
...this.options,
app: {
...this.options.app,
@@ -8,9 +8,9 @@ import { readIosCredentialsAsync } from '../../credentialsJson/read';
import { IosCredentials } from '../../credentialsJson/types';
import { ensureAllTargetsAreConfigured } from '../../credentialsJson/utils';
import { App, Target } from '../types';
import { SetupTargetBuildCredentialsFromCredentialsJson } from './SetupTargetBuildCredentialsFromCredentialsJson';
import { SetUpTargetBuildCredentialsFromCredentialsJson } from './SetUpTargetBuildCredentialsFromCredentialsJson';
export class SetupBuildCredentialsFromCredentialsJson {
export class SetUpBuildCredentialsFromCredentialsJson {
constructor(
private app: App,
private targets: Target[],
@@ -32,7 +32,7 @@ export class SetupBuildCredentialsFromCredentialsJson {
);
Log.newLine();
}
await new SetupTargetBuildCredentialsFromCredentialsJson(
await new SetUpTargetBuildCredentialsFromCredentialsJson(
{
account: this.app.account,
projectName: this.app.projectName,
@@ -18,7 +18,7 @@ import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { CreateDistributionCertificate } from './CreateDistributionCertificate';
import { formatDistributionCertificate } from './DistributionCertificateUtils';
export class SetupDistributionCertificate {
export class SetUpDistributionCertificate {
private validDistCerts?: AppleDistributionCertificateFragment[];
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
@@ -4,8 +4,8 @@ import { promptAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
import { getAllBuildCredentialsAsync } from './BuildCredentialsUtils';
import { SetupAdhocProvisioningProfile } from './SetupAdhocProvisioningProfile';
import { SetupProvisioningProfile } from './SetupProvisioningProfile';
import { SetUpAdhocProvisioningProfile } from './SetUpAdhocProvisioningProfile';
import { SetUpProvisioningProfile } from './SetUpProvisioningProfile';
/**
* It's used when setting up credentials for internal distribution but `enterpriseProvisioning` is not set.
@@ -14,7 +14,7 @@ import { SetupProvisioningProfile } from './SetupProvisioningProfile';
* to choose if they want to set up an adhoc or universal distribution provisioning profile. Otherwise, always
* set up an adhoc provisioning profile.
*/
export class SetupInternalProvisioningProfile {
export class SetUpInternalProvisioningProfile {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
@@ -82,13 +82,13 @@ export class SetupInternalProvisioningProfile {
private async setupAdhocProvisioningProfileAsync(
ctx: CredentialsContext
): Promise<IosAppBuildCredentialsFragment> {
return await new SetupAdhocProvisioningProfile(this.app).runAsync(ctx);
return await new SetUpAdhocProvisioningProfile(this.app).runAsync(ctx);
}
private async setupUniversalProvisioningProfileAsync(
ctx: CredentialsContext
): Promise<IosAppBuildCredentialsFragment> {
return await new SetupProvisioningProfile(this.app, IosDistributionType.Enterprise).runAsync(
return await new SetUpProvisioningProfile(this.app, IosDistributionType.Enterprise).runAsync(
ctx
);
}
@@ -20,12 +20,12 @@ import {
import { ConfigureProvisioningProfile } from './ConfigureProvisioningProfile';
import { CreateProvisioningProfile } from './CreateProvisioningProfile';
import { formatProvisioningProfileFromApple } from './ProvisioningProfileUtils';
import { SetupDistributionCertificate } from './SetupDistributionCertificate';
import { SetUpDistributionCertificate } from './SetUpDistributionCertificate';
/**
* Sets up either APP_STORE or ENTERPRISE provisioning profiles
*/
export class SetupProvisioningProfile {
export class SetUpProvisioningProfile {
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
async areBuildCredentialsSetupAsync(ctx: CredentialsContext): Promise<boolean> {
@@ -84,7 +84,7 @@ export class SetupProvisioningProfile {
}
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
const distCert = await new SetupDistributionCertificate(
const distCert = await new SetUpDistributionCertificate(
this.app,
this.distributionType
).runAsync(ctx);
@@ -14,7 +14,7 @@ import {
selectPushKeyAsync,
} from './PushKeyUtils';
export class SetupPushKey {
export class SetUpPushKey {
constructor(private app: AppLookupParams) {}
async isPushKeySetupAsync(ctx: CredentialsContext): Promise<boolean> {
@@ -8,9 +8,9 @@ import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { AppLookupParams as GraphQLAppLookupParams } from '../api/GraphqlClient';
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
import { SetupAdhocProvisioningProfile } from './SetupAdhocProvisioningProfile';
import { SetupInternalProvisioningProfile } from './SetupInternalProvisioningProfile';
import { SetupProvisioningProfile } from './SetupProvisioningProfile';
import { SetUpAdhocProvisioningProfile } from './SetUpAdhocProvisioningProfile';
import { SetUpInternalProvisioningProfile } from './SetUpInternalProvisioningProfile';
import { SetUpProvisioningProfile } from './SetUpProvisioningProfile';
interface Options {
app: GraphQLAppLookupParams;
@@ -18,7 +18,7 @@ interface Options {
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
export class SetupTargetBuildCredentials {
export class SetUpTargetBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
@@ -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;
}
}
@@ -50,17 +50,17 @@ export class SetupTargetBuildCredentials {
const { app, distribution, enterpriseProvisioning } = this.options;
if (distribution === 'internal') {
if (enterpriseProvisioning === 'adhoc') {
return await new SetupAdhocProvisioningProfile(app).runAsync(ctx);
return await new SetUpAdhocProvisioningProfile(app).runAsync(ctx);
} else if (enterpriseProvisioning === 'universal') {
return await new SetupProvisioningProfile(
return await new SetUpProvisioningProfile(
app,
GraphQLIosDistributionType.Enterprise
).runAsync(ctx);
} else {
return await new SetupInternalProvisioningProfile(app).runAsync(ctx);
return await new SetUpInternalProvisioningProfile(app).runAsync(ctx);
}
} else {
return await new SetupProvisioningProfile(app, GraphQLIosDistributionType.AppStore).runAsync(
return await new SetUpProvisioningProfile(app, GraphQLIosDistributionType.AppStore).runAsync(
ctx
);
}
@@ -22,7 +22,7 @@ import {
getProvisioningProfileAsync,
} from './BuildCredentialsUtils';
export class SetupTargetBuildCredentialsFromCredentialsJson {
export class SetUpTargetBuildCredentialsFromCredentialsJson {
constructor(
private app: AppLookupParams,
private distributionType: IosDistributionType,
@@ -5,7 +5,7 @@ import {
import { testDistCertFragmentOneDependency } from '../../../__tests__/fixtures-ios';
import { CredentialsContext } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
export class SetupDistributionCertificate {
export class SetUpDistributionCertificate {
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
public async runAsync(_ctx: CredentialsContext): Promise<AppleDistributionCertificateFragment> {
@@ -0,0 +1,90 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKey } from '../../../__tests__/fixtures-ios';
import { getCredentialsFromUserAsync } from '../../../utils/promptForCredentials';
import {
AppStoreApiKeyPurpose,
getAscApiKeyName,
promptForAscApiKeyPathAsync,
} from '../AscApiKeyUtils';
jest.mock('../../../../prompts');
jest.mock('../../../utils/promptForCredentials');
function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
return Object.keys(obj) as K[];
}
afterEach(() => {
asMock(promptAsync).mockClear();
asMock(getCredentialsFromUserAsync).mockClear();
});
describe(getAscApiKeyName, () => {
// Apple enforces a 30 char limit on this name
it('produces a name under 30 chars', async () => {
for (const value of enumKeys(AppStoreApiKeyPurpose)) {
const purpose = AppStoreApiKeyPurpose[value];
const name = getAscApiKeyName(purpose);
expect(name.length < 30).toBe(true);
}
});
});
describe(promptForAscApiKeyPathAsync, () => {
it('prompts for keyId, keyP8Path and issuerId when user is not authenticated to Apple', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => {
throw new Error('this should not be called');
}),
authCtx: null,
},
});
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
expect(ascApiKeyPath).toEqual({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
keyP8Path: '/asc-api-key.p8',
});
expect(promptAsync).toHaveBeenCalledTimes(1); // keyP8Path
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(2); // keyId, issuerId
});
it('prompts for keyId, keyP8Path and detects issuerId when user is authenticated to Apple', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
keyId: 'test-key-id',
}));
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
getAscApiKeyAsync: jest.fn(() => testAscApiKey),
},
});
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
expect(ascApiKeyPath).toEqual({
keyId: 'test-key-id',
issuerId: 'test-issuer-id-from-apple',
keyP8Path: '/asc-api-key.p8',
});
expect(promptAsync).toHaveBeenCalledTimes(1); // keyP8Path
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(1); // keyId
expect(asMock(ctx.appStore.getAscApiKeyAsync).mock.calls.length).toBe(1); // issuerId
});
});
@@ -0,0 +1,46 @@
import { findApplicationTarget } from '../../../../project/ios/target';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKeyFragment, testTargets } from '../../../__tests__/fixtures-ios';
import { AppStoreApiKeyPurpose } from '../AscApiKeyUtils';
import { AssignAscApiKey } from '../AssignAscApiKey';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
describe(AssignAscApiKey, () => {
it('assigns an App Store Connect Api Key in Interactive Mode for EAS Submit', async () => {
const ctx = createCtxMock({
nonInteractive: false,
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const assignAscApiKeyAction = new AssignAscApiKey(appLookupParams);
await assignAscApiKeyAction.runAsync(
ctx,
testAscApiKeyFragment,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
// expect app credentials to be fetched/created, then updated
expect(ctx.ios.createOrGetIosAppCredentialsWithCommonFieldsAsync).toHaveBeenCalledTimes(1);
expect(ctx.ios.updateIosAppCredentialsAsync).toHaveBeenCalledTimes(1);
});
it('works in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const assignAscApiKeyAction = new AssignAscApiKey(appLookupParams);
await assignAscApiKeyAction.runAsync(
ctx,
testAscApiKeyFragment,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
// dont fail if users are running in non-interactive mode
await expect(
assignAscApiKeyAction.runAsync(
ctx,
testAscApiKeyFragment,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
)
).resolves.not.toThrowError();
});
});
@@ -0,0 +1,44 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKey, testTargets } from '../../../__tests__/fixtures-ios';
import { AppStoreApiKeyPurpose } from '../AscApiKeyUtils';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreateAscApiKey } from '../CreateAscApiKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(CreateAscApiKey, () => {
it('creates a App Store Api Key in Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
createAscApiKeyAsync: jest.fn(() => testAscApiKey),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const createAscApiKeyAction = new CreateAscApiKey(appLookupParams.account);
await createAscApiKeyAction.runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
// expect api key to be created on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
// expect api key to be created on apple portal
expect(asMock(ctx.appStore.createAscApiKeyAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const createAscApiKeyAction = new CreateAscApiKey(appLookupParams.account);
await expect(
createAscApiKeyAction.runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE)
).rejects.toThrowError();
});
});
@@ -0,0 +1,33 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKeyFragment, testTargets } from '../../../__tests__/fixtures-ios';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { RemoveAscApiKey } from '../RemoveAscApiKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(RemoveAscApiKey, () => {
it('removes an Asc Api Key', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const removeAscApiKeyAction = new RemoveAscApiKey(
appLookupParams.account,
testAscApiKeyFragment
);
await removeAscApiKeyAction.runAsync(ctx);
expect(ctx.ios.deleteAscApiKeyAsync).toHaveBeenCalledTimes(1);
expect(ctx.appStore.revokeAscApiKeyAsync).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const removeAscApiKeyAction = new RemoveAscApiKey(
appLookupParams.account,
testAscApiKeyFragment
);
await expect(removeAscApiKeyAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -1,4 +1,4 @@
import { doUDIDsMatch } from '../SetupAdhocProvisioningProfile';
import { doUDIDsMatch } from '../SetUpAdhocProvisioningProfile';
describe(doUDIDsMatch, () => {
it('return false if UDIDs do not match', () => {
@@ -0,0 +1,146 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync, promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import {
getNewIosApiMock,
testAscApiKey,
testAscApiKeyFragment,
testCommonIosAppCredentialsFragment,
testTargets,
} from '../../../__tests__/fixtures-ios';
import { AppStoreApiKeyPurpose } from '../AscApiKeyUtils';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetUpAscApiKey, SetupAscApiKeyChoice } from '../SetUpAscApiKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(SetUpAscApiKey, () => {
it('skips setting up a ASC Api Key if it is already configured', async () => {
const ctx = createCtxMock({
nonInteractive: false,
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC Api Key on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration not to be updated with a new ASC Api Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
});
it('sets up a ASC Api Key, creating a new one if there are no existing ASC Api Keys', async () => {
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
createAscApiKeyAsync: jest.fn(() => testAscApiKey),
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(),
createAscApiKeyAsync: jest.fn(() => testAscApiKeyFragment),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await setupAscApiKeyAction.runAsync(ctx);
// expect to create a new ASC Api Key on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
// expect configuration to be updated with a new ASC Api Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a ASC Api Key, use autoselected ASC Api Key when there are existing keys', async () => {
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
listAscApiKeysAsync: jest.fn(() => [testAscApiKey]),
},
ios: {
...getNewIosApiMock(),
getAscApiKeysForAccountAsync: jest.fn(() => [testAscApiKeyFragment]),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC Api Key on expo servers, we are using the existing one
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration to be updated with a new ASC Api Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a ASC Api Key, allowing user to choose existing keys', async () => {
const ctx = createCtxMock({
nonInteractive: false,
ios: {
...getNewIosApiMock(),
getAscApiKeysForAccountAsync: jest.fn(() => [testAscApiKeyFragment]),
},
});
asMock(promptAsync).mockImplementation(() => ({
choice: SetupAscApiKeyChoice.USE_EXISTING,
chosenAscApiKey: testAscApiKeyFragment,
}));
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC Api Key on expo servers, we are using the existing one
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration to be updated with a new ASC Api Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('works in Non Interactive Mode if ASC Key is configured', async () => {
const ctx = createCtxMock({
nonInteractive: true,
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await expect(setupAscApiKeyAction.runAsync(ctx)).resolves.not.toThrowError();
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const createAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await expect(createAscApiKeyAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -4,13 +4,13 @@ import { promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAllBuildCredentialsAsync } from '../BuildCredentialsUtils';
import { SetupAdhocProvisioningProfile } from '../SetupAdhocProvisioningProfile';
import { SetupInternalProvisioningProfile } from '../SetupInternalProvisioningProfile';
import { SetupProvisioningProfile } from '../SetupProvisioningProfile';
import { SetUpAdhocProvisioningProfile } from '../SetUpAdhocProvisioningProfile';
import { SetUpInternalProvisioningProfile } from '../SetUpInternalProvisioningProfile';
import { SetUpProvisioningProfile } from '../SetUpProvisioningProfile';
jest.mock('../../../../prompts');
jest.mock('../SetupAdhocProvisioningProfile');
jest.mock('../SetupProvisioningProfile');
jest.mock('../SetUpAdhocProvisioningProfile');
jest.mock('../SetUpProvisioningProfile');
jest.mock('../BuildCredentialsUtils', () => ({ getAllBuildCredentialsAsync: jest.fn() }));
beforeEach(() => {
@@ -33,15 +33,15 @@ const testEnterpriseBuildCredentials: IosAppBuildCredentialsFragment = {
iosDistributionType: IosDistributionType.Enterprise,
};
describe(SetupInternalProvisioningProfile, () => {
describe(SetUpInternalProvisioningProfile, () => {
describe('interactive mode', () => {
describe('when authenticated with apple', () => {
it('runs the SetupAdhocProvisioningProfile action for non-enterprise team', async () => {
it('runs the SetUpAdhocProvisioningProfile action for non-enterprise team', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -56,7 +56,7 @@ describe(SetupInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetupAdhocProvisioningProfile.prototype.runAsync = runAsync;
SetUpAdhocProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
@@ -72,7 +72,7 @@ describe(SetupInternalProvisioningProfile, () => {
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -90,7 +90,7 @@ describe(SetupInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetupProvisioningProfile.prototype.runAsync = runAsync;
SetUpProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
@@ -110,7 +110,7 @@ describe(SetupInternalProvisioningProfile, () => {
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -125,14 +125,14 @@ describe(SetupInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetupProvisioningProfile.prototype.runAsync = runAsync;
SetUpProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
expect(runAsync).toHaveBeenCalled();
});
it('runs the SetupAdhocProvisioningProfile action when adhoc credentials exist', async () => {
it('runs the SetUpAdhocProvisioningProfile action when adhoc credentials exist', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
distributionType: IosDistributionType.Enterprise,
}));
@@ -141,7 +141,7 @@ describe(SetupInternalProvisioningProfile, () => {
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -156,14 +156,14 @@ describe(SetupInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetupAdhocProvisioningProfile.prototype.runAsync = runAsync;
SetUpAdhocProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
expect(runAsync).toHaveBeenCalled();
});
it('runs the SetupProvisioningProfile action when enterprise credentials exist', async () => {
it('runs the SetUpProvisioningProfile action when enterprise credentials exist', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
distributionType: IosDistributionType.Enterprise,
}));
@@ -174,7 +174,7 @@ describe(SetupInternalProvisioningProfile, () => {
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -189,7 +189,7 @@ describe(SetupInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetupProvisioningProfile.prototype.runAsync = runAsync;
SetUpProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
@@ -205,7 +205,7 @@ describe(SetupInternalProvisioningProfile, () => {
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -235,7 +235,7 @@ describe(SetupInternalProvisioningProfile, () => {
];
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -253,7 +253,7 @@ describe(SetupInternalProvisioningProfile, () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -264,12 +264,12 @@ describe(SetupInternalProvisioningProfile, () => {
await expect(action.runAsync(ctx)).rejects.toThrow(/couldn't find any credentials/);
});
it('runs the SetupAdhocProvisioningProfile action when adhoc credentials exist', async () => {
it('runs the SetUpAdhocProvisioningProfile action when adhoc credentials exist', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [testAdhocBuildCredentials];
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -279,19 +279,19 @@ describe(SetupInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetupAdhocProvisioningProfile.prototype.runAsync = runAsync;
SetUpAdhocProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
expect(runAsync).toHaveBeenCalled();
});
it('runs the SetupProvisioningProfile action when enterprise credentials exist', async () => {
it('runs the SetUpProvisioningProfile action when enterprise credentials exist', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [testEnterpriseBuildCredentials];
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -301,7 +301,7 @@ describe(SetupInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetupProvisioningProfile.prototype.runAsync = runAsync;
SetUpProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
@@ -16,16 +16,16 @@ import {
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { validateProvisioningProfileAsync } from '../../validators/validateProvisioningProfile';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetupProvisioningProfile } from '../SetupProvisioningProfile';
import { SetUpProvisioningProfile } from '../SetUpProvisioningProfile';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mock('../SetupDistributionCertificate');
jest.mock('../SetUpDistributionCertificate');
jest.mock('../ConfigureProvisioningProfile');
jest.mock('../CreateProvisioningProfile');
jest.mock('../../validators/validateProvisioningProfile');
describe('SetupProvisioningProfile', () => {
describe('SetUpProvisioningProfile', () => {
it('repairs existing Provisioning Profile with bad build credentials in Interactive Mode', async () => {
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
const ctx = createCtxMock({
@@ -54,7 +54,7 @@ describe('SetupProvisioningProfile', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupProvisioningProfileAction = new SetupProvisioningProfile(
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
IosDistributionType.AppStore
);
@@ -84,7 +84,7 @@ describe('SetupProvisioningProfile', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupProvisioningProfileAction = new SetupProvisioningProfile(
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
IosDistributionType.AppStore
);
@@ -114,7 +114,7 @@ describe('SetupProvisioningProfile', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupProvisioningProfileAction = new SetupProvisioningProfile(
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
IosDistributionType.AppStore
);
@@ -140,7 +140,7 @@ describe('SetupProvisioningProfile', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupProvisioningProfileAction = new SetupProvisioningProfile(
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
IosDistributionType.AppStore
);
@@ -157,7 +157,7 @@ describe('SetupProvisioningProfile', () => {
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupProvisioningProfileAction = new SetupProvisioningProfile(
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
IosDistributionType.AppStore
);
@@ -5,12 +5,12 @@ import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appsto
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getNewIosApiMock, testPushKey, testTargets } from '../../../__tests__/fixtures-ios';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetupPushKey } from '../SetupPushKey';
import { SetUpPushKey } from '../SetUpPushKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(SetupPushKey, () => {
describe(SetUpPushKey, () => {
it('skips setting up a Push Key if it is already configured', async () => {
const ctx = createCtxMock({
nonInteractive: false,
@@ -20,7 +20,7 @@ describe(SetupPushKey, () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupPushKeyAction = new SetupPushKey(appLookupParams);
const setupPushKeyAction = new SetUpPushKey(appLookupParams);
await setupPushKeyAction.runAsync(ctx);
// expect not to create a new push key on expo servers
@@ -38,7 +38,7 @@ describe(SetupPushKey, () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupPushKeyAction = new SetupPushKey(appLookupParams);
const setupPushKeyAction = new SetUpPushKey(appLookupParams);
await setupPushKeyAction.runAsync(ctx);
// expect to create a new push key on expo servers
@@ -62,7 +62,7 @@ describe(SetupPushKey, () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupPushKeyAction = new SetupPushKey(appLookupParams);
const setupPushKeyAction = new SetUpPushKey(appLookupParams);
await setupPushKeyAction.runAsync(ctx);
// expect not to create a new push key on expo servers, we are using the existing one
@@ -75,7 +75,7 @@ describe(SetupPushKey, () => {
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const createPushKeyAction = new SetupPushKey(appLookupParams);
const createPushKeyAction = new SetUpPushKey(appLookupParams);
await expect(createPushKeyAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -15,7 +15,7 @@ import {
} from '../../../__tests__/fixtures-ios';
import { IosTargetCredentials } from '../../../credentialsJson/types';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetupTargetBuildCredentialsFromCredentialsJson } from '../SetupTargetBuildCredentialsFromCredentialsJson';
import { SetUpTargetBuildCredentialsFromCredentialsJson } from '../SetUpTargetBuildCredentialsFromCredentialsJson';
jest.mock('../../../../prompts');
jest.mock('../../../credentialsJson/read');
@@ -25,7 +25,7 @@ beforeEach(() => {
asMock(confirmAsync).mockImplementation(() => true);
});
describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
const targetCredentials: IosTargetCredentials = {
distributionCertificate: {
certificateP12: testDistCert.certP12,
@@ -53,7 +53,7 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
new SetUpTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
@@ -82,7 +82,7 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
new SetUpTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
@@ -115,7 +115,7 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
new SetUpTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
@@ -149,7 +149,7 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
new SetUpTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
@@ -1,7 +1,10 @@
import { UserRole } from '@expo/apple-utils';
import nullthrows from 'nullthrows';
import {
AppFragment,
AppStoreConnectApiKeyFragment,
AppStoreConnectUserRole,
AppleAppIdentifierFragment,
AppleDeviceFragment,
AppleDistributionCertificateFragment,
@@ -14,7 +17,9 @@ import {
import { isWildcardBundleIdentifier } from '../../../project/ios/bundleIdentifier';
import { Account } from '../../../user/Account';
import { DistributionCertificate, PushKey } from '../appstore/Credentials.types';
import { MinimalAscApiKey } from '../credentials';
import { AppleTeamMissingError } from '../errors';
import { AppStoreConnectApiKeyMutation } from './graphql/mutations/AppStoreConnectApiKeyMutation';
import { AppleAppIdentifierMutation } from './graphql/mutations/AppleAppIdentifierMutation';
import {
AppleDistributionCertificateMutation,
@@ -29,6 +34,7 @@ import { AppleTeamMutation } from './graphql/mutations/AppleTeamMutation';
import { IosAppBuildCredentialsMutation } from './graphql/mutations/IosAppBuildCredentialsMutation';
import { IosAppCredentialsMutation } from './graphql/mutations/IosAppCredentialsMutation';
import { AppQuery } from './graphql/queries/AppQuery';
import { AppStoreConnectApiKeyQuery } from './graphql/queries/AppStoreConnectApiKeyQuery';
import { AppleAppIdentifierQuery } from './graphql/queries/AppleAppIdentifierQuery';
import { AppleDeviceQuery } from './graphql/queries/AppleDeviceQuery';
import { AppleDistributionCertificateQuery } from './graphql/queries/AppleDistributionCertificateQuery';
@@ -161,8 +167,10 @@ export async function updateIosAppCredentialsAsync(
appCredentials: CommonIosAppCredentialsFragment,
{
applePushKeyId,
ascApiKeyIdForSubmissions,
}: {
applePushKeyId?: string;
ascApiKeyIdForSubmissions?: string;
}
): Promise<CommonIosAppCredentialsFragment> {
let updatedAppCredentials = appCredentials;
@@ -172,6 +180,13 @@ export async function updateIosAppCredentialsAsync(
applePushKeyId
);
}
if (ascApiKeyIdForSubmissions) {
updatedAppCredentials =
await IosAppCredentialsMutation.setAppStoreConnectApiKeyForSubmissionsAsync(
updatedAppCredentials.id,
ascApiKeyIdForSubmissions
);
}
return updatedAppCredentials;
}
@@ -413,5 +428,78 @@ export async function deletePushKeyAsync(pushKeyId: string): Promise<void> {
return await ApplePushKeyMutation.deleteApplePushKeyAsync(pushKeyId);
}
export async function createAscApiKeyAsync(
account: Account,
ascApiKey: MinimalAscApiKey
): Promise<AppStoreConnectApiKeyFragment> {
const maybeAppleTeam = ascApiKey.teamId
? await createOrGetExistingAppleTeamAsync(account, {
appleTeamIdentifier: ascApiKey.teamId,
appleTeamName: ascApiKey.teamName,
})
: null;
return await AppStoreConnectApiKeyMutation.createAppStoreConnectApiKeyAsync(
{
issuerIdentifier: ascApiKey.issuerId,
keyIdentifier: ascApiKey.keyId,
keyP8: ascApiKey.keyP8,
name: ascApiKey.name ?? null,
roles: ascApiKey.roles?.map(role => convertUserRoleToGraphqlType(role)) ?? null,
appleTeamId: maybeAppleTeam ? maybeAppleTeam.id : null,
},
account.id
);
}
export async function getAscApiKeysForAccountAsync(
account: Account
): Promise<AppStoreConnectApiKeyFragment[]> {
return await AppStoreConnectApiKeyQuery.getAllForAccountAsync(account.name);
}
export async function getAscApiKeyForAppSubmissionsAsync(
appLookupParams: AppLookupParams
): Promise<AppStoreConnectApiKeyFragment | null> {
const maybeIosAppCredentials = await getIosAppCredentialsWithCommonFieldsAsync(appLookupParams);
return maybeIosAppCredentials?.appStoreConnectApiKeyForSubmissions ?? null;
}
export async function deleteAscApiKeyAsync(ascApiKeyId: string): Promise<void> {
return await AppStoreConnectApiKeyMutation.deleteAppStoreConnectApiKeyAsync(ascApiKeyId);
}
function convertUserRoleToGraphqlType(userRole: UserRole): AppStoreConnectUserRole {
switch (userRole) {
case UserRole.ADMIN:
return AppStoreConnectUserRole.Admin;
case UserRole.ACCESS_TO_REPORTS:
return AppStoreConnectUserRole.AccessToReports;
case UserRole.ACCOUNT_HOLDER:
return AppStoreConnectUserRole.AccountHolder;
case UserRole.APP_MANAGER:
return AppStoreConnectUserRole.AppManager;
case UserRole.CLOUD_MANAGED_APP_DISTRIBUTION:
return AppStoreConnectUserRole.CloudManagedAppDistribution;
case UserRole.CLOUD_MANAGED_DEVELOPER_ID:
return AppStoreConnectUserRole.CloudManagedDeveloperId;
case UserRole.CREATE_APPS:
return AppStoreConnectUserRole.CreateApps;
case UserRole.CUSTOMER_SUPPORT:
return AppStoreConnectUserRole.CustomerSupport;
case UserRole.DEVELOPER:
return AppStoreConnectUserRole.Developer;
case UserRole.FINANCE:
return AppStoreConnectUserRole.Finance;
case UserRole.MARKETING:
return AppStoreConnectUserRole.Marketing;
case UserRole.READ_ONLY:
return AppStoreConnectUserRole.ReadOnly;
case UserRole.SALES:
return AppStoreConnectUserRole.Sales;
case UserRole.TECHNICAL:
return AppStoreConnectUserRole.Technical;
}
}
const formatProjectFullName = ({ account, projectName }: AppLookupParams): string =>
`@${account.name}/${projectName}`;
@@ -0,0 +1,75 @@
import assert from 'assert';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
import {
AppStoreConnectApiKeyFragment,
AppStoreConnectApiKeyInput,
CreateAppStoreConnectApiKeyMutation,
DeleteAppStoreConnectApiKeyMutation,
} from '../../../../../graphql/generated';
import { AppStoreConnectApiKeyFragmentNode } from '../../../../../graphql/types/credentials/AppStoreConnectApiKey';
export const AppStoreConnectApiKeyMutation = {
async createAppStoreConnectApiKeyAsync(
appStoreConnectApiKeyInput: AppStoreConnectApiKeyInput,
accountId: string
): Promise<AppStoreConnectApiKeyFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateAppStoreConnectApiKeyMutation>(
gql`
mutation CreateAppStoreConnectApiKeyMutation(
$appStoreConnectApiKeyInput: AppStoreConnectApiKeyInput!
$accountId: ID!
) {
appStoreConnectApiKey {
createAppStoreConnectApiKey(
appStoreConnectApiKeyInput: $appStoreConnectApiKeyInput
accountId: $accountId
) {
id
...AppStoreConnectApiKeyFragment
}
}
}
${print(AppStoreConnectApiKeyFragmentNode)}
`,
{
appStoreConnectApiKeyInput,
accountId,
}
)
.toPromise()
);
assert(
data.appStoreConnectApiKey.createAppStoreConnectApiKey,
'GraphQL: `createAppStoreConnectApiKey` not defined in server response'
);
return data.appStoreConnectApiKey.createAppStoreConnectApiKey;
},
async deleteAppStoreConnectApiKeyAsync(appStoreConnectApiKeyId: string): Promise<void> {
await withErrorHandlingAsync(
graphqlClient
.mutation<DeleteAppStoreConnectApiKeyMutation>(
gql`
mutation DeleteAppStoreConnectApiKeyMutation($appStoreConnectApiKeyId: ID!) {
appStoreConnectApiKey {
deleteAppStoreConnectApiKey(id: $appStoreConnectApiKeyId) {
id
}
}
}
`,
{
appStoreConnectApiKeyId,
},
{
additionalTypenames: ['AppStoreConnectApiKey', 'IosAppCredentials'],
}
)
.toPromise()
);
},
};
@@ -7,6 +7,7 @@ import {
CommonIosAppCredentialsFragment,
CreateIosAppCredentialsMutation,
IosAppCredentialsInput,
SetAppStoreConnectApiKeyForSubmissionsMutation,
SetPushKeyMutation,
} from '../../../../../graphql/generated';
import { CommonIosAppCredentialsFragmentNode } from '../../../../../graphql/types/credentials/IosAppCredentials';
@@ -80,4 +81,37 @@ export const IosAppCredentialsMutation = {
);
return data.iosAppCredentials.setPushKey;
},
async setAppStoreConnectApiKeyForSubmissionsAsync(
iosAppCredentialsId: string,
ascApiKeyId: string
): Promise<CommonIosAppCredentialsFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<SetAppStoreConnectApiKeyForSubmissionsMutation>(
gql`
mutation SetAppStoreConnectApiKeyForSubmissionsMutation(
$iosAppCredentialsId: ID!
$ascApiKeyId: ID!
) {
iosAppCredentials {
setAppStoreConnectApiKeyForSubmissions(
id: $iosAppCredentialsId
ascApiKeyId: $ascApiKeyId
) {
id
...CommonIosAppCredentialsFragment
}
}
}
${print(CommonIosAppCredentialsFragmentNode)}
`,
{
iosAppCredentialsId,
ascApiKeyId,
}
)
.toPromise()
);
return data.iosAppCredentials.setAppStoreConnectApiKeyForSubmissions;
},
};
@@ -22,7 +22,10 @@ export const AppQuery = {
}
${print(AppFragmentNode)}
`,
{ fullName }
{ fullName },
{
additionalTypenames: ['App'],
}
)
.toPromise()
);
@@ -0,0 +1,41 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
import {
AppStoreConnectApiKeyByAccountQuery,
AppStoreConnectApiKeyFragment,
} from '../../../../../graphql/generated';
import { AppStoreConnectApiKeyFragmentNode } from '../../../../../graphql/types/credentials/AppStoreConnectApiKey';
export const AppStoreConnectApiKeyQuery = {
async getAllForAccountAsync(accountName: string): Promise<AppStoreConnectApiKeyFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppStoreConnectApiKeyByAccountQuery>(
gql`
query AppStoreConnectApiKeyByAccountQuery($accountName: String!) {
account {
byName(accountName: $accountName) {
id
appStoreConnectApiKeys {
id
...AppStoreConnectApiKeyFragment
}
}
}
}
${print(AppStoreConnectApiKeyFragmentNode)}
`,
{
accountName,
},
{
additionalTypenames: ['AppStoreConnectApiKey'],
}
)
.toPromise()
);
return data.account.byName.appStoreConnectApiKeys;
},
};
@@ -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',
@@ -150,7 +150,7 @@ export default class AppStoreApi {
return await listAscApiKeysAsync(ctx);
}
public async getAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
public async getAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo | null> {
const ctx = await this.ensureAuthenticatedAsync();
return await getAscApiKeyAsync(ctx, keyId);
}
@@ -76,6 +76,7 @@ export type AscApiKeyInfo = {
name: string;
teamName?: string;
roles: UserRole[];
isRevoked: boolean;
};
export type AscApiKey = AscApiKeyInfo & {
@@ -47,6 +47,7 @@ const mockAscApiKeyInfo = {
roles: ['ADMIN'],
teamId: 'test-id',
teamName: 'test-name',
isRevoked: false,
};
beforeEach(() => {
@@ -6,28 +6,36 @@ 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();
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.`);
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.`);
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();
export async function getAscApiKeyAsync(
authCtx: AuthCtx,
keyId: string
): Promise<AscApiKeyInfo | null> {
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}).`);
spinner.succeed(`Fetched App Store Connect Api Key (ID: ${keyId}).`);
return getAscApiKeyInfo(apiKey, authCtx);
} catch (error) {
} catch (error: any) {
const message = error?.message ?? '';
if (message.includes("There is no resource of type 'apiKeys' with id")) {
spinner.stop();
return null;
}
Log.error(error);
spinner.fail(`Failed to fetch App Store Connect API Key.`);
spinner.fail(`Failed to fetch App Store Connect Api Key.`);
throw error;
}
}
@@ -41,7 +49,7 @@ export async function createAscApiKeyAsync(
keyType,
}: Partial<Pick<ApiKeyProps, 'nickname' | 'roles' | 'allAppsVisible' | 'keyType'>>
): Promise<AscApiKey> {
const spinner = ora(`Creating App Store Connect API Key.`).start();
const spinner = ora(`Creating App Store Connect Api Key.`).start();
try {
const context = getRequestContext(authCtx);
const key = await ApiKey.createAsync(context, {
@@ -64,12 +72,15 @@ export async function createAscApiKeyAsync(
}
throw new Error(`Failed to download .p8 file of ${humanReadableKey}.`);
}
// this object has more optional parameters populated
const fullKey = await ApiKey.infoAsync(context, { id: key.id });
spinner.succeed(`Created App Store Connect Api Key.`);
return {
...getAscApiKeyInfo(key, authCtx),
...getAscApiKeyInfo(fullKey, authCtx),
keyP8,
};
} catch (err: any) {
spinner.fail('Failed to create App Store Connect API Key.');
spinner.fail('Failed to create App Store Connect Api Key.');
throw err;
}
}
@@ -78,16 +89,16 @@ export async function revokeAscApiKeyAsync(
authCtx: AuthCtx,
keyId: string
): Promise<AscApiKeyInfo> {
const spinner = ora(`Revoking App Store Connect API Key.`).start();
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.`);
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.`);
spinner.fail(`Failed to revoke App Store Connect Api Key.`);
throw error;
}
}
@@ -100,5 +111,6 @@ export function getAscApiKeyInfo(apiKey: ApiKey, authCtx: AuthCtx): AscApiKeyInf
teamId: authCtx.team.id,
teamName: authCtx.team.name,
roles: apiKey.attributes.roles,
isRevoked: !!apiKey.attributes.revokingDate,
};
}
@@ -118,14 +118,20 @@ export interface AscApiKeyPath {
issuerId: string;
}
export const ascApiKeyMetadataSchema: CredentialSchema<Omit<MinimalAscApiKey, 'keyP8'>> = {
name: 'App Store Connect API Key',
export const ascApiKeyIdSchema: CredentialSchema<Pick<MinimalAscApiKey, 'keyId'>> = {
name: 'App Store Connect Api Key',
questions: [
{
field: 'keyId',
type: 'string',
question: 'Key ID:',
},
],
};
export const ascApiKeyIssuerIdSchema: CredentialSchema<Pick<MinimalAscApiKey, 'issuerId'>> = {
name: 'App Store Connect Api Key',
questions: [
{
field: 'issuerId',
type: 'string',
Loaded 100 of 167 files, more files were not shown because too many files have changed in this diff. Show more