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
83 changed files with 2551 additions and 674 deletions

No files matched your search

+33 -2
View File
@@ -6,13 +6,44 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
### 🎉 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))
### 🎉 New features
### 🐛 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))
+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.34.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",
+443
View File
@@ -33,7 +33,265 @@ eas --help COMMAND
# Commands
<!-- commands -->
* [`eas account:login`](#eas-accountlogin)
* [`eas account:logout`](#eas-accountlogout)
* [`eas account:view`](#eas-accountview)
* [`eas analytics [STATUS]`](#eas-analytics-status)
* [`eas build`](#eas-build)
* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
* [`eas build:configure`](#eas-buildconfigure)
* [`eas build:list`](#eas-buildlist)
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
* [`eas config`](#eas-config)
* [`eas credentials`](#eas-credentials)
* [`eas device:create`](#eas-devicecreate)
* [`eas device:list`](#eas-devicelist)
* [`eas device:view [UDID]`](#eas-deviceview-udid)
* [`eas diagnostics`](#eas-diagnostics)
* [`eas help [COMMAND]`](#eas-help-command)
* [`eas project:info`](#eas-projectinfo)
* [`eas project:init`](#eas-projectinit)
* [`eas secret:create`](#eas-secretcreate)
* [`eas secret:delete`](#eas-secretdelete)
* [`eas secret:list`](#eas-secretlist)
* [`eas submit`](#eas-submit)
* [`eas webhook:create`](#eas-webhookcreate)
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
* [`eas webhook:list`](#eas-webhooklist)
* [`eas webhook:update`](#eas-webhookupdate)
* [`eas webhook:view ID`](#eas-webhookview-id)
## `eas account:login`
log in with your Expo account
```
USAGE
$ eas account:login
ALIASES
$ eas login
```
_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`
log out
```
USAGE
$ eas account:logout
ALIASES
$ eas logout
```
_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`
show the username you are logged in as
```
USAGE
$ eas account:view
ALIASES
$ eas whoami
```
_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]`
view or change analytics settings
```
USAGE
$ eas analytics [STATUS]
```
_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`
start a build
```
USAGE
$ eas build
OPTIONS
-p, --platform=(android|ios|all)
--auto-submit Submit on build complete using the submit profile with the same name as the
build profile
--auto-submit-with-profile=PROFILE_NAME Submit on build complete using the submit profile with provided name
--clear-cache Clear cache before the build
--json Enable JSON output, non-JSON messages will be printed to stderr
--local Run build locally [experimental]
--non-interactive Run command in non-interactive mode
--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.34.1/packages/eas-cli/src/commands/build/index.js)_
## `eas build:cancel [BUILD_ID]`
Cancel a build.
```
USAGE
$ eas build:cancel [BUILD_ID]
```
_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`
Configure the project to support EAS Build.
```
USAGE
$ eas build:configure
OPTIONS
-p, --platform=(android|ios|all) Platform to configure
```
_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`
list all builds for your project
```
USAGE
$ eas build:list
OPTIONS
--appBuildVersion=appBuildVersion
--appIdentifier=appIdentifier
--appVersion=appVersion
--buildProfile=buildProfile
--channel=channel
--distribution=(store|internal|simulator)
--gitCommitHash=gitCommitHash
--json Enable JSON output, non-JSON messages will be printed
to stderr
--limit=limit
--platform=(all|android|ios)
--runtimeVersion=runtimeVersion
--sdkVersion=sdkVersion
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
_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]`
view a build for your project
```
USAGE
$ eas build:view [BUILD_ID]
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.34.1/packages/eas-cli/src/commands/build/view.js)_
## `eas config`
show the eas.json config
```
USAGE
$ eas config
OPTIONS
-p, --platform=(android|ios)
--profile=profile
```
_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`
manage your credentials
```
USAGE
$ eas credentials
```
_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`
register new Apple Devices to use for internal distribution
```
USAGE
$ eas device:create
```
_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`
list all registered devices for your account
```
USAGE
$ eas device:list
OPTIONS
--apple-team-id=apple-team-id
```
_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]`
view a device for your project
```
USAGE
$ eas device:view [UDID]
```
_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`
log environment info to the console
```
USAGE
$ eas diagnostics
```
_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]`
@@ -51,4 +309,189 @@ OPTIONS
```
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.3/src/commands/help.ts)_
## `eas project:info`
information about the current project
```
USAGE
$ eas project:info
```
_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`
create or link an EAS project
```
USAGE
$ eas project:init
ALIASES
$ eas init
```
_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`
Create an environment secret on the current project or owner account.
```
USAGE
$ eas secret:create
OPTIONS
--force Delete and recreate existing secrets
--name=name Name of the secret
--scope=(account|project) [default: project] Scope for the secret
--value=value Value of the secret
```
_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`
Delete an environment secret by ID.
```
USAGE
$ eas secret:delete
OPTIONS
--id=id ID of the secret to delete
DESCRIPTION
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.34.1/packages/eas-cli/src/commands/secret/delete.js)_
## `eas secret:list`
Lists environment secrets available for your current app
```
USAGE
$ eas secret:list
```
_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`
submit build archive to app store
```
USAGE
$ eas submit
OPTIONS
-p, --platform=(android|ios|all)
--id=id ID of the build to submit
--latest Submit the latest build for specified platform
--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 "production" if defined in
eas.json.
--url=url App archive url
--verbose Always print logs from Submission Service
--[no-]wait Wait for submission to complete
DESCRIPTION
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.34.1/packages/eas-cli/src/commands/submit.js)_
## `eas webhook:create`
Create a webhook on the current project.
```
USAGE
$ eas webhook:create
OPTIONS
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
--secret=secret Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature'
header.
--url=url Webhook URL
```
_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]`
Delete a webhook on the current project.
```
USAGE
$ eas webhook:delete [ID]
ARGUMENTS
ID ID of the webhook to delete
```
_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`
List webhooks on the current project.
```
USAGE
$ eas webhook:list
OPTIONS
--event=(BUILD) Event type that triggers the webhook
```
_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`
Create a webhook on the current project.
```
USAGE
$ eas webhook:update
OPTIONS
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
--id=id (required) Webhook ID
--secret=secret Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature'
header.
--url=url Webhook URL
```
_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`
View a webhook on the current project.
```
USAGE
$ eas webhook:view ID
ARGUMENTS
ID ID of the webhook to view
```
_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 -->
+138 -272
View File
@@ -924,6 +924,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "displayName",
"description": "Best-effort human readable name for this actor for use in user interfaces during action attribution.\nFor example, when displaying a sentence indicating that actor X created a build or published an update.",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "accounts",
"description": "Associated accounts",
@@ -1865,30 +1881,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "appleAppSpecificPasswords",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "AppleAppSpecificPassword",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "appStoreConnectApiKeys",
"description": "",
@@ -2930,6 +2922,30 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "releaseChannels",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "builds",
"description": "(EAS Build) Builds associated with this app",
@@ -3637,6 +3653,46 @@
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "filterPlatforms",
"description": "",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "AppPlatform",
"ofType": null
}
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "filterReleaseChannels",
"description": "",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
@@ -5119,6 +5175,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "displayName",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "isSecondFactorAuthenticationEnabled",
"description": "",
@@ -8089,18 +8161,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "appSpecificPassword",
"description": "",
"args": [],
"type": {
"kind": "OBJECT",
"name": "AppleAppSpecificPassword",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "iosAppBuildCredentialsArray",
"description": "",
@@ -9566,109 +9626,6 @@
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AppleAppSpecificPassword",
"description": "",
"fields": [
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "account",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Account",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "appleIdUsername",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "passwordLabel",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updatedAt",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AndroidAppCredentialsFilter",
@@ -11342,6 +11299,30 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nextInvoice",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "willCancel",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -11877,8 +11858,8 @@
}
}
},
"isDeprecated": false,
"deprecationReason": null
"isDeprecated": true,
"deprecationReason": "App directory no longer supported"
}
],
"inputFields": null,
@@ -13548,22 +13529,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "appleAppSpecificPassword",
"description": "Mutations that modify an App Specific Password for an Apple User Account",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "AppleAppSpecificPasswordMutation",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "appleDevice",
"description": "Mutations that modify an Apple Device",
@@ -15844,121 +15809,6 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AppleAppSpecificPasswordMutation",
"description": "",
"fields": [
{
"name": "createAppleAppSpecificPassword",
"description": "Create an App Specific Password for an Apple User Account",
"args": [
{
"name": "appleAppSpecificPasswordInput",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "AppleAppSpecificPasswordInput",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "accountId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "AppleAppSpecificPassword",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AppleAppSpecificPasswordInput",
"description": "",
"fields": null,
"inputFields": [
{
"name": "appleIdUsername",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "passwordLabel",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "appSpecificPassword",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AppleDeviceMutation",
@@ -19877,6 +19727,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "displayName",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "accounts",
"description": "Associated accounts",
+5 -5
View File
@@ -1,18 +1,18 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.34.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/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.0",
"@expo/eas-json": "^0.34.1",
"@expo/json-file": "8.2.33",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.14",
@@ -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"
}
}
@@ -11,6 +11,7 @@ import { resolveWorkflowAsync } from '../../project/workflow';
import { confirmAsync } from '../../prompts';
import { expoCommandAsync } from '../../utils/expoCli';
import { ProfileData } from '../../utils/profiles';
import { getVcsClient } from '../../vcs';
import { reviewAndCommitChangesAsync } from './repository';
export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
@@ -107,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';
};
+15 -5
View File
@@ -14,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 {
@@ -392,7 +392,8 @@ export async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
if (!(await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir)))) {
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
if (!(await fs.pathExists(easJsonPath))) {
return;
}
const easJsonReader = new EasJsonReader(projectDir);
@@ -423,7 +424,7 @@ export async function handleDeprecatedEasJsonAsync(
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`
)} in ${chalk.bold('eas.json')} or with the following prompt.`
);
Log.newLine();
@@ -432,12 +433,21 @@ export async function handleDeprecatedEasJsonAsync(
{ 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(EasJsonReader.formatEasJsonPath(projectDir), rawEasJson, { spaces: 2 });
await fs.writeJSON(easJsonPath, rawEasJson, { spaces: 2 });
Log.withTick('Updated eas.json');
if (mode === 'requireCommit') {
setVcsClient(new GitClient());
await getVcsClient().trackFileAsync(easJsonPath);
await reviewAndCommitChangesAsync('Set cli.requireCommit to true in eas.json', {
nonInteractive,
});
}
}
@@ -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({
@@ -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
);
@@ -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(
@@ -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;
}
}
@@ -159,7 +159,7 @@ export async function provideOrGenerateDistributionCertificateAsync(
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 {
@@ -19,7 +19,7 @@ export async function provideOrGeneratePushKeyAsync(ctx: CredentialsContext): Pr
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);
@@ -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> {
@@ -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;
},
};
@@ -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;
},
};
@@ -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',
@@ -2,47 +2,55 @@
exports[`print credentials prints the IosAppCredentials map 1`] = `
Array [
"iOS Credentials
Project @quinlanj/test52
Bundle Identifier com.quinlanj.test52
Apple Team test-apple-identifier
Push Key
Developer Portal ID 9839M6AY8W
Apple Team 77KQ969CHE
Updated 6 months ago
App Store Configuration
Distribution Certificate
Serial Number test-serial
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 6 months ago
Provisioning Profile
Developer Portal ID test-profile-id
Status active
Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 5 months ago
Ad Hoc Configuration
Distribution Certificate
Serial Number test-serial
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 6 months ago
Provisioning Profile
Developer Portal ID test-profile-id
Status active
Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 5 months ago
",
"iOS Credentials
Project @quinlanj/test52
Bundle Identifier com.quinlanj.test52
Apple Team test-apple-identifier
Push Key
Developer Portal ID 9839M6AY8W
Apple Team 77KQ969CHE
Updated 6 months ago
App Store Connect Api Key
Developer Portal ID NYN468XNN3
Name Expo Submissions pl94k8_lnS
Issuer ID 69a6de93-e4e4-47e3-e053-5b8c7c11a4d1
Roles ADMIN
Apple Team 77KQ969CHE
Updated 6 months ago
App Store Configuration
Distribution Certificate
Serial Number test-serial
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 6 months ago
Provisioning Profile
Developer Portal ID test-profile-id
Status active
Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 5 months ago
Ad Hoc Configuration
Distribution Certificate
Serial Number test-serial
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 6 months ago
Provisioning Profile
Developer Portal ID test-profile-id
Status active
Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 5 months ago
",
]
`;
@@ -2,6 +2,7 @@ import chalk from 'chalk';
import dateformat from 'dateformat';
import {
AppStoreConnectApiKeyFragment,
AppleDeviceFragment,
ApplePushKeyFragment,
IosAppBuildCredentialsFragment,
@@ -91,7 +92,7 @@ export function displayIosCredentials(
continue;
}
const { appleTeam, pushKey } = targetAppCredentials;
const { appleTeam, pushKey, appStoreConnectApiKeyForSubmissions } = targetAppCredentials;
if (appleTeam) {
const { appleTeamIdentifier, appleTeamName } = appleTeam;
fields.push({
@@ -105,6 +106,10 @@ export function displayIosCredentials(
displayApplePushKey(pushKey, fields);
}
if (appStoreConnectApiKeyForSubmissions) {
displayAscApiKey(appStoreConnectApiKeyForSubmissions, fields);
}
const sortedIosAppBuildCredentialsList = sortBuildCredentialsByDistributionType(
targetAppCredentials.iosAppBuildCredentialsList
);
@@ -230,6 +235,35 @@ function displayApplePushKey(
fields.push({ label: '', value: '' });
}
function displayAscApiKey(
maybeAscApiKey: AppStoreConnectApiKeyFragment | null,
fields: { label: string; value: string }[]
): void {
fields.push({ label: 'App Store Connect Api Key', value: '' });
if (maybeAscApiKey) {
const { keyIdentifier, issuerIdentifier, appleTeam, name, roles, updatedAt } = maybeAscApiKey;
fields.push({ label: 'Developer Portal ID', value: keyIdentifier });
if (name) {
fields.push({ label: 'Name', value: name });
}
fields.push({ label: 'Issuer ID', value: issuerIdentifier });
if (roles) {
fields.push({ label: 'Roles', value: roles.join(',') });
}
if (appleTeam) {
const { appleTeamIdentifier, appleTeamName } = appleTeam;
fields.push({
label: 'Apple Team',
value: `${appleTeamIdentifier} ${appleTeamName ? `(${appleTeamName})` : ''}`,
});
}
fields.push({ label: 'Updated', value: `${fromNow(new Date(updatedAt))} ago` });
} else {
fields.push({ label: '', value: 'None assigned yet' });
}
fields.push({ label: '', value: '' });
}
function formatAppleDevice(device: AppleDeviceFragment): string {
let deviceString = '';
if (device.name) {
@@ -0,0 +1,29 @@
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
import { CredentialsContext } from '../../context';
import { AscApiKeyInfo } from '../appstore/Credentials.types';
import { MinimalAscApiKey } from '../credentials';
export async function isAscApiKeyValidAndTrackedAsync(
ctx: CredentialsContext,
ascApiKey: MinimalAscApiKey
): Promise<boolean> {
const ascApiKeyInfo = await ctx.appStore.getAscApiKeyAsync(ascApiKey.keyId);
return isKeyValid(ascApiKeyInfo);
}
export async function getValidAndTrackedAscApiKeysAsync(
ctx: CredentialsContext,
ascApiKeys: AppStoreConnectApiKeyFragment[]
): Promise<AppStoreConnectApiKeyFragment[]> {
const ascApiKeysInfo = await ctx.appStore.listAscApiKeysAsync();
const validAscApiKeysInfo = ascApiKeysInfo.filter(keyInfo => isKeyValid(keyInfo));
const validKeyIdentifiers = new Set(validAscApiKeysInfo.map(keyInfo => keyInfo.keyId));
return ascApiKeys.filter(key => validKeyIdentifiers.has(key.keyIdentifier));
}
function isKeyValid(ascApiKeyInfo: AscApiKeyInfo | null): boolean {
if (!ascApiKeyInfo) {
return false;
}
return !ascApiKeyInfo.isRevoked;
}
@@ -25,26 +25,31 @@ export enum AndroidActionType {
CreateGsaKey,
UseExistingGsaKey,
RemoveGsaKey,
SetupGsaKey,
SetUpGsaKey,
UpdateCredentialsJson,
SetupBuildCredentialsFromCredentialsJson,
SetUpBuildCredentialsFromCredentialsJson,
}
export enum IosActionType {
ManageCredentialsJson,
ManageBuildCredentials,
ManagePushKey,
ManageAscApiKey,
GoBackToCaller,
GoBackToHighLevelActions,
SetupBuildCredentials,
SetupBuildCredentialsFromCredentialsJson,
SetUpBuildCredentials,
SetUpBuildCredentialsFromCredentialsJson,
UpdateCredentialsJson,
UseExistingDistributionCertificate,
RemoveProvisioningProfile,
CreateDistributionCertificate,
RemoveDistributionCertificate,
SetupPushKey,
SetUpPushKey,
CreatePushKey,
UseExistingPushKey,
RemovePushKey,
SetUpAscApiKeyForSubmissions,
UseExistingAscApiKeyForSubmissions,
CreateAscApiKeyForSubmissions,
RemoveAscApiKey,
}
@@ -35,7 +35,7 @@ export const credentialsJsonActions: ActionInfo[] = [
scope: Scope.Project,
},
{
value: AndroidActionType.SetupBuildCredentialsFromCredentialsJson,
value: AndroidActionType.SetUpBuildCredentialsFromCredentialsJson,
title: 'Upload credentials from credentials.json to EAS',
scope: Scope.Project,
},
@@ -89,8 +89,8 @@ export const fcmActions: ActionInfo[] = [
export const gsaKeyActions: ActionInfo[] = [
{
value: AndroidActionType.SetupGsaKey,
title: 'Setup a Google Service Account Key',
value: AndroidActionType.SetUpGsaKey,
title: 'Set up a Google Service Account Key',
scope: Scope.Project,
},
{
@@ -12,6 +12,11 @@ export const highLevelActions: ActionInfo[] = [
title: 'Push Notifications: Manage your Apple Push Notifications Key',
scope: Scope.Manager,
},
{
value: IosActionType.ManageAscApiKey,
title: 'App Store Connect: Manage your Api Key',
scope: Scope.Manager,
},
{
value: IosActionType.ManageCredentialsJson,
title: 'credentials.json: Upload/Download credentials between EAS servers and your local json ',
@@ -31,7 +36,7 @@ export const credentialsJsonActions: ActionInfo[] = [
scope: Scope.Project,
},
{
value: IosActionType.SetupBuildCredentialsFromCredentialsJson,
value: IosActionType.SetUpBuildCredentialsFromCredentialsJson,
title: 'Upload credentials from credentials.json to EAS',
scope: Scope.Project,
},
@@ -45,8 +50,8 @@ export const credentialsJsonActions: ActionInfo[] = [
export function getPushKeyActions(ctx: CredentialsContext): ActionInfo[] {
return [
{
value: IosActionType.SetupPushKey,
title: 'Setup your project to use Push Notifications',
value: IosActionType.SetUpPushKey,
title: 'Set up your project to use Push Notifications',
scope: Scope.Project,
},
{
@@ -72,12 +77,42 @@ export function getPushKeyActions(ctx: CredentialsContext): ActionInfo[] {
];
}
export function getAscApiKeyActions(ctx: CredentialsContext): ActionInfo[] {
return [
{
value: IosActionType.SetUpAscApiKeyForSubmissions,
title: 'Set up your project to use an Api Key for EAS Submit',
scope: Scope.Project,
},
{
value: IosActionType.UseExistingAscApiKeyForSubmissions,
title: 'Use an existing Api Key for EAS Submit',
scope: Scope.Project,
},
{
value: IosActionType.CreateAscApiKeyForSubmissions,
title: 'Add a new Api Key For EAS Submit',
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account,
},
{
value: IosActionType.RemoveAscApiKey,
title: 'Delete an Api Key',
scope: Scope.Account,
},
{
value: IosActionType.GoBackToHighLevelActions,
title: 'Go back',
scope: Scope.Manager,
},
];
}
export function getBuildCredentialsActions(ctx: CredentialsContext): ActionInfo[] {
return [
{
// This command will be triggered during build to ensure all credentials are ready
// I'm leaving it here for now to simplify testing
value: IosActionType.SetupBuildCredentials,
value: IosActionType.SetUpBuildCredentials,
title: 'All: Set up all the required credentials to build your project',
scope: Scope.Project,
},
@@ -25,8 +25,8 @@ import { DownloadKeystore } from '../android/actions/DownloadKeystore';
import { RemoveFcm } from '../android/actions/RemoveFcm';
import { SelectAndRemoveGoogleServiceAccountKey } from '../android/actions/RemoveGoogleServiceAccountKey';
import { RemoveKeystore } from '../android/actions/RemoveKeystore';
import { SetupBuildCredentialsFromCredentialsJson } from '../android/actions/SetupBuildCredentialsFromCredentialsJson';
import { SetupGoogleServiceAccountKey } from '../android/actions/SetupGoogleServiceAccountKey';
import { SetUpBuildCredentialsFromCredentialsJson } from '../android/actions/SetUpBuildCredentialsFromCredentialsJson';
import { SetUpGoogleServiceAccountKey } from '../android/actions/SetUpGoogleServiceAccountKey';
import { UpdateCredentialsJson } from '../android/actions/UpdateCredentialsJson';
import { UseExistingGoogleServiceAccountKey } from '../android/actions/UseExistingGoogleServiceAccountKey';
import {
@@ -228,8 +228,8 @@ export class ManageAndroid {
}
} else if (action === AndroidActionType.RemoveGsaKey) {
await new SelectAndRemoveGoogleServiceAccountKey(appLookupParams.account).runAsync(ctx);
} else if (action === AndroidActionType.SetupGsaKey) {
await new SetupGoogleServiceAccountKey(appLookupParams).runAsync(ctx);
} else if (action === AndroidActionType.SetUpGsaKey) {
await new SetUpGoogleServiceAccountKey(appLookupParams).runAsync(ctx);
} else if (action === AndroidActionType.UpdateCredentialsJson) {
const buildCredentials = await new SelectExistingAndroidBuildCredentials(
appLookupParams
@@ -237,8 +237,8 @@ export class ManageAndroid {
if (buildCredentials) {
await new UpdateCredentialsJson().runAsync(ctx, buildCredentials);
}
} else if (action === AndroidActionType.SetupBuildCredentialsFromCredentialsJson) {
await new SetupBuildCredentialsFromCredentialsJson(appLookupParams).runAsync(ctx);
} else if (action === AndroidActionType.SetUpBuildCredentialsFromCredentialsJson) {
await new SetUpBuildCredentialsFromCredentialsJson(appLookupParams).runAsync(ctx);
}
}
}
@@ -19,20 +19,28 @@ import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
import { Account, findAccountByName } from '../../user/Account';
import { ensureActorHasUsername } from '../../user/actions';
import { CredentialsContext } from '../context';
import {
AppStoreApiKeyPurpose,
selectAscApiKeysFromAccountAsync,
} from '../ios/actions/AscApiKeyUtils';
import { AssignAscApiKey } from '../ios/actions/AssignAscApiKey';
import { AssignPushKey } from '../ios/actions/AssignPushKey';
import { getAppLookupParamsFromContext } from '../ios/actions/BuildCredentialsUtils';
import { CreateAscApiKey } from '../ios/actions/CreateAscApiKey';
import { CreateDistributionCertificate } from '../ios/actions/CreateDistributionCertificate';
import { CreatePushKey } from '../ios/actions/CreatePushKey';
import { selectValidDistributionCertificateAsync } from '../ios/actions/DistributionCertificateUtils';
import { selectPushKeyAsync } from '../ios/actions/PushKeyUtils';
import { SelectAndRemoveAscApiKey } from '../ios/actions/RemoveAscApiKey';
import { SelectAndRemoveDistributionCertificate } from '../ios/actions/RemoveDistributionCertificate';
import { RemoveProvisioningProfiles } from '../ios/actions/RemoveProvisioningProfile';
import { SelectAndRemovePushKey } from '../ios/actions/RemovePushKey';
import { SetupAdhocProvisioningProfile } from '../ios/actions/SetupAdhocProvisioningProfile';
import { SetupBuildCredentials } from '../ios/actions/SetupBuildCredentials';
import { SetupBuildCredentialsFromCredentialsJson } from '../ios/actions/SetupBuildCredentialsFromCredentialsJson';
import { SetupProvisioningProfile } from '../ios/actions/SetupProvisioningProfile';
import { SetupPushKey } from '../ios/actions/SetupPushKey';
import { SetUpAdhocProvisioningProfile } from '../ios/actions/SetUpAdhocProvisioningProfile';
import { SetUpAscApiKey } from '../ios/actions/SetUpAscApiKey';
import { SetUpBuildCredentials } from '../ios/actions/SetUpBuildCredentials';
import { SetUpBuildCredentialsFromCredentialsJson } from '../ios/actions/SetUpBuildCredentialsFromCredentialsJson';
import { SetUpProvisioningProfile } from '../ios/actions/SetUpProvisioningProfile';
import { SetUpPushKey } from '../ios/actions/SetUpPushKey';
import { UpdateCredentialsJson } from '../ios/actions/UpdateCredentialsJson';
import { AppLookupParams } from '../ios/api/GraphqlClient';
import { getManagedEntitlementsJsonAsync } from '../ios/appstore/entitlements';
@@ -42,6 +50,7 @@ import { ActionInfo, IosActionType, Scope } from './Actions';
import { Action, PressAnyKeyToContinue } from './HelperActions';
import {
credentialsJsonActions,
getAscApiKeyActions,
getBuildCredentialsActions,
getPushKeyActions,
highLevelActions,
@@ -58,9 +67,9 @@ export class ManageIos {
): Promise<void> {
const buildCredentialsActions = getBuildCredentialsActions(ctx);
const pushKeyActions = getPushKeyActions(ctx);
const ascApiKeyActions = getAscApiKeyActions(ctx);
await ctx.bestEffortAppStoreAuthenticateAsync();
const accountName = ctx.hasProjectContext
? getProjectAccountName(ctx.exp, ctx.user)
: ensureActorHasUsername(ctx.user);
@@ -70,7 +79,6 @@ export class ManageIos {
throw new Error(`You do not have access to account: ${accountName}`);
}
const { app, targets, buildProfile } = await this.createProjectContextAsync(ctx, account);
while (true) {
try {
if (ctx.hasProjectContext) {
@@ -107,6 +115,9 @@ export class ManageIos {
} else if (chosenAction === IosActionType.ManagePushKey) {
currentActions = pushKeyActions;
continue;
} else if (chosenAction === IosActionType.ManageAscApiKey) {
currentActions = ascApiKeyActions;
continue;
} else if (chosenAction === IosActionType.GoBackToHighLevelActions) {
currentActions = highLevelActions;
continue;
@@ -195,8 +206,12 @@ export class ManageIos {
await new CreateDistributionCertificate(account).runAsync(ctx);
} else if (action === IosActionType.CreatePushKey) {
await new CreatePushKey(account).runAsync(ctx);
} else if (action === IosActionType.CreateAscApiKeyForSubmissions) {
await new CreateAscApiKey(account).runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
} else if (action === IosActionType.RemovePushKey) {
await new SelectAndRemovePushKey(account).runAsync(ctx);
} else if (action === IosActionType.RemoveAscApiKey) {
await new SelectAndRemoveAscApiKey(account).runAsync(ctx);
}
}
@@ -207,8 +222,8 @@ export class ManageIos {
buildProfile: IosBuildProfile,
action: IosActionType
): Promise<void> {
if (action === IosActionType.SetupBuildCredentials) {
await new SetupBuildCredentials({
if (action === IosActionType.SetUpBuildCredentials) {
await new SetUpBuildCredentials({
app,
targets,
distribution: buildProfile.distribution,
@@ -224,8 +239,8 @@ export class ManageIos {
buildProfile
).runAsync(ctx);
if (action === IosActionType.SetupBuildCredentialsFromCredentialsJson) {
await new SetupBuildCredentialsFromCredentialsJson(app, targets, distributionType).runAsync(
if (action === IosActionType.SetUpBuildCredentialsFromCredentialsJson) {
await new SetUpBuildCredentialsFromCredentialsJson(app, targets, distributionType).runAsync(
ctx
);
return;
@@ -290,15 +305,15 @@ export class ManageIos {
}
return;
}
case IosActionType.SetupPushKey: {
const setupPushKeyAction = await new SetupPushKey(appLookupParams);
case IosActionType.SetUpPushKey: {
const setupPushKeyAction = await new SetUpPushKey(appLookupParams);
const isPushKeySetup = await setupPushKeyAction.isPushKeySetupAsync(ctx);
if (isPushKeySetup) {
Log.log(
`Push Key is already set up for ${appLookupParams.projectName} ${appLookupParams.bundleIdentifier}`
);
} else {
await new SetupPushKey(appLookupParams).runAsync(ctx);
await new SetUpPushKey(appLookupParams).runAsync(ctx);
}
return;
}
@@ -319,6 +334,43 @@ export class ManageIos {
}
return;
}
case IosActionType.SetUpAscApiKeyForSubmissions: {
await new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
).runAsync(ctx);
return;
}
case IosActionType.UseExistingAscApiKeyForSubmissions: {
const ascApiKey = await selectAscApiKeysFromAccountAsync(ctx, appLookupParams.account, {
filterDifferentAppleTeam: true,
});
if (ascApiKey) {
await new AssignAscApiKey(appLookupParams).runAsync(
ctx,
ascApiKey,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
}
return;
}
case IosActionType.CreateAscApiKeyForSubmissions: {
const ascApiKey = await new CreateAscApiKey(appLookupParams.account).runAsync(
ctx,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
const confirm = await confirmAsync({
message: `Do you want ${appLookupParams.projectName} to use the new Api Key?`,
});
if (confirm) {
await new AssignAscApiKey(appLookupParams).runAsync(
ctx,
ascApiKey,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
}
return;
}
default:
throw new Error('Unknown action selected');
}
@@ -333,11 +385,11 @@ export class ManageIos {
Log.log(`Setting up ${appLookupParams.projectName} to use Distribution Certificate`);
Log.log(`Creating provisioning profile...`);
if (distributionType === IosDistributionTypeGraphql.AdHoc) {
return await new SetupAdhocProvisioningProfile(
return await new SetUpAdhocProvisioningProfile(
appLookupParams
).runWithDistributionCertificateAsync(ctx, distCert);
} else {
return await new SetupProvisioningProfile(
return await new SetUpProvisioningProfile(
appLookupParams,
distributionType
).createAndAssignProfileAsync(ctx, distCert);
@@ -49,7 +49,7 @@ export async function askForUserProvidedAsync<T>(
export async function getCredentialsFromUserAsync<T>(
credentialsSchema: CredentialSchema<T>,
initialValues: Partial<T>
initialValues: Partial<T> = {}
): Promise<T> {
const results: any = {};
for (const question of credentialsSchema.questions) {
@@ -63,7 +63,7 @@ export async function getCredentialsFromUserAsync<T>(
: (results as T);
}
async function shouldAutoGenerateCredentialsAsync<T>(
export async function shouldAutoGenerateCredentialsAsync<T>(
schema: CredentialSchema<T>
): Promise<boolean> {
const answer = await confirmAsync({
+105 -35
View File
@@ -147,6 +147,11 @@ export type Actor = {
firstName?: Maybe<Scalars['String']>;
created: Scalars['DateTime'];
isExpoAdmin: Scalars['Boolean'];
/**
* Best-effort human readable name for this actor for use in user interfaces during action attribution.
* For example, when displaying a sentence indicating that actor X created a build or published an update.
*/
displayName: Scalars['String'];
/** Associated accounts */
accounts: Array<Account>;
/** Access Tokens belonging to this actor */
@@ -212,7 +217,6 @@ export type Account = {
applePushKeys: Array<ApplePushKey>;
appleProvisioningProfiles: Array<AppleProvisioningProfile>;
appleDevices: Array<AppleDevice>;
appleAppSpecificPasswords: Array<AppleAppSpecificPassword>;
appStoreConnectApiKeys: Array<AppStoreConnectApiKey>;
/** Android credentials for account */
googleServiceAccountKeys: Array<GoogleServiceAccountKey>;
@@ -436,6 +440,7 @@ export type App = Project & {
privacySetting: AppPrivacy;
latestReleaseId: Scalars['ID'];
pushSecurityEnabled: Scalars['Boolean'];
releaseChannels: Array<Scalars['String']>;
/** (EAS Build) Builds associated with this app */
builds: Array<Build>;
buildJobs: Array<BuildJob>;
@@ -591,6 +596,8 @@ export type AppActivityTimelineProjectActivitiesArgs = {
limit: Scalars['Int'];
createdBefore?: Maybe<Scalars['DateTime']>;
filterTypes?: Maybe<Array<ActivityTimelineProjectActivityType>>;
filterPlatforms?: Maybe<Array<AppPlatform>>;
filterReleaseChannels?: Maybe<Array<Scalars['String']>>;
};
@@ -734,6 +741,7 @@ export type User = Actor & {
appetizeCode?: Maybe<Scalars['String']>;
emailVerified: Scalars['Boolean'];
isExpoAdmin: Scalars['Boolean'];
displayName: Scalars['String'];
isSecondFactorAuthenticationEnabled: Scalars['Boolean'];
/** Get all certified second factor authentication methods */
secondFactorDevices: Array<UserSecondFactorDevice>;
@@ -1115,7 +1123,6 @@ export type IosAppCredentials = {
iosAppBuildCredentialsList: Array<IosAppBuildCredentials>;
pushKey?: Maybe<ApplePushKey>;
appStoreConnectApiKeyForSubmissions?: Maybe<AppStoreConnectApiKey>;
appSpecificPassword?: Maybe<AppleAppSpecificPassword>;
/** @deprecated use iosAppBuildCredentialsList instead */
iosAppBuildCredentialsArray: Array<IosAppBuildCredentials>;
};
@@ -1279,16 +1286,6 @@ export enum AppStoreConnectUserRole {
Unknown = 'UNKNOWN'
}
export type AppleAppSpecificPassword = {
__typename?: 'AppleAppSpecificPassword';
id: Scalars['ID'];
account: Account;
appleIdUsername: Scalars['String'];
passwordLabel?: Maybe<Scalars['String']>;
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
};
export type AndroidAppCredentialsFilter = {
legacyOnly?: Maybe<Scalars['Boolean']>;
applicationIdentifier?: Maybe<Scalars['String']>;
@@ -1475,6 +1472,8 @@ export type AddonDetails = {
id: Scalars['ID'];
planId: Scalars['String'];
name: Scalars['String'];
nextInvoice?: Maybe<Scalars['DateTime']>;
willCancel?: Maybe<Scalars['Boolean']>;
};
export type Charge = {
@@ -1550,7 +1549,10 @@ export type AppQuery = {
/** Look up app by app id */
byId: App;
byFullName: App;
/** Public apps in the app directory */
/**
* Public apps in the app directory
* @deprecated App directory no longer supported
*/
all: Array<App>;
};
@@ -1877,8 +1879,6 @@ export type RootMutation = {
googleServiceAccountKey: GoogleServiceAccountKeyMutation;
/** Mutations that modify an Identifier for an iOS App */
appleAppIdentifier: AppleAppIdentifierMutation;
/** Mutations that modify an App Specific Password for an Apple User Account */
appleAppSpecificPassword: AppleAppSpecificPasswordMutation;
/** Mutations that modify an Apple Device */
appleDevice: AppleDeviceMutation;
/** Mutations that modify an Apple Device registration request */
@@ -2278,24 +2278,6 @@ export type AppleAppIdentifierInput = {
parentAppleAppId?: Maybe<Scalars['ID']>;
};
export type AppleAppSpecificPasswordMutation = {
__typename?: 'AppleAppSpecificPasswordMutation';
/** Create an App Specific Password for an Apple User Account */
createAppleAppSpecificPassword: AppleAppSpecificPassword;
};
export type AppleAppSpecificPasswordMutationCreateAppleAppSpecificPasswordArgs = {
appleAppSpecificPasswordInput: AppleAppSpecificPasswordInput;
accountId: Scalars['ID'];
};
export type AppleAppSpecificPasswordInput = {
appleIdUsername: Scalars['String'];
passwordLabel?: Maybe<Scalars['String']>;
appSpecificPassword: Scalars['String'];
};
export type AppleDeviceMutation = {
__typename?: 'AppleDeviceMutation';
/** Create an Apple Device */
@@ -2880,6 +2862,7 @@ export type Robot = Actor & {
firstName?: Maybe<Scalars['String']>;
created: Scalars['DateTime'];
isExpoAdmin: Scalars['Boolean'];
displayName: Scalars['String'];
/** Associated accounts */
accounts: Array<Account>;
/** Access Tokens belonging to this actor */
@@ -4108,6 +4091,40 @@ export type GoogleServiceAccountKeyByAccountQuery = (
) }
);
export type CreateAppStoreConnectApiKeyMutationVariables = Exact<{
appStoreConnectApiKeyInput: AppStoreConnectApiKeyInput;
accountId: Scalars['ID'];
}>;
export type CreateAppStoreConnectApiKeyMutation = (
{ __typename?: 'RootMutation' }
& { appStoreConnectApiKey: (
{ __typename?: 'AppStoreConnectApiKeyMutation' }
& { createAppStoreConnectApiKey: (
{ __typename?: 'AppStoreConnectApiKey' }
& Pick<AppStoreConnectApiKey, 'id'>
& AppStoreConnectApiKeyFragment
) }
) }
);
export type DeleteAppStoreConnectApiKeyMutationVariables = Exact<{
appStoreConnectApiKeyId: Scalars['ID'];
}>;
export type DeleteAppStoreConnectApiKeyMutation = (
{ __typename?: 'RootMutation' }
& { appStoreConnectApiKey: (
{ __typename?: 'AppStoreConnectApiKeyMutation' }
& { deleteAppStoreConnectApiKey: (
{ __typename?: 'deleteAppStoreConnectApiKeyResult' }
& Pick<DeleteAppStoreConnectApiKeyResult, 'id'>
) }
) }
);
export type CreateAppleAppIdentifierMutationVariables = Exact<{
appleAppIdentifierInput: AppleAppIdentifierInput;
accountId: Scalars['ID'];
@@ -4411,6 +4428,24 @@ export type SetPushKeyMutation = (
) }
);
export type SetAppStoreConnectApiKeyForSubmissionsMutationVariables = Exact<{
iosAppCredentialsId: Scalars['ID'];
ascApiKeyId: Scalars['ID'];
}>;
export type SetAppStoreConnectApiKeyForSubmissionsMutation = (
{ __typename?: 'RootMutation' }
& { iosAppCredentials: (
{ __typename?: 'IosAppCredentialsMutation' }
& { setAppStoreConnectApiKeyForSubmissions: (
{ __typename?: 'IosAppCredentials' }
& Pick<IosAppCredentials, 'id'>
& CommonIosAppCredentialsFragment
) }
) }
);
export type AppByFullNameQueryVariables = Exact<{
fullName: Scalars['String'];
}>;
@@ -4428,6 +4463,27 @@ export type AppByFullNameQuery = (
)> }
);
export type AppStoreConnectApiKeyByAccountQueryVariables = Exact<{
accountName: Scalars['String'];
}>;
export type AppStoreConnectApiKeyByAccountQuery = (
{ __typename?: 'RootQuery' }
& { account: (
{ __typename?: 'AccountQuery' }
& { byName: (
{ __typename?: 'Account' }
& Pick<Account, 'id'>
& { appStoreConnectApiKeys: Array<(
{ __typename?: 'AppStoreConnectApiKey' }
& Pick<AppStoreConnectApiKey, 'id'>
& AppStoreConnectApiKeyFragment
)> }
) }
) }
);
export type AppleAppIdentifierByBundleIdQueryVariables = Exact<{
accountName: Scalars['String'];
bundleIdentifier: Scalars['String'];
@@ -5262,10 +5318,10 @@ export type BuildFragment = (
& Pick<BuildArtifacts, 'buildUrl' | 'xcodeBuildLogsUrl'>
)>, initiatingActor?: Maybe<(
{ __typename: 'User' }
& Pick<User, 'username' | 'id'>
& Pick<User, 'id' | 'displayName'>
) | (
{ __typename: 'Robot' }
& Pick<Robot, 'firstName' | 'id'>
& Pick<Robot, 'id' | 'displayName'>
)>, project: (
{ __typename: 'Snack' }
& Pick<Snack, 'id' | 'name'>
@@ -5376,6 +5432,16 @@ export type AndroidKeystoreFragment = (
& Pick<AndroidKeystore, 'id' | 'type' | 'keystore' | 'keystorePassword' | 'keyAlias' | 'keyPassword' | 'md5CertificateFingerprint' | 'sha1CertificateFingerprint' | 'sha256CertificateFingerprint' | 'createdAt' | 'updatedAt'>
);
export type AppStoreConnectApiKeyFragment = (
{ __typename?: 'AppStoreConnectApiKey' }
& Pick<AppStoreConnectApiKey, 'id' | 'issuerIdentifier' | 'keyIdentifier' | 'name' | 'roles' | 'createdAt' | 'updatedAt'>
& { appleTeam?: Maybe<(
{ __typename?: 'AppleTeam' }
& Pick<AppleTeam, 'id'>
& AppleTeamFragment
)> }
);
export type AppleAppIdentifierFragment = (
{ __typename?: 'AppleAppIdentifier' }
& Pick<AppleAppIdentifier, 'id' | 'bundleIdentifier'>
@@ -5505,6 +5571,10 @@ export type CommonIosAppCredentialsWithoutBuildCredentialsFragment = (
{ __typename?: 'ApplePushKey' }
& Pick<ApplePushKey, 'id'>
& ApplePushKeyFragment
)>, appStoreConnectApiKeyForSubmissions?: Maybe<(
{ __typename?: 'AppStoreConnectApiKey' }
& Pick<AppStoreConnectApiKey, 'id'>
& AppStoreConnectApiKeyFragment
)> }
);
+1 -6
View File
@@ -17,12 +17,7 @@ export const BuildFragmentNode = gql`
initiatingActor {
__typename
id
... on User {
username
}
... on Robot {
firstName
}
displayName
}
project {
__typename
@@ -0,0 +1,21 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { AppleTeamFragmentNode } from './AppleTeam';
export const AppStoreConnectApiKeyFragmentNode = gql`
fragment AppStoreConnectApiKeyFragment on AppStoreConnectApiKey {
id
appleTeam {
id
...AppleTeamFragment
}
issuerIdentifier
keyIdentifier
name
roles
createdAt
updatedAt
}
${print(AppleTeamFragmentNode)}
`;
@@ -1,6 +1,7 @@
import gql from 'graphql-tag';
import { AppFragmentNode } from '../App';
import { AppStoreConnectApiKeyFragmentNode } from './AppStoreConnectApiKey';
import { AppleAppIdentifierFragmentNode } from './AppleAppIdentifier';
import { ApplePushKeyFragmentNode } from './ApplePushKey';
import { AppleTeamFragmentNode } from './AppleTeam';
@@ -25,11 +26,16 @@ export const CommonIosAppCredentialsWithoutBuildCredentialsFragmentNode = gql`
id
...ApplePushKeyFragment
}
appStoreConnectApiKeyForSubmissions {
id
...AppStoreConnectApiKeyFragment
}
}
${AppFragmentNode}
${AppleTeamFragmentNode}
${AppleAppIdentifierFragmentNode}
${ApplePushKeyFragmentNode}
${AppStoreConnectApiKeyFragmentNode}
`;
export const CommonIosAppCredentialsFragmentNode = gql`
@@ -288,6 +288,7 @@ function formatBuildChoice(build: BuildFragment, expiryDate: Date): prompts.Choi
buildProfile,
appBuildVersion,
releaseChannel,
initiatingActor,
} = build;
const formatValue = (field?: string | null): string =>
@@ -310,6 +311,7 @@ function formatBuildChoice(build: BuildFragment, expiryDate: Date): prompts.Choi
.filter(it => it != null)
.join(', '),
`\tProfile: ${formatValue(buildProfile)}, Release channel: ${formatValue(releaseChannel)}`,
`\tAuthored by: ${formatValue(initiatingActor?.displayName)}`,
].join('\n');
return {
@@ -3,7 +3,7 @@ import chalk from 'chalk';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { SetupGoogleServiceAccountKey } from '../../credentials/android/actions/SetupGoogleServiceAccountKey';
import { SetUpGoogleServiceAccountKey } from '../../credentials/android/actions/SetUpGoogleServiceAccountKey';
import { readAndValidateServiceAccountKey } from '../../credentials/android/utils/googleServiceAccountKey';
import Log, { learnMore } from '../../log';
import { promptAsync } from '../../prompts';
@@ -109,7 +109,7 @@ export async function getServiceAccountFromCredentialsServiceAsync(
projectName: ctx.projectName,
androidApplicationIdentifier,
};
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
const androidAppCredentials = await setupGoogleServiceAccountKeyAction.runAsync(
ctx.credentialsCtx
);
@@ -6,7 +6,7 @@ import { v4 as uuidv4 } from 'uuid';
import { asMock } from '../../../__tests__/utils';
import { testAndroidAppCredentialsFragment } from '../../../credentials/__tests__/fixtures-android';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { SetupGoogleServiceAccountKey } from '../../../credentials/android/actions/SetupGoogleServiceAccountKey';
import { SetUpGoogleServiceAccountKey } from '../../../credentials/android/actions/SetUpGoogleServiceAccountKey';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { promptAsync } from '../../../prompts';
import { createSubmissionContextAsync } from '../../context';
@@ -20,7 +20,7 @@ import {
jest.mock('fs');
jest.mock('../../../prompts');
jest.mock('../../../project/projectUtils');
jest.mock('../../../credentials/android/actions/SetupGoogleServiceAccountKey');
jest.mock('../../../credentials/android/actions/SetUpGoogleServiceAccountKey');
jest.mock('../../../user/User', () => ({
getUserAsync: jest.fn(() => mockJester),
}));
@@ -52,7 +52,7 @@ beforeAll(() => {
'/other_dir/invalid_file.txt': 'this is not even a JSON',
});
jest
.spyOn(SetupGoogleServiceAccountKey.prototype, 'runAsync')
.spyOn(SetUpGoogleServiceAccountKey.prototype, 'runAsync')
.mockImplementation(async () => testAndroidAppCredentialsFragment);
});
afterAll(() => {
@@ -1,12 +1,5 @@
import chalk from 'chalk';
import wrapAnsi from 'wrap-ansi';
import Log, { learnMore } from '../../log';
import { promptAsync } from '../../prompts';
export enum AppSpecificPasswordSourceType {
userDefined,
prompt,
}
interface AppSpecificPasswordSourceBase {
@@ -18,38 +11,13 @@ interface AppSpecificPasswordUserDefinedSource extends AppSpecificPasswordSource
appSpecificPassword: string;
}
interface AppSpecificPasswordPromptSource extends AppSpecificPasswordSourceBase {
sourceType: AppSpecificPasswordSourceType.prompt;
}
export type AppSpecificPasswordSource =
| AppSpecificPasswordUserDefinedSource
| AppSpecificPasswordPromptSource;
export type AppSpecificPasswordSource = AppSpecificPasswordUserDefinedSource;
export async function getAppSpecificPasswordAsync(
source: AppSpecificPasswordSource
): Promise<string> {
if (source.sourceType === AppSpecificPasswordSourceType.userDefined) {
return source.appSpecificPassword;
} else if (source.sourceType === AppSpecificPasswordSourceType.prompt) {
Log.addNewLineIfNone();
Log.log(
wrapAnsi(
`Please enter your Apple app-specific password. You can also provide it by using the ${chalk.bold(
'EXPO_APPLE_APP_SPECIFIC_PASSWORD'
)} environment variable.`,
process.stdout.columns || 80
)
);
Log.log(learnMore('https://expo.fyi/apple-app-specific-password'));
const { appSpecificPassword } = await promptAsync({
name: 'appSpecificPassword',
message: 'Your Apple app-specific password:',
type: 'password',
validate: (val: string) => val !== '' || 'Apple app-specific password cannot be empty!',
});
return appSpecificPassword;
} else {
// exhaustive -- should never happen
throw new Error(`Unknown app specific password source type "${(source as any)?.sourceType}"`);
@@ -1,19 +1,33 @@
import { Platform } from '@expo/eas-build-job';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { promptForAscApiKeyAsync } from '../../credentials/ios/actions/AscApiKeyUtils';
import {
AppStoreApiKeyPurpose,
promptForAscApiKeyPathAsync,
} from '../../credentials/ios/actions/AscApiKeyUtils';
import { SetUpAscApiKey } from '../../credentials/ios/actions/SetUpAscApiKey';
import { AscApiKeyPath, MinimalAscApiKey } from '../../credentials/ios/credentials';
import Log from '../../log';
import { getBundleIdentifierAsync } from '../../project/ios/bundleIdentifier';
import { findAccountByName } from '../../user/Account';
import { SubmissionContext } from '../context';
import { isExistingFileAsync } from '../utils/files';
export enum AscApiKeySourceType {
path,
prompt,
credentialsService,
}
interface AscApiKeySourceBase {
sourceType: AscApiKeySourceType;
}
interface AscApiKeyCredentialsServiceSource extends AscApiKeySourceBase {
sourceType: AscApiKeySourceType.credentialsService;
}
interface AscApiKeyPromptSource extends AscApiKeySourceBase {
sourceType: AscApiKeySourceType.prompt;
}
@@ -23,21 +37,80 @@ interface AscApiKeyEnvVarSource extends AscApiKeySourceBase {
path: AscApiKeyPath;
}
export type AscApiKeySource = AscApiKeyEnvVarSource | AscApiKeyPromptSource;
export type AscApiKeySource =
| AscApiKeyEnvVarSource
| AscApiKeyPromptSource
| AscApiKeyCredentialsServiceSource;
type AscApiKeySummary = {
source: 'local' | 'EAS servers';
path?: string;
keyId: string;
name?: string;
};
export type AscApiKeyFromExpoServers = {
ascApiKeyId: string;
};
export type AscApiKeyResult = {
result: MinimalAscApiKey;
result: MinimalAscApiKey | AscApiKeyFromExpoServers;
summary: AscApiKeySummary;
};
export async function getAscApiKeyLocallyAsync(source: AscApiKeySource): Promise<AscApiKeyResult> {
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
export async function getAscApiKeyResultAsync(
ctx: SubmissionContext<Platform.IOS>,
source: AscApiKeySource
): Promise<AscApiKeyResult> {
if (source.sourceType === AscApiKeySourceType.credentialsService) {
return await getAscApiKeyFromCredentialsServiceAsync(ctx);
} else {
return await getAscApiKeyLocallyAsync(ctx, source);
}
}
async function getAscApiKeyFromCredentialsServiceAsync(
ctx: SubmissionContext<Platform.IOS>
): Promise<AscApiKeyResult> {
const bundleIdentifier = await getBundleIdentifierAsync(ctx.projectDir, ctx.exp);
Log.log(`Looking up credentials configuration for ${bundleIdentifier}...`);
const appLookupParams = {
account: nullthrows(
findAccountByName(ctx.user.accounts, ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.projectName,
bundleIdentifier,
};
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
const iosAppCredentials = await setupAscApiKeyAction.runAsync(ctx.credentialsCtx);
const ascKeyForSubmissions = nullthrows(
iosAppCredentials.appStoreConnectApiKeyForSubmissions,
`An EAS Submit ASC Api Key could not be found for ${iosAppCredentials.appleAppIdentifier.bundleIdentifier}`
);
const { id, keyIdentifier, name } = ascKeyForSubmissions;
Log.log(`Using Api Key ID: ${keyIdentifier}${name ? ` (${name})` : ''}`);
return {
result: {
ascApiKeyId: id,
},
summary: {
source: 'EAS servers',
keyId: keyIdentifier,
name: name ?? undefined,
},
};
}
async function getAscApiKeyLocallyAsync(
ctx: SubmissionContext<Platform.IOS>,
source: AscApiKeySource
): Promise<AscApiKeyResult> {
const ascApiKeyPath = await getAscApiKeyPathAsync(ctx, source);
const { keyP8Path, keyId, issuerId } = ascApiKeyPath;
const keyP8 = await fs.readFile(keyP8Path, 'utf-8');
@@ -51,27 +124,38 @@ export async function getAscApiKeyLocallyAsync(source: AscApiKeySource): Promise
};
}
export async function getAscApiKeyPathAsync(source: AscApiKeySource): Promise<AscApiKeyPath> {
export async function getAscApiKeyPathAsync(
ctx: SubmissionContext<Platform.IOS>,
source: AscApiKeySource
): Promise<AscApiKeyPath> {
switch (source.sourceType) {
case AscApiKeySourceType.path:
return await handlePathSourceAsync(source);
return await handlePathSourceAsync(ctx, source);
case AscApiKeySourceType.prompt:
return await handlePromptSourceAsync(source);
return await handlePromptSourceAsync(ctx, source);
case AscApiKeySourceType.credentialsService:
throw new Error(`AscApiKeySourceType ${source} does not return a path.`);
}
}
async function handlePathSourceAsync(source: AscApiKeyEnvVarSource): Promise<AscApiKeyPath> {
async function handlePathSourceAsync(
ctx: SubmissionContext<Platform.IOS>,
source: AscApiKeyEnvVarSource
): Promise<AscApiKeyPath> {
const { keyP8Path } = source.path;
if (!(await isExistingFileAsync(keyP8Path))) {
Log.warn(`File ${keyP8Path} doesn't exist.`);
return await getAscApiKeyPathAsync({ sourceType: AscApiKeySourceType.prompt });
return await getAscApiKeyPathAsync(ctx, { sourceType: AscApiKeySourceType.prompt });
}
return source.path;
}
async function handlePromptSourceAsync(_source: AscApiKeyPromptSource): Promise<AscApiKeyPath> {
const ascApiKeyPath = await promptForAscApiKeyAsync();
return await getAscApiKeyPathAsync({
async function handlePromptSourceAsync(
ctx: SubmissionContext<Platform.IOS>,
_source: AscApiKeyPromptSource
): Promise<AscApiKeyPath> {
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx.credentialsCtx);
return await getAscApiKeyPathAsync(ctx, {
sourceType: AscApiKeySourceType.path,
path: ascApiKeyPath,
});
@@ -34,15 +34,17 @@ export default class IosSubmitCommand {
| { appSpecificPasswordSource: Result<AppSpecificPasswordSource> }
| { ascApiKeySource: Result<AscApiKeySource> }
> {
// Fall back to app specific password if no ascApiKey defined
const ascApiKeySource = this.resolveAscApiKeySource();
const shouldUseAppSpecificPassword =
!ascApiKeySource.ok && ascApiKeySource.enforceError() instanceof MissingCredentialsError;
if (shouldUseAppSpecificPassword) {
// if an App Specific Password env var is not specified, use ASC Api Key
const appSpecificPasswordSource = this.resolveAppSpecificPasswordSource();
const shouldSkipAppSpecificPasswordSource =
!appSpecificPasswordSource.ok &&
appSpecificPasswordSource.enforceError() instanceof MissingCredentialsError;
if (!shouldSkipAppSpecificPasswordSource) {
return { appSpecificPasswordSource: this.resolveAppSpecificPasswordSource() };
} else {
return { ascApiKeySource };
}
const ascApiKeySource = this.resolveAscApiKeySource();
return { ascApiKeySource };
}
private async resolveSubmissionOptionsAsync(): Promise<IosSubmissionOptions> {
@@ -96,13 +98,13 @@ export default class IosSubmitCommand {
sourceType: AppSpecificPasswordSourceType.userDefined,
appSpecificPassword: envAppSpecificPassword,
});
} else if (this.ctx.nonInteractive) {
return result(new Error('Set the EXPO_APPLE_APP_SPECIFIC_PASSWORD environment variable.'));
} else {
return result({
sourceType: AppSpecificPasswordSourceType.prompt,
});
}
return result(
new MissingCredentialsError(
'The EXPO_APPLE_APP_SPECIFIC_PASSWORD environment variable must be set.'
)
);
}
private resolveAscApiKeySource(): Result<AscApiKeySource> {
@@ -127,11 +129,7 @@ export default class IosSubmitCommand {
});
}
return result(
new MissingCredentialsError(
'Set the ascApiKeyPath, ascApiKeyIssuerId and ascApiKeyId fields in eas.json.'
)
);
return result({ sourceType: AscApiKeySourceType.credentialsService });
}
private resolveArchiveSource(): Result<ArchiveSource> {
+34 -20
View File
@@ -1,6 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import { MinimalAscApiKey } from '../../credentials/ios/credentials';
import { IosSubmissionConfigInput, SubmissionFragment } from '../../graphql/generated';
import { SubmissionMutation } from '../../graphql/mutations/SubmissionMutation';
import formatFields from '../../utils/formatFields';
@@ -15,7 +16,12 @@ import {
AppSpecificPasswordSource,
getAppSpecificPasswordAsync,
} from './AppSpecificPasswordSource';
import { AscApiKeyResult, AscApiKeySource, getAscApiKeyLocallyAsync } from './AscApiKeySource';
import {
AscApiKeyFromExpoServers,
AscApiKeyResult,
AscApiKeySource,
getAscApiKeyResultAsync,
} from './AscApiKeySource';
export interface IosSubmissionOptions
extends Pick<IosSubmissionConfigInput, 'appleIdUsername' | 'ascAppIdentifier'> {
@@ -69,7 +75,7 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
? await getAppSpecificPasswordAsync(this.options.appSpecificPasswordSource)
: null;
const maybeAppStoreConnectApiKey = this.options.ascApiKeySource
? await getAscApiKeyLocallyAsync(this.options.ascApiKeySource)
? await getAscApiKeyResultAsync(this.ctx, this.options.ascApiKeySource)
: null;
return {
archive,
@@ -88,18 +94,24 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
appleIdUsername,
archiveUrl: archive.url,
appleAppSpecificPassword: appSpecificPassword,
...(ascApiKeyResult?.result
? {
ascApiKey: {
keyP8: ascApiKeyResult?.result.keyP8,
keyIdentifier: ascApiKeyResult?.result.keyId,
issuerIdentifier: ascApiKeyResult?.result.issuerId,
},
}
: null),
...(ascApiKeyResult?.result ? this.formatAscApiKeyResult(ascApiKeyResult.result) : null),
};
}
private formatAscApiKeyResult(
result: MinimalAscApiKey | AscApiKeyFromExpoServers
): Pick<IosSubmissionConfigInput, 'ascApiKey'> | Pick<IosSubmissionConfigInput, 'ascApiKeyId'> {
return 'ascApiKeyId' in result
? { ascApiKeyId: result.ascApiKeyId }
: {
ascApiKey: {
keyP8: result.keyP8,
keyIdentifier: result.keyId,
issuerIdentifier: result.issuerId,
},
};
}
private prepareSummaryData(
options: IosSubmissionOptions,
{ archive, ascApiKeyResult }: ResolvedSourceOptions
@@ -111,9 +123,7 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
ascAppIdentifier,
appleIdUsername: appleIdUsername ?? undefined,
projectId,
...(ascApiKeyResult
? { formattedAscApiKey: formatServiceAccountSummary(ascApiKeyResult) }
: null),
...(ascApiKeyResult ? { formattedAscApiKey: formatAscApiKeySummary(ascApiKeyResult) } : null),
...formatArchiveSourceSummary(archive),
};
}
@@ -136,10 +146,18 @@ const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
formattedAscApiKey: 'App Store Connect Api Key',
};
function formatServiceAccountSummary({ summary }: AscApiKeyResult): string {
const { source, path, keyId } = summary;
function formatAscApiKeySummary({ summary }: AscApiKeyResult): string {
const { source, path, keyId, name } = summary;
const fields = [
{
label: 'Key Name',
value: name,
},
{
label: 'Key ID',
value: keyId,
},
{
label: 'Key Source',
value: source,
@@ -148,10 +166,6 @@ function formatServiceAccountSummary({ summary }: AscApiKeyResult): string {
label: 'Key Path',
value: path,
},
{
label: 'Key ID',
value: keyId,
},
];
const filteredFields = fields.filter(({ value }) => value !== undefined && value !== null) as {
@@ -1,18 +1,59 @@
import { Platform } from '@expo/eas-build-job';
import { vol } from 'memfs';
import { v4 as uuidv4 } from 'uuid';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { testCommonIosAppCredentialsFragment } from '../../../credentials/__tests__/fixtures-ios';
import { SetUpAscApiKey } from '../../../credentials/ios/actions/SetUpAscApiKey';
import { getCredentialsFromUserAsync } from '../../../credentials/utils/promptForCredentials';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { getBundleIdentifierAsync } from '../../../project/ios/bundleIdentifier';
import { promptAsync } from '../../../prompts';
import { SubmissionContext, createSubmissionContextAsync } from '../../context';
import {
AscApiKeySource,
AscApiKeySourceType,
getAscApiKeyLocallyAsync,
getAscApiKeyPathAsync,
getAscApiKeyResultAsync,
} from '../AscApiKeySource';
jest.mock('fs');
jest.mock('../../../prompts');
jest.mock('../../../credentials/utils/promptForCredentials');
jest.mock('../../../user/User', () => ({
getUserAsync: jest.fn(() => mockJester),
}));
jest.mock('../../../user/Account', () => ({
findAccountByName: jest.fn(() => mockJester.accounts[0]),
}));
jest.mock('../../../project/ios/bundleIdentifier');
const testProject = createTestProject(mockJester, {
android: {
package: 'com.expo.test.project',
},
});
const mockManifest = { exp: testProject.appJSON.expo };
jest.mock('@expo/config', () => ({
getConfig: jest.fn(() => mockManifest),
}));
const projectId = uuidv4();
async function getIosSubmissionContextAsync(): Promise<SubmissionContext<Platform.IOS>> {
return await createSubmissionContextAsync({
platform: Platform.IOS,
projectDir: testProject.projectRoot,
projectId,
archiveFlags: {
url: 'http://expo.dev/fake.apk',
},
profile: {
language: 'en-US',
},
nonInteractive: true,
});
}
beforeAll(() => {
vol.fromJSON({
@@ -35,15 +76,16 @@ describe(getAscApiKeyPathAsync, () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementationOnce(() => ({
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const ctx = await getIosSubmissionContextAsync();
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/doesnt-exist.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
const ascApiKeyPath = await getAscApiKeyPathAsync(ctx, source);
expect(promptAsync).toHaveBeenCalled();
expect(ascApiKeyPath).toEqual({
keyP8Path: '/asc-api-key.p8',
@@ -53,20 +95,22 @@ describe(getAscApiKeyPathAsync, () => {
});
it("doesn't prompt for path if the provided file exists", async () => {
const ctx = await getIosSubmissionContextAsync();
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
await getAscApiKeyPathAsync(source);
await getAscApiKeyPathAsync(ctx, source);
expect(promptAsync).not.toHaveBeenCalled();
});
it('returns the provided file path if the file exists', async () => {
const ctx = await getIosSubmissionContextAsync();
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
const ascApiKeyPath = await getAscApiKeyPathAsync(ctx, source);
expect(ascApiKeyPath).toEqual({
issuerId: 'test-issuer-id',
keyId: 'test-key-id',
@@ -80,14 +124,15 @@ describe(getAscApiKeyPathAsync, () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementationOnce(() => ({
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const ctx = await getIosSubmissionContextAsync();
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.prompt,
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
const ascApiKeyPath = await getAscApiKeyPathAsync(ctx, source);
expect(promptAsync).toHaveBeenCalled();
expect(ascApiKeyPath).toEqual({
keyP8Path: '/asc-api-key.p8',
@@ -111,10 +156,11 @@ describe(getAscApiKeyPathAsync, () => {
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const ctx = await getIosSubmissionContextAsync();
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.prompt,
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
const ascApiKeyPath = await getAscApiKeyPathAsync(ctx, source);
expect(promptAsync).toHaveBeenCalledTimes(3);
expect(ascApiKeyPath).toEqual({
keyP8Path: '/asc-api-key.p8',
@@ -125,13 +171,14 @@ describe(getAscApiKeyPathAsync, () => {
});
});
describe(getAscApiKeyLocallyAsync, () => {
describe(getAscApiKeyResultAsync, () => {
it('returns a local Asc Api Key file with a AscApiKeySourceType.path source', async () => {
const ctx = await getIosSubmissionContextAsync();
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
const ascApiKeyResult = await getAscApiKeyLocallyAsync(source);
const ascApiKeyResult = await getAscApiKeyResultAsync(ctx, source);
expect(ascApiKeyResult).toMatchObject({
result: {
keyP8: 'super secret',
@@ -154,10 +201,11 @@ describe(getAscApiKeyLocallyAsync, () => {
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const ctx = await getIosSubmissionContextAsync();
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.prompt,
};
const serviceAccountResult = await getAscApiKeyLocallyAsync(source);
const serviceAccountResult = await getAscApiKeyResultAsync(ctx, source);
expect(serviceAccountResult).toMatchObject({
result: {
keyP8: 'super secret',
@@ -171,4 +219,39 @@ describe(getAscApiKeyLocallyAsync, () => {
},
});
});
it('returns an Asc Api Key from server with a AscApiKeySourceType.credentialService source', async () => {
const ctx = await createSubmissionContextAsync({
platform: Platform.IOS,
projectDir: testProject.projectRoot,
projectId,
archiveFlags: {
url: 'http://expo.dev/fake.apk',
},
profile: {
language: 'en-US',
},
nonInteractive: true,
});
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.credentialsService,
};
jest
.spyOn(SetUpAscApiKey.prototype, 'runAsync')
.mockImplementation(async _ctx => testCommonIosAppCredentialsFragment);
asMock(getBundleIdentifierAsync).mockImplementation(() => 'com.hello.world');
const result = await getAscApiKeyResultAsync(ctx, source);
expect(result).toEqual({
result: {
ascApiKeyId: testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.id,
},
summary: {
keyId:
testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.keyIdentifier,
name: testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.name,
source: 'EAS servers',
},
});
});
});
@@ -16,6 +16,7 @@ enum SubmissionErrorCode {
IOS_INVALID_SIGNATURE = 'SUBMISSION_SERVICE_IOS_INVALID_SIGNATURE',
IOS_INCORRECT_CREDENTIALS = 'SUBMISSION_SERVICE_IOS_INVALID_CREDENTIALS',
IOS_IPAD_INVALID_ORIENTATION = 'SUBMISSION_SERVICE_IOS_IPAD_INVALID_ORIENTATION',
IOS_APPLE_MAINTENANCE = 'SUBMISSION_SERVICE_IOS_APPLE_MAINTENANCE',
}
const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
@@ -62,6 +63,8 @@ const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
"Your app doesn't support iPad multitasking and has to require full screen.\n" +
"If you're submitting a managed Expo project, set the `expo.ios.requireFullScreen` to true in app.json and build the project again.\n" +
`${learnMore('https://expo.fyi/ipad-requires-fullscreen')}`,
[SubmissionErrorCode.IOS_APPLE_MAINTENANCE]:
'It looks like Apple servers are undergoing an unscheduled maintenance. Please try again later.',
};
export function printSubmissionError(error: SubmissionError): boolean {
+5 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with the eas.json",
"version": "0.34.0",
"description": "A library for interacting with eas.json",
"version": "0.34.1",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
@@ -24,13 +24,15 @@
"homepage": "https://github.com/expo/eas-cli",
"license": "MIT",
"main": "build/index.js",
"types": "build/index.d.ts",
"repository": "expo/eas-cli",
"scripts": {
"build": "tsc --project tsconfig.build.json",
"watch": "yarn build --watch --preserveWatchOutput",
"typecheck": "tsc",
"prepack": "rm -rf build && yarn build",
"test": "jest"
"test": "jest",
"clean": "rm -rf build node_modules yarn-error.log"
},
"publishConfig": {
"access": "public"
Generated
+4 -4
View File
@@ -833,10 +833,10 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
"@expo/[email protected]5":
version "0.0.0-alpha.25"
resolved "https://registry.yarnpkg.com/@expo/apple-utils/-/apple-utils-0.0.0-alpha.25.tgz#fea4f5a409cc1bf79a0d2acec3eca52dbd8443c1"
integrity sha512-meyCJ/5jHJsgZNBMwlAugqD5Z0bcazCLgAyYQCw8O1/sXh4gWf0IiWJGnkevdxnA3g1R9nmJFmjkFuLoGOt+Bw==
"@expo/[email protected]6":
version "0.0.0-alpha.26"
resolved "https://registry.yarnpkg.com/@expo/apple-utils/-/apple-utils-0.0.0-alpha.26.tgz#cf5a893b7c9cc779af5b54a482196325dcde1426"
integrity sha512-t+xOhn4bYSAXkXamhDPUiI2Ol+QIwHRHLn/2QiCmNAGHolaVan/hMaVveSzvCYitpaJ16b4nthvcWFoJipxGlA==
"@expo/bunyan@^4.0.0":
version "4.0.0"