mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc0b3b1237 | ||
|
|
0e6254453a | ||
|
|
587a29892f | ||
|
|
db8245d954 | ||
|
|
0088306bca | ||
|
|
b6fc718713 | ||
|
|
18e55dafd4 | ||
|
|
8e013ab793 | ||
|
|
b88e79f0f3 | ||
|
|
35b505120d | ||
|
|
6807d3080b | ||
|
|
0d1cfb3286 | ||
|
|
0b9a5621fe | ||
|
|
9bd15af02a | ||
|
|
aab3bf1330 | ||
|
|
9e95218523 | ||
|
|
bb98e6de03 | ||
|
|
2604228233 | ||
|
|
a7035d3469 | ||
|
|
f6dce0e17c | ||
|
|
ccfa6f9fc3 | ||
|
|
0ee8e2f967 | ||
|
|
94dc4c6e93 | ||
|
|
4144e387d6 | ||
|
|
de4e05e1e0 |
No files matched your search
+33
-2
@@ -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
@@ -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.
|
||||

|
||||
- Run `npm dist-tags add eas-cli-local-build-plugin@VERSION latest` with the version from the previous step.
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.34.0"
|
||||
"version": "0.34.1"
|
||||
}
|
||||
+2
-1
@@ -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",
|
||||
|
||||
@@ -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 -->
|
||||
Generated
+138
-272
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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> {
|
||||
+1
-1
@@ -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> {
|
||||
+1
-1
@@ -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> {
|
||||
+5
-5
@@ -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
|
||||
);
|
||||
+5
-5
@@ -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();
|
||||
+5
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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,
|
||||
+3
-3
@@ -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,
|
||||
+1
-1
@@ -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) {}
|
||||
+5
-5
@@ -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
|
||||
);
|
||||
}
|
||||
+3
-3
@@ -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);
|
||||
+1
-1
@@ -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
-8
@@ -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
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -22,7 +22,7 @@ import {
|
||||
getProvisioningProfileAsync,
|
||||
} from './BuildCredentialsUtils';
|
||||
|
||||
export class SetupTargetBuildCredentialsFromCredentialsJson {
|
||||
export class SetUpTargetBuildCredentialsFromCredentialsJson {
|
||||
constructor(
|
||||
private app: AppLookupParams,
|
||||
private distributionType: IosDistributionType,
|
||||
+1
-1
@@ -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
-1
@@ -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();
|
||||
});
|
||||
});
|
||||
+28
-28
@@ -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);
|
||||
|
||||
+8
-8
@@ -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
|
||||
);
|
||||
+6
-6
@@ -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();
|
||||
});
|
||||
});
|
||||
+6
-6
@@ -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}`;
|
||||
+75
@@ -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()
|
||||
);
|
||||
},
|
||||
};
|
||||
+34
@@ -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;
|
||||
},
|
||||
};
|
||||
+41
@@ -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;
|
||||
},
|
||||
};
|
||||
+15
@@ -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',
|
||||
|
||||
+50
-42
@@ -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
@@ -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
|
||||
)> }
|
||||
);
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user