Compare commits

..
Author SHA1 Message Date
Dominik Sokal 9b36ae0ef8 convert to ESM 2021-10-11 16:15:21 +02:00
Dominik Sokal 1a4cb107af add clean script 2021-10-11 16:12:42 +02:00
151 changed files with 3352 additions and 5466 deletions

No files matched your search

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