Compare commits

..
Author SHA1 Message Date
Quinlan Jung dc0b3b1237 [eas-cli] remove asp prompt 2021-11-05 16:15:33 -07:00
68 changed files with 788 additions and 2009 deletions

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

+1
View File
@@ -3,6 +3,7 @@
# Checklist
- [ ] I've added an entry to [CHANGELOG.md](https://github.com/expo/eas-cli/blob/main/CHANGELOG.md#main) if necessary. You can comment this pull request with `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]` and CHANGELOG.md will be updated automatically.
- [ ] I've tagged the changelog entry with `[EAS BUILD API]` if it's a part of a breaking change to EAS Build API (only possible when updating `@expo/eas-build-job` package).
# Why
+22 -54
View File
@@ -8,56 +8,24 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
### 🐛 Bug fixes
- Fix `dateformat` import issue when building with `--local` flag. ([a520d](https://github.com/expo/eas-cli/commit/a520d1750736d61d6489da6487879f35ed3deb23) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
## [0.36.0](https://github.com/expo/eas-cli/releases/tag/v0.36.0) - 2021-11-15
### 🛠 Breaking changes
- Do not require manual `eas-cli-local-build-plugin` installation. An existing global installation (either with `yarn global add` or `npm install -g`) will not be used anymore. ([#753](https://github.com/expo/eas-cli/pull/753) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Use Keystore Service when `keytool` is not installed. ([#754](https://github.com/expo/eas-cli/pull/754) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix auto-submitting with `--auto-submit-with-profile`. ([#748](https://github.com/expo/eas-cli/pull/748) by [@dsokal](https://github.com/dsokal))
- Pass env from build profile when resolving entitlements. ([#751](https://github.com/expo/eas-cli/pull/751) by [@wkozyra95](https://github.com/wkozyra95))
- Only prompt for Apple Id username if authenticating with an App Specific Password. ([#745](https://github.com/expo/eas-cli/pull/745) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Use same URL paths for local development of EAS CLI as production. ([#750](https://github.com/expo/eas-cli/pull/750) by [@ide](https://github.com/ide))
- Grammar: Api -> API. ([#755](https://github.com/expo/eas-cli/pull/755) by [@quinlanj](https://github.com/quinlanj))
- Add analytics for EAS Submit. ([#752](https://github.com/expo/eas-cli/pull/752) by [@quinlanj](https://github.com/quinlanj))
- Add an 'attempt' event to build and submit analytics. ([#757](https://github.com/expo/eas-cli/pull/757) by [@quinlanj](https://github.com/quinlanj))
## [0.35.0](https://github.com/expo/eas-cli/releases/tag/v0.35.0) - 2021-11-08
### 🎉 New features
- Add ASC API Key generation workflow. ([#718](https://github.com/expo/eas-cli/pull/718) by [@quinlanj](https://github.com/quinlanj))
- Add support for removal of ASC API Keys. ([#721](https://github.com/expo/eas-cli/pull/721) by [@quinlanj](https://github.com/quinlanj))
- Allow users to assign an ASC API Key to their project. ([#719](https://github.com/expo/eas-cli/pull/719) by [@quinlanj](https://github.com/quinlanj))
- Add setup support for ASC API Keys. ([#733](https://github.com/expo/eas-cli/pull/733) by [@quinlanj](https://github.com/quinlanj))
- Add ASC Api Key generation workflow. ([#718](https://github.com/expo/eas-cli/pull/718) by [@quinlanj](https://github.com/quinlanj))
- Add support for removal of ASC Api Keys. ([#721](https://github.com/expo/eas-cli/pull/721) by [@quinlanj](https://github.com/quinlanj))
- Allow users to assign an ASC Api Key to their project. ([#719](https://github.com/expo/eas-cli/pull/719) by [@quinlanj](https://github.com/quinlanj))
- Add setup support for ASC Api Keys. ([#733](https://github.com/expo/eas-cli/pull/733) by [@quinlanj](https://github.com/quinlanj))
- Show initiating user display name when selecting a build to submit. ([#730](https://github.com/expo/eas-cli/pull/730) by [@barthap](https://github.com/barthap))
- Handle Apple servers maintenance error in `eas submit`. ([#738](https://github.com/expo/eas-cli/pull/738) by [@barthap](https://github.com/barthap))
- Integrate ASC API Key with submissions workflow. ([#737](https://github.com/expo/eas-cli/pull/737) by [@quinlanj](https://github.com/quinlanj))
- Change EAS API server domain. ([#744](https://github.com/expo/eas-cli/pull/744) by [@ide](https://github.com/ide))
- Integrate ASC Api Key with submissions workflow. ([#737](https://github.com/expo/eas-cli/pull/737) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
### 🧹 Chores
- Clean up credentials prompt method. ([#728](https://github.com/expo/eas-cli/pull/728) by [@quinlanj](https://github.com/quinlanj))
- Bump @expo/apple-utils to 0.0.0-alpha.26. ([#723](https://github.com/expo/eas-cli/pull/723) by [@brentvatne](https://github.com/brentvatne))
- Bump @expo/apple-utils to 0.0.0-alpha.26. ([#723](https://github.com/expo/eas-cli/pull/723] by [@brentvatne](https://github.com/brentvatne))
- Grammar: replace setup with set up. ([#735](https://github.com/expo/eas-cli/pull/735) by [@quinlanj](https://github.com/quinlanj))
- Improve VCS workflow migration experience. ([#732](https://github.com/expo/eas-cli/pull/732) by [@wkozyra95](https://github.com/wkozyra95))
## [0.34.1](https://github.com/expo/eas-cli/releases/tag/v0.34.1) - 2021-11-02
## [0.34.1](https://github.com/expo/eas-cli/releases/tag/0.34.1) - 2021-11-02
### 🐛 Bug fixes
@@ -65,16 +33,16 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 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))
- 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/v0.34.0) - 2021-11-01
## [0.34.0](https://github.com/expo/eas-cli/releases/tag/0.34.0) - 2021-11-01
### 🛠 Breaking changes
- 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))
- `[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))
- Implement no-commit build workflow. Add required `cli` field in root of `eas.json`. ([#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
@@ -89,7 +57,7 @@ This is the log of notable changes to EAS CLI and related packages.
- 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/v0.33.1) - 2021-10-22
## [0.33.1](https://github.com/expo/eas-cli/releases/tag/0.33.1) - 2021-10-22
### 🎉 New features
@@ -99,7 +67,7 @@ This is the log of notable changes to EAS CLI and related packages.
- [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/v0.33.0) - 2021-10-20
## [0.33.0](https://github.com/expo/eas-cli/releases/tag/0.33.0) - 2021-10-20
### 🎉 New features
@@ -122,11 +90,11 @@ This is the log of notable changes to EAS CLI and related packages.
- 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/v0.32.0) - 2021-10-15
## [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))
- 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))
@@ -136,16 +104,16 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 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))
- 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/v0.31.1) - 2021-10-08
## [0.31.1](https://github.com/expo/eas-cli/releases/tag/0.31.1) - 2021-10-08
### 🐛 Bug fixes
- Google Service Account Keys: Fix non-interactive bug in credentials service workflow ([#682](https://github.com/expo/eas-cli/pull/682) by [@quinlanj](https://github.com/quinlanj))
## [0.31.0](https://github.com/expo/eas-cli/releases/tag/v0.31.0) - 2021-10-08
## [0.31.0](https://github.com/expo/eas-cli/releases/tag/0.31.0) - 2021-10-08
### 🛠 Breaking changes
@@ -156,7 +124,7 @@ This is the log of notable changes to EAS CLI and related packages.
- Integrate credentials service with Android submissions. ([#664](https://github.com/expo/eas-cli/pull/664) by [@quinlanj](https://github.com/quinlanj))
- Add option to review ad-hoc devices when reusing provisioning profile. ([#673](https://github.com/expo/eas-cli/pull/673) by [@dsokal](https://github.com/dsokal))
## [0.30.1](https://github.com/expo/eas-cli/releases/tag/v0.30.1) - 2021-10-06
## [0.30.1](https://github.com/expo/eas-cli/releases/tag/0.30.1) - 2021-10-06
### 🐛 Bug fixes
@@ -535,7 +503,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
- Remove "Auto" option for `schemeBuildConfiguration` and make the old "Auto" behaviour the default. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
- `[EAS BUILD API]` Remove "Auto" option for `schemeBuildConfiguration` and make the old "Auto" behaviour the default. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
- Remove `experimental.disableIosBundleIdentifierValidation` flag from eas.json. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
- Deprecate `android.package` and `ios.bundleIdentifier` in app config for generic projects. EAS CLI depends on the values in native code now. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
- Remove application id synchronization (`android.package` and `ios.bundleIdentifier`) between app.json and native code when running builds. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
+7 -1
View File
@@ -1,4 +1,10 @@
## Releasing a new version
1. Run `yarn release` in the repository root folder.
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
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 `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.36.1"
"version": "0.34.1"
}
+1 -1
View File
@@ -9,7 +9,7 @@
"watch": "yarn start",
"eas": "packages/eas-cli/bin/run",
"lint": "eslint . --ext .ts --ext .js",
"release": "./scripts/bin/run update-local-plugin && lerna version",
"release": "lerna version",
"test": "jest",
"clean": "lerna run clean && rm -rf node_modules coverage"
},
+28 -27
View File
@@ -73,7 +73,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/account/login.js)_
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/login.js)_
## `eas account:logout`
@@ -87,7 +87,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/account/logout.js)_
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/logout.js)_
## `eas account:view`
@@ -101,7 +101,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/account/view.js)_
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/view.js)_
## `eas analytics [STATUS]`
@@ -112,7 +112,7 @@ USAGE
$ eas analytics [STATUS]
```
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/analytics.js)_
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/analytics.js)_
## `eas build`
@@ -146,7 +146,7 @@ OPTIONS
--[no-]wait Wait for build(s) to complete
```
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/build/index.js)_
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/index.js)_
## `eas build:cancel [BUILD_ID]`
@@ -157,7 +157,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/build/cancel.js)_
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/cancel.js)_
## `eas build:configure`
@@ -171,7 +171,7 @@ OPTIONS
-p, --platform=(android|ios|all) Platform to configure
```
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/build/configure.js)_
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/configure.js)_
## `eas build:list`
@@ -204,7 +204,7 @@ OPTIONS
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/build/list.js)_
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/list.js)_
## `eas build:view [BUILD_ID]`
@@ -218,7 +218,7 @@ OPTIONS
--json Enable JSON output, non-JSON messages will be printed to stderr
```
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/build/view.js)_
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/view.js)_
## `eas config`
@@ -233,7 +233,7 @@ OPTIONS
--profile=profile
```
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/config.js)_
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/config.js)_
## `eas credentials`
@@ -244,7 +244,7 @@ USAGE
$ eas credentials
```
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/credentials.js)_
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/credentials.js)_
## `eas device:create`
@@ -255,7 +255,7 @@ USAGE
$ eas device:create
```
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/device/create.js)_
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/create.js)_
## `eas device:list`
@@ -269,7 +269,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/device/list.js)_
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/list.js)_
## `eas device:view [UDID]`
@@ -280,7 +280,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/device/view.js)_
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/view.js)_
## `eas diagnostics`
@@ -291,7 +291,7 @@ USAGE
$ eas diagnostics
```
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/diagnostics.js)_
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/diagnostics.js)_
## `eas help [COMMAND]`
@@ -319,7 +319,7 @@ USAGE
$ eas project:info
```
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/project/info.js)_
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/project/info.js)_
## `eas project:init`
@@ -333,7 +333,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/project/init.js)_
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/project/init.js)_
## `eas secret:create`
@@ -350,7 +350,7 @@ OPTIONS
--value=value Value of the secret
```
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/secret/create.js)_
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/create.js)_
## `eas secret:delete`
@@ -367,7 +367,7 @@ DESCRIPTION
Unsure where to find the secret's ID? Run eas secret:list
```
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/secret/delete.js)_
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/delete.js)_
## `eas secret:list`
@@ -378,7 +378,7 @@ USAGE
$ eas secret:list
```
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/secret/list.js)_
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/list.js)_
## `eas submit`
@@ -405,13 +405,14 @@ OPTIONS
--[no-]wait Wait for submission to complete
DESCRIPTION
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
See how to configure submits with eas.json:
https://docs.expo.dev/submit/eas-json/
ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/submit.js)_
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/submit.js)_
## `eas webhook:create`
@@ -430,7 +431,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/webhook/create.js)_
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/create.js)_
## `eas webhook:delete [ID]`
@@ -444,7 +445,7 @@ ARGUMENTS
ID ID of the webhook to delete
```
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/webhook/delete.js)_
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/delete.js)_
## `eas webhook:list`
@@ -458,7 +459,7 @@ OPTIONS
--event=(BUILD) Event type that triggers the webhook
```
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/webhook/list.js)_
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/list.js)_
## `eas webhook:update`
@@ -478,7 +479,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/webhook/update.js)_
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/update.js)_
## `eas webhook:view ID`
@@ -492,5 +493,5 @@ ARGUMENTS
ID ID of the webhook to view
```
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/webhook/view.js)_
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/view.js)_
<!-- commandsstop -->
+1 -1
View File
@@ -1,5 +1,5 @@
overwrite: true
schema: 'https://staging-api.expo.dev/graphql'
schema: 'https://staging.exp.host/--/graphql'
documents:
- 'src/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
+184 -769
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.36.1",
"version": "0.34.1",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -12,7 +12,7 @@
"@expo/config": "6.0.3",
"@expo/config-plugins": "4.0.3",
"@expo/eas-build-job": "0.2.57",
"@expo/eas-json": "^0.36.0",
"@expo/eas-json": "^0.34.1",
"@expo/json-file": "8.2.33",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.14",
+2 -2
View File
@@ -8,7 +8,7 @@ import { apiClient, getExpoApiBaseUrl } from '../api';
describe('apiClient', () => {
it('converts Expo APIv2 error to ApiV2Error', async () => {
nock(getExpoApiBaseUrl())
.post('/v2/test')
.post('/--/api/v2/test')
.reply(400, {
errors: [
{
@@ -39,7 +39,7 @@ describe('apiClient', () => {
});
it('does not convert non-APIv2 error to ApiV2Error', async () => {
nock(getExpoApiBaseUrl()).post('/v2/test').reply(500, 'Something went wrong');
nock(getExpoApiBaseUrl()).post('/--/api/v2/test').reply(500, 'Something went wrong');
let error: Error | null = null;
try {
@@ -3,8 +3,8 @@ import os from 'os';
import { URL } from 'url';
import { v4 as uuidv4 } from 'uuid';
import UserSettings from '../user/UserSettings';
import { easCliVersion } from '../utils/easCli';
import UserSettings from './user/UserSettings';
import { easCliVersion } from './utils/easCli';
const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
darwin: 'Mac',
-25
View File
@@ -1,25 +0,0 @@
import { Analytics, Event } from './events';
export type TrackingContext = Record<string, string | number | boolean>;
export async function withAnalyticsAsync<Result>(
fn: () => Promise<Result>,
analytics: {
attemptEvent: Event;
successEvent: Event;
failureEvent: Event;
trackingCtx: TrackingContext;
}
): Promise<Result> {
try {
const result = await fn();
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
return result;
} catch (error: any) {
Analytics.logEvent(analytics.failureEvent, {
...analytics.trackingCtx,
reason: error.message,
});
throw error;
}
}
-52
View File
@@ -1,52 +0,0 @@
import { logEvent } from './rudderstackClient';
export type Event = BuildEvent | SubmissionEvent;
export enum SubmissionEvent {
SUBMIT_COMMAND = 'submit cli submit command',
SUBMIT_COMMAND_ATTEMPT = 'submit cli attempt',
SUBMIT_COMMAND_SUCCESS = 'submit cli success',
SUBMIT_COMMAND_FAIL = 'submit cli fail',
GATHER_CREDENTIALS_ATTEMPT = 'submit cli gather credentials attempt',
GATHER_CREDENTIALS_SUCCESS = 'submit cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'submit cli gather credentials fail',
GATHER_ARCHIVE_ATTEMPT = 'submit cli gather archive attempt',
GATHER_ARCHIVE_SUCCESS = 'submit cli gather archive success',
GATHER_ARCHIVE_FAIL = 'submit cli gather archive fail',
SUBMIT_REQUEST_ATTEMPT = 'submit cli request attempt',
SUBMIT_REQUEST_SUCCESS = 'submit cli request success',
SUBMIT_REQUEST_FAIL = 'submit cli request fail',
}
export enum BuildEvent {
BUILD_COMMAND = 'build cli build command',
PROJECT_UPLOAD_ATTEMPT = 'build cli project upload attempt',
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
GATHER_CREDENTIALS_ATTEMPT = 'build cli gather credentials attempt',
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
CONFIGURE_PROJECT_ATTEMPT = 'build cli configure project attempt',
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
BUILD_REQUEST_ATTEMPT = 'build cli build request attempt',
BUILD_REQUEST_SUCCESS = 'build cli build request success',
BUILD_REQUEST_FAIL = 'build cli build request fail',
BUILD_STATUS_COMMAND = 'build cli build status',
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
CREDENTIALS_SYNC_UPDATE_LOCAL_ATTEMPT = 'build cli credentials sync update local attempt',
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
CREDENTIALS_SYNC_UPDATE_REMOTE_ATTEMPT = 'build cli credentials sync update remote attempt',
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
}
export class Analytics {
static logEvent(name: Event, properties: Record<string, any>): void {
logEvent(name, properties);
}
}
+3 -3
View File
@@ -4,7 +4,7 @@ import ApiV2Error from './ApiV2Error';
import { getAccessToken, getSessionSecret } from './user/sessionStorage';
export const apiClient = got.extend({
prefixUrl: getExpoApiBaseUrl() + '/v2/',
prefixUrl: getExpoApiBaseUrl() + '/--/api/v2/',
hooks: {
beforeRequest: [
(options: NormalizedOptions) => {
@@ -40,11 +40,11 @@ export const apiClient = got.extend({
export function getExpoApiBaseUrl(): string {
if (process.env.EXPO_STAGING) {
return `https://staging-api.expo.dev`;
return `https://staging.exp.host`;
} else if (process.env.EXPO_LOCAL) {
return `http://127.0.0.1:3000`;
} else {
return `https://api.expo.dev`;
return `https://exp.host`;
}
}
+31 -14
View File
@@ -3,8 +3,6 @@ import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import { withAnalyticsAsync } from '../analytics/common';
import { BuildEvent } from '../analytics/events';
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
import { BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildQuery } from '../graphql/queries/BuildQuery';
@@ -19,6 +17,8 @@ import { getVcsClient } from '../vcs';
import { BuildContext } from './context';
import { runLocalBuildAsync } from './local';
import { MetadataContext, collectMetadataAsync } from './metadata';
import { TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
import { printDeprecationWarnings } from './utils/printBuildInfo';
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
@@ -55,17 +55,15 @@ export async function prepareBuildRequestForPlatformAsync<
const credentialsResult = await withAnalyticsAsync(
async () => await builder.ensureCredentialsAsync(ctx),
{
attemptEvent: BuildEvent.GATHER_CREDENTIALS_ATTEMPT,
successEvent: BuildEvent.GATHER_CREDENTIALS_SUCCESS,
failureEvent: BuildEvent.GATHER_CREDENTIALS_FAIL,
successEvent: Event.GATHER_CREDENTIALS_SUCCESS,
failureEvent: Event.GATHER_CREDENTIALS_FAIL,
trackingCtx: ctx.trackingCtx,
}
);
if (!ctx.skipProjectConfiguration) {
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(ctx), {
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
successEvent: Event.CONFIGURE_PROJECT_SUCCESS,
failureEvent: Event.CONFIGURE_PROJECT_FAIL,
trackingCtx: ctx.trackingCtx,
});
}
@@ -168,9 +166,8 @@ async function uploadProjectAsync<TPlatform extends Platform>(
return bucketKey;
},
{
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
successEvent: BuildEvent.PROJECT_UPLOAD_SUCCESS,
failureEvent: BuildEvent.PROJECT_UPLOAD_FAIL,
successEvent: Event.PROJECT_UPLOAD_SUCCESS,
failureEvent: Event.PROJECT_UPLOAD_FAIL,
trackingCtx: ctx.trackingCtx,
}
);
@@ -203,14 +200,34 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
return build;
},
{
attemptEvent: BuildEvent.BUILD_REQUEST_ATTEMPT,
successEvent: BuildEvent.BUILD_REQUEST_SUCCESS,
failureEvent: BuildEvent.BUILD_REQUEST_FAIL,
successEvent: Event.BUILD_REQUEST_SUCCESS,
failureEvent: Event.BUILD_REQUEST_FAIL,
trackingCtx: ctx.trackingCtx,
}
);
}
async function withAnalyticsAsync<Result>(
fn: () => Promise<Result>,
analytics: {
successEvent: Event;
failureEvent: Event;
trackingCtx: TrackingContext;
}
): Promise<Result> {
try {
const result = await fn();
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
return result;
} catch (error: any) {
Analytics.logEvent(analytics.failureEvent, {
...analytics.trackingCtx,
reason: error.message,
});
throw error;
}
}
export async function waitForBuildEndAsync(
buildIds: string[],
{ timeoutSec = 3600, intervalSec = 30 } = {}
+3 -3
View File
@@ -5,8 +5,6 @@ import JsonFile from '@expo/json-file';
import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
import { TrackingContext } from '../analytics/common';
import { Analytics, BuildEvent } from '../analytics/events';
import { CredentialsContext } from '../credentials/context';
import { RequestedPlatform } from '../platform';
import { getExpoConfig } from '../project/expoConfig';
@@ -15,6 +13,8 @@ import { resolveWorkflowAsync } from '../project/workflow';
import { findAccountByName } from '../user/Account';
import { Actor } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
import { TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
export interface ConfigureContext {
user: Actor;
@@ -95,7 +95,7 @@ export async function createBuildContextAsync<T extends Platform>({
project_type: workflow,
...devClientProperties,
};
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
return {
accountName,
@@ -20,11 +20,7 @@ export async function ensureIosCredentialsAsync(
app: getAppFromContext(buildCtx.credentialsCtx),
targets,
iosCapabilitiesOptions: {
entitlements: await resolveEntitlementsJsonAsync(
buildCtx.projectDir,
buildCtx.workflow,
buildCtx.buildProfile.env ?? {}
),
entitlements: await resolveEntitlementsJsonAsync(buildCtx.projectDir, buildCtx.workflow),
},
distribution: buildCtx.buildProfile.distribution ?? 'store',
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
+37 -19
View File
@@ -1,27 +1,45 @@
import { Job } from '@expo/eas-build-job';
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import Log from '../log';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.48';
export async function runLocalBuildAsync(job: Job): Promise<void> {
const { command, args } = getCommandAndArgs(job);
await spawnAsync(command, args, {
stdio: 'inherit',
});
}
function getCommandAndArgs(job: Job): { command: string; args: string[] } {
const jobBase64 = Buffer.from(JSON.stringify({ job })).toString('base64');
if (process.env.EAS_LOCAL_BUILD_PLUGIN_PATH) {
return {
command: process.env.EAS_LOCAL_BUILD_PLUGIN_PATH,
args: [jobBase64],
};
} else {
return {
command: 'npx',
args: ['-y', `${PLUGIN_PACKAGE_NAME}@${PLUGIN_PACKAGE_VERSION}`, jobBase64],
};
const execNameOrPath =
process.env.EAS_LOCAL_BUILD_PLUGIN_PATH ??
(await findWithNodeResolutionAsync()) ??
PLUGIN_PACKAGE_NAME;
try {
const arg = Buffer.from(JSON.stringify({ job })).toString('base64');
await spawnAsync(execNameOrPath, [arg], {
stdio: 'inherit',
});
} catch (err: any) {
if (err.code === 'ENOENT') {
Log.warn(`Could not resolve executable ${execNameOrPath}.`);
Log.warn(
`Install ${PLUGIN_PACKAGE_NAME} package from npm e.g. ${chalk.bold(
`npm install -g ${PLUGIN_PACKAGE_NAME}`
)} and make sure it's in PATH.`
);
throw err;
}
}
}
async function findWithNodeResolutionAsync(): Promise<string | undefined> {
try {
const resolvedPath = path.resolve(
path.dirname(require.resolve(PLUGIN_PACKAGE_NAME)),
'../bin/run'
);
if (await fs.pathExists(resolvedPath)) {
return resolvedPath;
}
} catch {}
return undefined;
}
+2
View File
@@ -12,3 +12,5 @@ export enum BuildDistributionType {
INTERNAL = 'internal',
SIMULATOR = 'simulator',
}
export type TrackingContext = Record<string, string | number | boolean>;
@@ -0,0 +1,29 @@
import { logEvent } from '../../analytics';
export enum Event {
BUILD_COMMAND = 'build cli build command',
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
BUILD_REQUEST_SUCCESS = 'build cli build request success',
BUILD_REQUEST_FAIL = 'build cli build request fail',
BUILD_STATUS_COMMAND = 'build cli build status',
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
}
export default {
logEvent(name: Event, properties: Record<string, any>) {
logEvent(name, properties);
},
};
+1 -1
View File
@@ -22,7 +22,7 @@ export function getArtifactUrl(artifactId: string): string {
export function getInternalDistributionInstallUrl(build: BuildFragment): string {
if (build.platform === AppPlatform.Ios) {
return `itms-services://?action=download-manifest;url=${getExpoApiBaseUrl()}/v2/projects/${
return `itms-services://?action=download-manifest;url=${getExpoApiBaseUrl()}/--/api/v2/projects/${
build.project.id
}/builds/${build.id}/manifest.plist`;
}
@@ -7,7 +7,7 @@ import {
flushAsync as flushAnalyticsAsync,
initAsync as initAnalyticsAsync,
logEvent,
} from '../analytics/rudderstackClient';
} from '../analytics';
import Log from '../log';
import { findProjectRootAsync } from '../project/projectUtils';
import { getUserAsync } from '../user/User';
@@ -1,5 +1,5 @@
import { asMock } from '../../__tests__/utils';
import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClient';
import { flushAsync, initAsync, logEvent } from '../../analytics';
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
import { getUserAsync } from '../../user/User';
import { ensureLoggedInAsync } from '../../user/actions';
@@ -10,8 +10,8 @@ TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
jest.mock('../../user/User');
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
jest.mock('../../analytics/rudderstackClient', () => {
const { AnalyticsEvent } = jest.requireActual('../../analytics/rudderstackClient');
jest.mock('../../analytics', () => {
const { AnalyticsEvent } = jest.requireActual('../../analytics');
return {
AnalyticsEvent,
logEvent: jest.fn(),
+1 -1
View File
@@ -274,7 +274,7 @@ export default class Build extends EasCommand {
wait: flags['wait'],
clearCache: flags['clear-cache'],
json: flags['json'],
autoSubmit: flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined,
autoSubmit: flags['auto-submit'],
submitProfile: flags['auto-submit-with-profile'] ?? profile,
};
}
@@ -9,7 +9,7 @@ export class RemoveFcm {
async runAsync(ctx: CredentialsContext): Promise<void> {
if (ctx.nonInteractive) {
throw new Error(
"Deleting an FCM API Key is a destructive operation. Start the CLI without the '--non-interactive' flag to delete the credentials."
"Deleting an FCM Api Key is a destructive operation. Start the CLI without the '--non-interactive' flag to delete the credentials."
);
}
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(
@@ -18,7 +18,7 @@ export class RemoveFcm {
const fcm = appCredentials?.androidFcm;
if (!fcm) {
Log.warn(
`There is no valid FCM API Key defined for ${formatProjectFullName(this.app)}, ${
`There is no valid FCM Api Key defined for ${formatProjectFullName(this.app)}, ${
this.app.androidApplicationIdentifier
}`
);
@@ -26,7 +26,7 @@ export class RemoveFcm {
}
const confirm = await confirmAsync({
message: 'Permanently delete the FCM API Key from Expo servers?',
message: 'Permanently delete the FCM Api Key from Expo servers?',
initial: false,
});
if (!confirm) {
@@ -34,6 +34,6 @@ export class RemoveFcm {
}
await ctx.android.deleteFcmAsync(fcm);
Log.succeed('FCM API Key removed');
Log.succeed('FCM Api Key removed');
}
}
@@ -12,7 +12,7 @@ jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(RemoveFcm, () => {
it('removes an FCM API Key', async () => {
it('removes an FCM Api Key', async () => {
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -1,35 +1,31 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import crypto from 'crypto';
import fs from 'fs-extra';
// (dsokal) We actually want to use node-fetch but the change is in progress.
// See https://github.com/expo/eas-cli/issues/32 for context.
// eslint-disable-next-line no-restricted-imports
import fetch from 'node-fetch';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { Analytics, BuildEvent } from '../../../analytics/events';
import Analytics, { Event } from '../../../build/utils/analytics';
import { AndroidKeystoreType } from '../../../graphql/generated';
import { KeystoreGenerationUrlMutation } from '../../../graphql/mutations/KeystoreGenerationUrlMutation';
import Log from '../../../log';
import { ora } from '../../../ora';
import { getTmpDirectory } from '../../../utils/paths';
import { KeystoreWithType } from '../credentials';
interface KeystoreParams {
keystorePassword: string;
keyAlias: string;
keyPassword: string;
}
export async function keytoolCommandExistsAsync(): Promise<boolean> {
try {
await spawnAsync('keytool');
return true;
} catch (error) {
return false;
}
return true;
}
async function ensureKeytoolCommandExistsAsync(): Promise<void> {
if (!(await keytoolCommandExistsAsync())) {
Log.error('keytool is required to run this command, make sure you have it installed?');
Log.warn('keytool is a part of OpenJDK: https://openjdk.java.net/');
Log.warn('Also make sure that keytool is in your PATH after installation.');
throw new Error('keytool not found');
}
}
enum KeystoreCreateStep {
@@ -39,7 +35,7 @@ enum KeystoreCreateStep {
}
export async function generateRandomKeystoreAsync(projectId: string): Promise<KeystoreWithType> {
const keystoreData: KeystoreParams = {
const keystoreData = {
keystorePassword: crypto.randomBytes(16).toString('hex'),
keyPassword: crypto.randomBytes(16).toString('hex'),
keyAlias: crypto.randomBytes(16).toString('hex'),
@@ -48,38 +44,23 @@ export async function generateRandomKeystoreAsync(projectId: string): Promise<Ke
}
async function createKeystoreAsync(
keystoreParams: KeystoreParams,
credentials: {
keystorePassword: string;
keyAlias: string;
keyPassword: string;
},
projectId: string
): Promise<KeystoreWithType> {
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Attempt,
type: AndroidKeystoreType.Jks,
});
try {
let keystore: KeystoreWithType | undefined;
let localAttempt = false;
if (await keytoolCommandExistsAsync()) {
try {
localAttempt = true;
keystore = await createKeystoreLocallyAsync(keystoreParams);
} catch {
Log.error('Failed to generate keystore locally. Falling back to cloud generation.');
}
}
if (!keystore) {
keystore = await createKeystoreInCloudAsync(keystoreParams, {
showKeytoolDetectionMsg: !localAttempt,
});
}
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Success,
type: AndroidKeystoreType.Jks,
});
return keystore;
await ensureKeytoolCommandExistsAsync();
} catch (error: any) {
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Fail,
reason: error.message,
@@ -87,11 +68,7 @@ async function createKeystoreAsync(
});
throw error;
}
}
async function createKeystoreLocallyAsync(
keystoreParams: KeystoreParams
): Promise<KeystoreWithType> {
await fs.mkdirp(getTmpDirectory());
const keystorePath = path.join(getTmpDirectory(), `${uuidv4()}-keystore.jks`);
try {
@@ -101,13 +78,13 @@ async function createKeystoreLocallyAsync(
'-storetype',
'JKS',
'-storepass',
keystoreParams.keystorePassword,
credentials.keystorePassword,
'-keypass',
keystoreParams.keyPassword,
credentials.keyPassword,
'-keystore',
keystorePath,
'-alias',
keystoreParams.keyAlias,
credentials.keyAlias,
'-keyalg',
'RSA',
'-keysize',
@@ -117,49 +94,27 @@ async function createKeystoreLocallyAsync(
'-dname',
`CN=,OU=,O=,L=,S=,C=US`,
]);
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Success,
type: AndroidKeystoreType.Jks,
});
return {
...keystoreParams,
...credentials,
keystore: await fs.readFile(keystorePath, 'base64'),
type: AndroidKeystoreType.Jks,
};
} catch (error: any) {
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Fail,
reason: error.message,
type: AndroidKeystoreType.Jks,
});
throw error;
} finally {
await fs.remove(keystorePath);
}
}
interface KeystoreServiceResult {
keystoreBase64: string;
keystorePassword: string;
keyPassword: string;
keyAlias: string;
}
async function createKeystoreInCloudAsync(
keystoreParams: KeystoreParams,
{ showKeytoolDetectionMsg = true } = {}
): Promise<KeystoreWithType> {
if (showKeytoolDetectionMsg) {
Log.log(`Detected that you do not have ${chalk.bold('keytool')} installed locally.`);
}
const spinner = ora('Generating keystore in the cloud...').start();
try {
const url = await KeystoreGenerationUrlMutation.createKeystoreGenerationUrlAsync();
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(keystoreParams),
headers: { 'Content-Type': 'application/json' },
});
const result: KeystoreServiceResult = await response.json();
spinner.succeed();
return {
type: AndroidKeystoreType.Jks,
keystore: result.keystoreBase64,
keystorePassword: result.keystorePassword,
keyAlias: result.keyAlias,
keyPassword: result.keyPassword,
};
} catch (err: any) {
spinner.fail();
throw err;
}
}
@@ -64,7 +64,7 @@ export async function provideOrGenerateAscApiKeyAsync(
purpose: AppStoreApiKeyPurpose
): Promise<MinimalAscApiKey> {
if (ctx.nonInteractive) {
throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
return await generateAscApiKeyAsync(ctx, purpose);
}
const userProvided = await promptForAscApiKeyAsync(ctx);
@@ -73,7 +73,7 @@ export async function provideOrGenerateAscApiKeyAsync(
}
if (!ctx.appStore.authCtx) {
Log.warn('Unable to validate App Store Connect API Key, you are not authenticated with Apple.');
Log.warn('Unable to validate App Store Connect Api Key, you are not authenticated with Apple.');
return userProvided;
}
@@ -82,7 +82,7 @@ export async function provideOrGenerateAscApiKeyAsync(
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?`,
message: `App Store Connect Api Key with ID ${userProvided.keyId} is not valid on Apple's servers. Proceed anyway?`,
});
if (useUserProvided) {
return userProvided;
@@ -94,7 +94,6 @@ async function generateAscApiKeyAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<MinimalAscApiKey> {
//console.log('debug');
await ctx.appStore.ensureAuthenticatedAsync();
const ascApiKey = await ctx.appStore.createAscApiKeyAsync({
nickname: getAscApiKeyName(purpose),
@@ -130,7 +129,7 @@ async function promptForKeyP8AndIdAsync(): Promise<Pick<AscApiKeyPath, 'keyP8Pat
const { keyP8Path } = await promptAsync({
type: 'text',
name: 'keyP8Path',
message: 'Path to App Store Connect API Key:',
message: 'Path to App Store Connect Api Key:',
initial: 'AuthKey_ABCD.p8',
// eslint-disable-next-line async-protect/async-suffix
validate: async (filePath: string) => {
@@ -190,12 +189,12 @@ export async function selectAscApiKeysFromAccountAsync(
if (filterDifferentAppleTeam) {
const maybeAppleTeamId = ctx.appStore.authCtx?.team.id;
Log.warn(
`There are no App Store Connect API Keys in your EAS account${
`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.`);
Log.warn(`There are no App Store Connect Api Keys available in your EAS account.`);
}
return null;
}
@@ -209,7 +208,7 @@ async function selectAscApiKeysAsync(
const { chosenAscApiKey } = await promptAsync({
type: 'select',
name: 'chosenAscApiKey',
message: 'Select an API Key from the list:',
message: 'Select an Api Key from the list:',
choices: sortedAscApiKeys.map(ascApiKey => ({
title: formatAscApiKey(ascApiKey),
value: ascApiKey,
@@ -31,7 +31,7 @@ export class AssignAscApiKey {
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}.`
`App Store Connect Api Key assigned to ${this.app.projectName}: ${this.app.bundleIdentifier} for ${purpose}.`
);
return updatedAppCredentials;
}
@@ -12,12 +12,12 @@ export class CreateAscApiKey {
purpose: AppStoreApiKeyPurpose
): Promise<AppStoreConnectApiKeyFragment> {
if (ctx.nonInteractive) {
throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
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');
Log.succeed('Created App Store Connect Api Key');
return result;
}
}
@@ -12,7 +12,7 @@ export class SelectAndRemoveAscApiKey {
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.succeed('Removed App Store Connect Api Key');
Log.newLine();
}
}
@@ -23,28 +23,28 @@ export class RemoveAscApiKey {
public async runAsync(ctx: CredentialsContext): Promise<void> {
if (ctx.nonInteractive) {
throw new Error(`Cannot remove App Store Connect API Keys in non-interactive mode.`);
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?`,
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');
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?`,
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.');
Log.log('Skipping Api Key revocation on the Apple Developer Portal.');
}
if (shouldRevoke) {
await ctx.appStore.revokeAscApiKeyAsync(this.ascApiKey.keyIdentifier);
@@ -31,10 +31,10 @@ export enum SetupAscApiKeyChoice {
export class SetUpAscApiKey {
public choices: { title: string; value: string }[] = [
{
title: '[Choose an existing ASC API Key]',
title: '[Choose an existing key]',
value: SetupAscApiKeyChoice.USE_EXISTING,
},
{ title: '[Add a new ASC API Key]', value: SetupAscApiKeyChoice.GENERATE },
{ title: '[Add a new key]', value: SetupAscApiKeyChoice.GENERATE },
];
constructor(private app: AppLookupParams, private purpose: AppStoreApiKeyPurpose) {}
@@ -42,15 +42,15 @@ export class SetUpAscApiKey {
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.');
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'
'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.'
'App Store Connect Api Keys cannot be set up in --non-interactive mode.'
);
}
@@ -88,11 +88,11 @@ export class SetUpAscApiKey {
}
const [autoselectedKey] = sortAscApiKeysByUpdatedAtDesc(validKeys);
const useAutoselected = await confirmAsync({
message: `Reuse this App Store Connect API Key?\n${formatAscApiKey(autoselectedKey)}`,
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}`);
Log.log(`Using App Store Connect Api Key with ID ${autoselectedKey.keyIdentifier}`);
return autoselectedKey;
} else {
return null;
@@ -105,7 +105,7 @@ export class SetUpAscApiKey {
): 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}.`);
throw new Error(`App Store Connect Api Key setup is not yet supported for ${purpose}.`);
}
return !!appCredentials?.appStoreConnectApiKeyForSubmissions;
}
@@ -123,7 +123,7 @@ export class SetUpAscApiKey {
const result = await promptAsync({
type: 'select',
name: 'choice',
message: 'Select what to use for your project:',
message: 'Select the App Store Connect Api Key to use for your project:',
choices,
});
choice = result.choice;
@@ -1,70 +0,0 @@
import chalk from 'chalk';
import nullthrows from 'nullthrows';
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';
export const PROMPT_FOR_APP_SPECIFIC_PASSWORD = 'PROMPT_FOR_APP_SPECIFIC_PASSWORD';
export class SetUpSubmissionCredentials {
private setupAscApiKeyAction;
constructor(app: AppLookupParams) {
this.setupAscApiKeyAction = new SetUpAscApiKey(app, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
// Add this unrelated choice to ASC API Key setup for legacy purposes -- we will deprecate it soon
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 {
const iosAppCredentials = await this.setupAscApiKeyAction.runAsync(ctx);
const { keyIdentifier, name } = nullthrows(
iosAppCredentials.appStoreConnectApiKeyForSubmissions,
'ASC API Key must be defined for EAS Submit'
);
Log.log(`Using API Key ID: ${keyIdentifier}${name ? ` (${name})` : ''}`);
return iosAppCredentials;
} catch (e) {
if (e instanceof UnsupportedCredentialsChoiceError) {
if (e.choice === PROMPT_FOR_APP_SPECIFIC_PASSWORD) {
return await this.promptForAppSpecificPasswordAsync();
}
}
throw e;
}
}
async promptForAppSpecificPasswordAsync(): Promise<string> {
Log.addNewLineIfNone();
Log.log(`Please enter your Apple app-specific password.`);
Log.log(learnMore('https://expo.fyi/apple-app-specific-password'));
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
)
);
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;
}
}
@@ -6,7 +6,7 @@ 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 () => {
it('assigns an App Store Connect Api Key in Interactive Mode for EAS Submit', async () => {
const ctx = createCtxMock({
nonInteractive: false,
});
@@ -12,7 +12,7 @@ jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(CreateAscApiKey, () => {
it('creates a App Store API Key in Interactive Mode', async () => {
it('creates a App Store Api Key in Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -10,7 +10,7 @@ jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(RemoveAscApiKey, () => {
it('removes an Asc API Key', async () => {
it('removes an Asc Api Key', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const removeAscApiKeyAction = new RemoveAscApiKey(
@@ -18,7 +18,7 @@ jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(SetUpAscApiKey, () => {
it('skips setting up a ASC API Key if it is already configured', async () => {
it('skips setting up a ASC Api Key if it is already configured', async () => {
const ctx = createCtxMock({
nonInteractive: false,
ios: {
@@ -35,12 +35,12 @@ describe(SetUpAscApiKey, () => {
);
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers
// 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 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 () => {
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: {
@@ -60,12 +60,12 @@ describe(SetUpAscApiKey, () => {
);
await setupAscApiKeyAction.runAsync(ctx);
// expect to create a new ASC API Key on expo servers
// 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 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 () => {
it('sets up a ASC Api Key, use autoselected ASC Api Key when there are existing keys', async () => {
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -86,12 +86,12 @@ describe(SetUpAscApiKey, () => {
);
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers, we are using the existing one
// 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 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 () => {
it('sets up a ASC Api Key, allowing user to choose existing keys', async () => {
const ctx = createCtxMock({
nonInteractive: false,
ios: {
@@ -110,9 +110,9 @@ describe(SetUpAscApiKey, () => {
);
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers, we are using the existing one
// 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 configuration to be updated with a new ASC Api Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('works in Non Interactive Mode if ASC Key is configured', async () => {
@@ -1,61 +0,0 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync, promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import {
getNewIosApiMock,
testCommonIosAppCredentialsFragment,
testTargets,
} from '../../../__tests__/fixtures-ios';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetUpAscApiKey } from '../SetUpAscApiKey';
import {
PROMPT_FOR_APP_SPECIFIC_PASSWORD,
SetUpSubmissionCredentials,
} from '../SetUpSubmissionCredentials';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(SetUpSubmissionCredentials, () => {
it('allows user to enter an App Specific Password', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({
choice: PROMPT_FOR_APP_SPECIFIC_PASSWORD,
}))
.mockImplementationOnce(() => ({
appSpecificPassword: 'super secret',
}));
const ctx = createCtxMock({
nonInteractive: false,
ios: {
...getNewIosApiMock(),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpSubmissionCredentials(appLookupParams);
const asp = await setupAscApiKeyAction.runAsync(ctx);
// prompt to choose ASP, then prompt to input ASP
expect(asMock(promptAsync).mock.calls.length).toBe(2);
expect(asp).toBe('super secret');
});
it('returns an ASC API key', async () => {
try {
jest
.spyOn(SetUpAscApiKey.prototype, 'runAsync')
.mockImplementation(async _ctx => testCommonIosAppCredentialsFragment);
const ctx = createCtxMock();
const appLookupParams = getAppLookupParamsFromContext(
ctx,
findApplicationTarget(testTargets)
);
const setupAscApiKeyAction = new SetUpSubmissionCredentials(appLookupParams);
const asc = await setupAscApiKeyAction.runAsync(ctx);
expect(asc).toEqual(testCommonIosAppCredentialsFragment);
} finally {
jest.restoreAllMocks();
}
});
});
@@ -6,14 +6,14 @@ import { AscApiKey, AscApiKeyInfo } from './Credentials.types';
import { AuthCtx, getRequestContext } from './authenticate';
export async function listAscApiKeysAsync(authCtx: AuthCtx): Promise<AscApiKeyInfo[]> {
const spinner = ora(`Fetching App Store Connect API Keys.`).start();
const spinner = ora(`Fetching App Store Connect Api Keys.`).start();
try {
const context = getRequestContext(authCtx);
const keys = await ApiKey.getAsync(context);
spinner.succeed(`Fetched App Store Connect API Keys.`);
spinner.succeed(`Fetched App Store Connect Api Keys.`);
return keys.map(key => getAscApiKeyInfo(key, authCtx));
} catch (error) {
spinner.fail(`Failed to fetch App Store Connect API Keys.`);
spinner.fail(`Failed to fetch App Store Connect Api Keys.`);
throw error;
}
}
@@ -22,11 +22,11 @@ export async function getAscApiKeyAsync(
authCtx: AuthCtx,
keyId: string
): Promise<AscApiKeyInfo | null> {
const spinner = ora(`Fetching App Store Connect API Key.`).start();
const spinner = ora(`Fetching App Store Connect Api Key.`).start();
try {
const context = getRequestContext(authCtx);
const apiKey = await ApiKey.infoAsync(context, { id: keyId });
spinner.succeed(`Fetched App Store Connect API Key (ID: ${keyId}).`);
spinner.succeed(`Fetched App Store Connect Api Key (ID: ${keyId}).`);
return getAscApiKeyInfo(apiKey, authCtx);
} catch (error: any) {
const message = error?.message ?? '';
@@ -35,7 +35,7 @@ export async function getAscApiKeyAsync(
return null;
}
Log.error(error);
spinner.fail(`Failed to fetch App Store Connect API Key.`);
spinner.fail(`Failed to fetch App Store Connect Api Key.`);
throw error;
}
}
@@ -49,7 +49,7 @@ export async function createAscApiKeyAsync(
keyType,
}: Partial<Pick<ApiKeyProps, 'nickname' | 'roles' | 'allAppsVisible' | 'keyType'>>
): Promise<AscApiKey> {
const spinner = ora(`Creating App Store Connect API Key.`).start();
const spinner = ora(`Creating App Store Connect Api Key.`).start();
try {
const context = getRequestContext(authCtx);
const key = await ApiKey.createAsync(context, {
@@ -74,13 +74,13 @@ export async function createAscApiKeyAsync(
}
// this object has more optional parameters populated
const fullKey = await ApiKey.infoAsync(context, { id: key.id });
spinner.succeed(`Created App Store Connect API Key.`);
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.');
spinner.fail('Failed to create App Store Connect Api Key.');
throw err;
}
}
@@ -89,16 +89,16 @@ export async function revokeAscApiKeyAsync(
authCtx: AuthCtx,
keyId: string
): Promise<AscApiKeyInfo> {
const spinner = ora(`Revoking App Store Connect API Key.`).start();
const spinner = ora(`Revoking App Store Connect Api Key.`).start();
try {
const context = getRequestContext(authCtx);
const apiKey = await ApiKey.infoAsync(context, { id: keyId });
const revokedKey = await apiKey.revokeAsync();
spinner.succeed(`Revoked App Store Connect API Key.`);
spinner.succeed(`Revoked App Store Connect Api Key.`);
return getAscApiKeyInfo(revokedKey, authCtx);
} catch (error) {
Log.error(error);
spinner.fail(`Failed to revoke App Store Connect API Key.`);
spinner.fail(`Failed to revoke App Store Connect Api Key.`);
throw error;
}
}
@@ -5,38 +5,25 @@ import plist from '@expo/plist';
import { getPrebuildConfig } from '@expo/prebuild-config';
import fs from 'fs-extra';
export async function getManagedEntitlementsJsonAsync(
projectDir: string,
env: Record<string, string>
): Promise<JSONObject> {
const originalProcessEnv: NodeJS.ProcessEnv = process.env;
try {
process.env = {
...process.env,
...env,
};
const { exp } = getPrebuildConfig(projectDir, { platforms: ['ios'] });
export async function getManagedEntitlementsJsonAsync(projectDir: string): Promise<JSONObject> {
let { exp } = getPrebuildConfig(projectDir, { platforms: ['ios'] });
const expWithMods = await compileModsAsync(exp, {
projectRoot: projectDir,
platforms: ['ios'],
introspect: true,
});
return expWithMods.ios?.entitlements || {};
} finally {
process.env = originalProcessEnv;
}
exp = await compileModsAsync(exp, {
projectRoot: projectDir,
platforms: ['ios'],
introspect: true,
});
return exp.ios?.entitlements || {};
}
export async function resolveEntitlementsJsonAsync(
projectDir: string,
workflow: Workflow,
env: Record<string, string>
workflow: Workflow
): Promise<JSONObject> {
if (workflow === Workflow.GENERIC) {
return (await getEntitlementsJsonAsync(projectDir)) || {};
} else if (workflow === Workflow.MANAGED) {
return await getManagedEntitlementsJsonAsync(projectDir, env);
return await getManagedEntitlementsJsonAsync(projectDir);
} else {
throw new Error(`Unknown workflow: ${workflow}`);
}
@@ -119,7 +119,7 @@ export interface AscApiKeyPath {
}
export const ascApiKeyIdSchema: CredentialSchema<Pick<MinimalAscApiKey, 'keyId'>> = {
name: 'App Store Connect API Key',
name: 'App Store Connect Api Key',
questions: [
{
field: 'keyId',
@@ -130,7 +130,7 @@ export const ascApiKeyIdSchema: CredentialSchema<Pick<MinimalAscApiKey, 'keyId'>
};
export const ascApiKeyIssuerIdSchema: CredentialSchema<Pick<MinimalAscApiKey, 'issuerId'>> = {
name: 'App Store Connect API Key',
name: 'App Store Connect Api Key',
questions: [
{
field: 'issuerId',
@@ -12,7 +12,7 @@ Developer Portal ID 9839M6AY8W
Apple Team 77KQ969CHE
Updated 6 months ago
App Store Connect API Key
App Store Connect Api Key
Developer Portal ID NYN468XNN3
Name Expo Submissions pl94k8_lnS
Issuer ID 69a6de93-e4e4-47e3-e053-5b8c7c11a4d1
@@ -239,7 +239,7 @@ function displayAscApiKey(
maybeAscApiKey: AppStoreConnectApiKeyFragment | null,
fields: { label: string; value: string }[]
): void {
fields.push({ label: 'App Store Connect API Key', value: '' });
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 });
@@ -8,7 +8,7 @@ export const highLevelActions: ActionInfo[] = [
},
{
value: AndroidActionType.ManageFcm,
title: 'Push Notifications: Manage your FCM API Key',
title: 'Push Notifications: Manage your FCM Api Key',
scope: Scope.Manager,
},
{
@@ -72,12 +72,12 @@ export const buildCredentialsActions: ActionInfo[] = [
export const fcmActions: ActionInfo[] = [
{
value: AndroidActionType.CreateFcm,
title: 'Upload an FCM API Key',
title: 'Upload an FCM Api Key',
scope: Scope.Project,
},
{
value: AndroidActionType.RemoveFcm,
title: 'Delete your FCM API Key',
title: 'Delete your FCM Api Key',
scope: Scope.Project,
},
{
@@ -14,7 +14,7 @@ export const highLevelActions: ActionInfo[] = [
},
{
value: IosActionType.ManageAscApiKey,
title: 'App Store Connect: Manage your API Key',
title: 'App Store Connect: Manage your Api Key',
scope: Scope.Manager,
},
{
@@ -81,22 +81,22 @@ export function getAscApiKeyActions(ctx: CredentialsContext): ActionInfo[] {
return [
{
value: IosActionType.SetUpAscApiKeyForSubmissions,
title: 'Set up your project to use an API Key for EAS Submit',
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',
title: 'Use an existing Api Key for EAS Submit',
scope: Scope.Project,
},
{
value: IosActionType.CreateAscApiKeyForSubmissions,
title: 'Add a new API Key For EAS Submit',
title: 'Add a new Api Key For EAS Submit',
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account,
},
{
value: IosActionType.RemoveAscApiKey,
title: 'Delete an API Key',
title: 'Delete an Api Key',
scope: Scope.Account,
},
{
@@ -229,10 +229,7 @@ export class ManageIos {
distribution: buildProfile.distribution,
enterpriseProvisioning: buildProfile.enterpriseProvisioning,
iosCapabilitiesOptions: {
entitlements: await getManagedEntitlementsJsonAsync(
ctx.projectDir,
buildProfile.env ?? {}
),
entitlements: await getManagedEntitlementsJsonAsync(ctx.projectDir),
},
}).runAsync(ctx);
return;
@@ -363,7 +360,7 @@ export class ManageIos {
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
const confirm = await confirmAsync({
message: `Do you want ${appLookupParams.projectName} to use the new API Key?`,
message: `Do you want ${appLookupParams.projectName} to use the new Api Key?`,
});
if (confirm) {
await new AssignAscApiKey(appLookupParams).runAsync(
+1 -1
View File
@@ -30,7 +30,7 @@ type SessionHeaders = {
};
export const graphqlClient = createUrqlClient({
url: getExpoApiBaseUrl() + '/graphql',
url: getExpoApiBaseUrl() + '/--/graphql',
exchanges: [
dedupExchange,
cacheExchange,
+54 -157
View File
@@ -41,7 +41,7 @@ export type RootQuery = {
appleDeviceRegistrationRequest: AppleDeviceRegistrationRequestQuery;
/** Top-level query object for querying Apple Teams. */
appleTeam: AppleTeamQuery;
app: AppQuery;
app?: Maybe<AppQuery>;
/**
* Look up app by app id
* @deprecated Use 'byId' field under 'app'.
@@ -60,8 +60,6 @@ export type RootQuery = {
clientBuilds: ClientBuildQuery;
/** Top-level query object for querying Experimentation configuration. */
experimentation: ExperimentationQuery;
/** Top-level query object for querying Stripe Invoices. */
invoice: InvoiceQuery;
project: ProjectQuery;
snack: SnackQuery;
submissions: SubmissionQuery;
@@ -430,31 +428,19 @@ export type App = Project & {
fullName: Scalars['String'];
description: Scalars['String'];
slug: Scalars['String'];
ownerAccount: Account;
privacySetting: AppPrivacy;
pushSecurityEnabled: Scalars['Boolean'];
/** Whether there have been any classic update publishes */
published: Scalars['Boolean'];
/** Time of last classic update publish */
updated: Scalars['DateTime'];
/** ID of latest classic update release */
latestReleaseId: Scalars['ID'];
/** Whether the latest classic update publish is using a deprecated SDK version */
isDeprecated: Scalars['Boolean'];
/** SDK version of the latest classic update publish, 0.0.0 otherwise */
sdkVersion: Scalars['String'];
/** Classic update release channel names (to be removed) */
releaseChannels: Array<Scalars['String']>;
/** Classic update release channel names that have at least one build */
buildsReleaseChannels: Array<Scalars['String']>;
/** githubUrl field from most recent classic update manifest */
published: Scalars['Boolean'];
ownerAccount: Account;
githubUrl?: Maybe<Scalars['String']>;
/** android.playStoreUrl field from most recent classic update manifest */
playStoreUrl?: Maybe<Scalars['String']>;
/** ios.appStoreUrl field from most recent classic update manifest */
appStoreUrl?: Maybe<Scalars['String']>;
/** Info about the icon specified in the most recent classic update manifest */
icon?: Maybe<AppIcon>;
sdkVersion: Scalars['String'];
isDeprecated: Scalars['Boolean'];
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>;
@@ -648,12 +634,6 @@ export type AppLatestReleaseForReleaseChannelArgs = {
releaseChannel: Scalars['String'];
};
export enum AppPrivacy {
Public = 'PUBLIC',
Unlisted = 'UNLISTED',
Hidden = 'HIDDEN'
}
export type AppIcon = {
__typename?: 'AppIcon';
url: Scalars['String'];
@@ -664,6 +644,12 @@ export type AppIcon = {
};
export enum AppPrivacy {
Public = 'PUBLIC',
Unlisted = 'UNLISTED',
Hidden = 'HIDDEN'
}
export type BuildFilter = {
platform?: Maybe<AppPlatform>;
status?: Maybe<BuildStatus>;
@@ -959,8 +945,7 @@ export enum SubmissionStatus {
export type AndroidSubmissionConfig = {
__typename?: 'AndroidSubmissionConfig';
/** @deprecated applicationIdentifier is deprecated and will be auto-detected on submit */
applicationIdentifier?: Maybe<Scalars['String']>;
applicationIdentifier: Scalars['String'];
/** @deprecated archiveType is deprecated and will be null */
archiveType?: Maybe<SubmissionAndroidArchiveType>;
track: SubmissionAndroidTrack;
@@ -989,7 +974,7 @@ export enum SubmissionAndroidReleaseStatus {
export type IosSubmissionConfig = {
__typename?: 'IosSubmissionConfig';
ascAppIdentifier: Scalars['String'];
appleIdUsername?: Maybe<Scalars['String']>;
appleIdUsername: Scalars['String'];
ascApiKeyId?: Maybe<Scalars['String']>;
};
@@ -1480,7 +1465,6 @@ export type SubscriptionDetails = {
endedAt?: Maybe<Scalars['DateTime']>;
trialEnd?: Maybe<Scalars['DateTime']>;
status?: Maybe<Scalars['String']>;
isDowngrading?: Maybe<Scalars['Boolean']>;
};
export type AddonDetails = {
@@ -1761,51 +1745,6 @@ export type ExperimentationQuery = {
deviceExperimentationUnit: Scalars['ID'];
};
export type InvoiceQuery = {
__typename?: 'InvoiceQuery';
/** Preview an upgrade subscription invoice, with proration */
previewInvoiceForSubscriptionUpdate: Invoice;
};
export type InvoiceQueryPreviewInvoiceForSubscriptionUpdateArgs = {
accountId: Scalars['String'];
newPlanIdentifier: Scalars['String'];
};
export type Invoice = {
__typename?: 'Invoice';
id: Scalars['ID'];
/** The total amount due for the invoice, in cents */
amountDue: Scalars['Int'];
/** The total amount that has been paid, considering any discounts or account credit. Value is in cents. */
amountPaid: Scalars['Int'];
/** The total amount that needs to be paid, considering any discounts or account credit. Value is in cents. */
amountRemaining: Scalars['Int'];
lineItems: Array<InvoiceLineItem>;
period: InvoicePeriod;
startingBalance: Scalars['Int'];
subtotal: Scalars['Int'];
total: Scalars['Int'];
};
export type InvoiceLineItem = {
__typename?: 'InvoiceLineItem';
id: Scalars['ID'];
description: Scalars['String'];
/** Line-item amount in cents */
amount: Scalars['Int'];
period: InvoicePeriod;
proration: Scalars['Boolean'];
quantity: Scalars['Int'];
};
export type InvoicePeriod = {
__typename?: 'InvoicePeriod';
start: Scalars['DateTime'];
end: Scalars['DateTime'];
};
export type ProjectQuery = {
__typename?: 'ProjectQuery';
byAccountNameAndSlug: Project;
@@ -1965,7 +1904,6 @@ export type RootMutation = {
iosAppBuildCredentials: IosAppBuildCredentialsMutation;
/** Mutations that modify the credentials for an iOS app */
iosAppCredentials: IosAppCredentialsMutation;
keystoreGenerationUrl: KeystoreGenerationUrlMutation;
/** Mutations that create, update, and delete Robots */
robot: RobotMutation;
/** Mutations that modify an EAS Submit submission */
@@ -2060,10 +1998,6 @@ export type AccountMutation = {
subscribeToProduct?: Maybe<Account>;
/** Cancels the active subscription */
cancelSubscription?: Maybe<Account>;
/** Upgrades or downgrades the active subscription to the newPlanIdentifier, which must be one of the EAS plans (i.e., Production or Enterprise). */
changePlan: Account;
/** Cancel scheduled subscription change */
cancelScheduledSubscriptionChange: 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']>;
/**
@@ -2111,17 +2045,6 @@ export type AccountMutationCancelSubscriptionArgs = {
};
export type AccountMutationChangePlanArgs = {
accountID: Scalars['ID'];
newPlanIdentifier: Scalars['String'];
};
export type AccountMutationCancelScheduledSubscriptionChangeArgs = {
accountID: Scalars['ID'];
};
export type AccountMutationRequestRefundArgs = {
accountID: Scalars['ID'];
chargeIdentifier: Scalars['ID'];
@@ -2900,18 +2823,6 @@ export type IosAppCredentialsInput = {
appStoreConnectApiKeyForSubmissionsId?: Maybe<Scalars['ID']>;
};
export type KeystoreGenerationUrlMutation = {
__typename?: 'KeystoreGenerationUrlMutation';
/** Create a Keystore Generation URL */
createKeystoreGenerationUrl: KeystoreGenerationUrl;
};
export type KeystoreGenerationUrl = {
__typename?: 'KeystoreGenerationUrl';
id: Scalars['ID'];
url: Scalars['String'];
};
export type RobotMutation = {
__typename?: 'RobotMutation';
/** Create a Robot and grant it Permissions on an Account */
@@ -3046,7 +2957,7 @@ export type AndroidSubmissionConfigInput = {
googleServiceAccountKeyId?: Maybe<Scalars['String']>;
googleServiceAccountKeyJson?: Maybe<Scalars['String']>;
archiveUrl?: Maybe<Scalars['String']>;
applicationIdentifier?: Maybe<Scalars['String']>;
applicationIdentifier: Scalars['String'];
track: SubmissionAndroidTrack;
releaseStatus?: Maybe<SubmissionAndroidReleaseStatus>;
changesNotSentForReview?: Maybe<Scalars['Boolean']>;
@@ -3629,7 +3540,7 @@ export type GetBranchInfoQueryVariables = Exact<{
export type GetBranchInfoQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
@@ -3639,7 +3550,7 @@ export type GetBranchInfoQuery = (
& Pick<UpdateBranch, 'id' | 'name'>
)> }
) }
) }
)> }
);
export type DeleteUpdateBranchMutationVariables = Exact<{
@@ -3666,7 +3577,7 @@ export type BranchesByAppQueryVariables = Exact<{
export type BranchesByAppQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
@@ -3677,7 +3588,7 @@ export type BranchesByAppQuery = (
& UpdateBranchFragment
)> }
) }
) }
)> }
);
export type GetUpdateGroupAsyncQueryVariables = Exact<{
@@ -3718,7 +3629,7 @@ export type ViewBranchQueryVariables = Exact<{
export type ViewBranchQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
@@ -3739,7 +3650,7 @@ export type ViewBranchQuery = (
)> }
)> }
) }
) }
)> }
);
export type CancelBuildMutationVariables = Exact<{
@@ -3784,7 +3695,7 @@ export type GetChannelByNameToEditQueryVariables = Exact<{
export type GetChannelByNameToEditQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
@@ -3798,7 +3709,7 @@ export type GetChannelByNameToEditQuery = (
)> }
)> }
) }
) }
)> }
);
export type UpdateChannelBranchMappingMutationVariables = Exact<{
@@ -3827,7 +3738,7 @@ export type GetAllChannelsForAppQueryVariables = Exact<{
export type GetAllChannelsForAppQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
@@ -3852,7 +3763,7 @@ export type GetAllChannelsForAppQuery = (
)> }
)> }
) }
) }
)> }
);
export type GetChannelByNameForAppQueryVariables = Exact<{
@@ -3863,7 +3774,7 @@ export type GetChannelByNameForAppQueryVariables = Exact<{
export type GetChannelByNameForAppQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
@@ -3888,7 +3799,7 @@ export type GetChannelByNameForAppQuery = (
)> }
)> }
) }
) }
)> }
);
export type AppInfoQueryVariables = Exact<{
@@ -3898,13 +3809,13 @@ export type AppInfoQueryVariables = Exact<{
export type AppInfoQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
& Pick<App, 'id' | 'fullName'>
) }
) }
)> }
);
export type DeleteUpdateGroupMutationVariables = Exact<{
@@ -4145,7 +4056,7 @@ export type CommonAndroidAppCredentialsWithBuildCredentialsByApplicationIdentifi
export type CommonAndroidAppCredentialsWithBuildCredentialsByApplicationIdentifierQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byFullName: (
{ __typename?: 'App' }
@@ -4156,7 +4067,7 @@ export type CommonAndroidAppCredentialsWithBuildCredentialsByApplicationIdentifi
& CommonAndroidAppCredentialsFragment
)> }
) }
) }
)> }
);
export type GoogleServiceAccountKeyByAccountQueryVariables = Exact<{
@@ -4542,14 +4453,14 @@ export type AppByFullNameQueryVariables = Exact<{
export type AppByFullNameQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byFullName: (
{ __typename?: 'App' }
& Pick<App, 'id'>
& AppFragment
) }
) }
)> }
);
export type AppStoreConnectApiKeyByAccountQueryVariables = Exact<{
@@ -4682,7 +4593,7 @@ export type AppleDistributionCertificateByAppQueryVariables = Exact<{
export type AppleDistributionCertificateByAppQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byFullName: (
{ __typename?: 'App' }
@@ -4706,7 +4617,7 @@ export type AppleDistributionCertificateByAppQuery = (
)> }
)> }
) }
) }
)> }
);
export type AppleDistributionCertificateByAccountQueryVariables = Exact<{
@@ -4739,7 +4650,7 @@ export type AppleProvisioningProfilesByAppQueryVariables = Exact<{
export type AppleProvisioningProfilesByAppQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byFullName: (
{ __typename?: 'App' }
@@ -4771,7 +4682,7 @@ export type AppleProvisioningProfilesByAppQuery = (
)> }
)> }
) }
) }
)> }
);
export type ApplePushKeyByAccountQueryVariables = Exact<{
@@ -4842,7 +4753,7 @@ export type IosAppBuildCredentialsByAppleAppIdentiferAndDistributionQueryVariabl
export type IosAppBuildCredentialsByAppleAppIdentiferAndDistributionQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byFullName: (
{ __typename?: 'App' }
@@ -4857,7 +4768,7 @@ export type IosAppBuildCredentialsByAppleAppIdentiferAndDistributionQuery = (
)> }
)> }
) }
) }
)> }
);
export type IosAppCredentialsWithBuildCredentialsByAppIdentifierIdQueryVariables = Exact<{
@@ -4869,7 +4780,7 @@ export type IosAppCredentialsWithBuildCredentialsByAppIdentifierIdQueryVariables
export type IosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byFullName: (
{ __typename?: 'App' }
@@ -4885,7 +4796,7 @@ export type IosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery = (
& CommonIosAppCredentialsWithoutBuildCredentialsFragment
)> }
) }
) }
)> }
);
export type CommonIosAppCredentialsWithBuildCredentialsByAppIdentifierIdQueryVariables = Exact<{
@@ -4896,7 +4807,7 @@ export type CommonIosAppCredentialsWithBuildCredentialsByAppIdentifierIdQueryVar
export type CommonIosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byFullName: (
{ __typename?: 'App' }
@@ -4907,7 +4818,7 @@ export type CommonIosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery =
& CommonIosAppCredentialsFragment
)> }
) }
) }
)> }
);
export type CreateAppMutationVariables = Exact<{
@@ -5028,20 +4939,6 @@ export type DeleteEnvironmentSecretMutation = (
) }
);
export type CreateKeystoreGenerationUrlMutationVariables = Exact<{ [key: string]: never; }>;
export type CreateKeystoreGenerationUrlMutation = (
{ __typename?: 'RootMutation' }
& { keystoreGenerationUrl: (
{ __typename?: 'KeystoreGenerationUrlMutation' }
& { createKeystoreGenerationUrl: (
{ __typename?: 'KeystoreGenerationUrl' }
& Pick<KeystoreGenerationUrl, 'id' | 'url'>
) }
) }
);
export type GetSignedUploadMutationVariables = Exact<{
contentTypes: Array<Scalars['String']>;
}>;
@@ -5210,7 +5107,7 @@ export type GetAllBuildsForAppQueryVariables = Exact<{
export type GetAllBuildsForAppQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
@@ -5221,7 +5118,7 @@ export type GetAllBuildsForAppQuery = (
& BuildFragment
)> }
) }
) }
)> }
);
export type EnvironmentSecretsByAccountNameQueryVariables = Exact<{
@@ -5252,7 +5149,7 @@ export type EnvironmentSecretsByAppIdQueryVariables = Exact<{
export type EnvironmentSecretsByAppIdQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
@@ -5263,7 +5160,7 @@ export type EnvironmentSecretsByAppIdQuery = (
& EnvironmentSecretFragment
)> }
) }
) }
)> }
);
export type ProjectByUsernameAndSlugQueryVariables = Exact<{
@@ -5330,7 +5227,7 @@ export type GetAllSubmissionsForAppQueryVariables = Exact<{
export type GetAllSubmissionsForAppQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
@@ -5341,7 +5238,7 @@ export type GetAllSubmissionsForAppQuery = (
& SubmissionFragment
)> }
) }
) }
)> }
);
export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
@@ -5374,7 +5271,7 @@ export type WebhooksByAppIdQueryVariables = Exact<{
export type WebhooksByAppIdQuery = (
{ __typename?: 'RootQuery' }
& { app: (
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
@@ -5385,7 +5282,7 @@ export type WebhooksByAppIdQuery = (
& WebhookFragment
)> }
) }
) }
)> }
);
export type WebhookByIdQueryVariables = Exact<{
@@ -1,26 +0,0 @@
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import { CreateKeystoreGenerationUrlMutation } from '../generated';
export const KeystoreGenerationUrlMutation = {
async createKeystoreGenerationUrlAsync(): Promise<string> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateKeystoreGenerationUrlMutation>(
gql`
mutation CreateKeystoreGenerationUrlMutation {
keystoreGenerationUrl {
createKeystoreGenerationUrl {
id
url
}
}
}
`
)
.toPromise()
);
return data.keystoreGenerationUrl.createKeystoreGenerationUrl.url;
},
};
+4 -66
View File
@@ -1,7 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { withAnalyticsAsync } from '../analytics/common';
import { Event, SubmissionEvent } from '../analytics/events';
import {
AndroidSubmissionConfigInput,
IosSubmissionConfigInput,
@@ -20,58 +18,12 @@ export interface SubmissionInput<P extends Platform> {
: IosSubmissionConfigInput;
buildId?: string;
}
export default abstract class BaseSubmitter<P extends Platform, SubmissionOptions> {
constructor(protected ctx: SubmissionContext<P>, protected options: SubmissionOptions) {}
interface AnalyticEvents {
attemptEvent: Event;
successEvent: Event;
failureEvent: Event;
}
public abstract submitAsync(): Promise<SubmissionFragment>;
export default abstract class BaseSubmitter<
P extends Platform,
ResolvedSourceOptions,
SubmissionOptions
> {
constructor(
protected ctx: SubmissionContext<P>,
protected options: SubmissionOptions,
protected sourceOptionResolver: {
[K in keyof ResolvedSourceOptions]: () => Promise<ResolvedSourceOptions[K]>;
},
protected sourceOptionAnalytics: Record<keyof ResolvedSourceOptions, AnalyticEvents>
) {}
private async getSourceOptionsAsync(): Promise<ResolvedSourceOptions> {
const resolvedSourceOptions: ResolvedSourceOptions = {} as ResolvedSourceOptions;
// Do not perform this in parallel as some of these require user interaction
for (const key in this.sourceOptionResolver) {
const sourceOptionKey = key as keyof ResolvedSourceOptions;
const sourceOptionAnalytics = this.sourceOptionAnalytics[sourceOptionKey];
const sourceOption = await withAnalyticsAsync<
ResolvedSourceOptions[keyof ResolvedSourceOptions]
>(async () => await this.sourceOptionResolver[sourceOptionKey](), {
attemptEvent: sourceOptionAnalytics.attemptEvent,
successEvent: sourceOptionAnalytics.successEvent,
failureEvent: sourceOptionAnalytics.failureEvent,
trackingCtx: this.ctx.trackingCtx,
});
resolvedSourceOptions[sourceOptionKey] = sourceOption;
}
return resolvedSourceOptions;
}
public async submitAsync(): Promise<SubmissionFragment> {
const resolvedSourceOptions = await this.getSourceOptionsAsync();
const input = await this.createSubmissionInputAsync(resolvedSourceOptions);
return await this.createSubmissionWithAnalyticsAsync(input);
}
public abstract createSubmissionInputAsync(
resolvedOptions: ResolvedSourceOptions
): Promise<SubmissionInput<P>>;
private async createSubmissionAsync(
protected async createSubmissionAsync(
submissionInput: SubmissionInput<P>
): Promise<SubmissionFragment> {
Log.addNewLineIfNone();
@@ -87,20 +39,6 @@ export default abstract class BaseSubmitter<
}
}
private async createSubmissionWithAnalyticsAsync(
submissionInput: SubmissionInput<P>
): Promise<SubmissionFragment> {
return await withAnalyticsAsync<SubmissionFragment>(
async () => this.createSubmissionAsync(submissionInput),
{
attemptEvent: SubmissionEvent.SUBMIT_REQUEST_ATTEMPT,
successEvent: SubmissionEvent.SUBMIT_REQUEST_SUCCESS,
failureEvent: SubmissionEvent.SUBMIT_REQUEST_FAIL,
trackingCtx: this.ctx.trackingCtx,
}
);
}
protected abstract createPlatformSubmissionAsync(
input: SubmissionInput<P>
): Promise<SubmissionFragment>;
@@ -1,7 +1,6 @@
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import { SubmissionEvent } from '../../analytics/events';
import {
AndroidSubmissionConfigInput,
SubmissionAndroidReleaseStatus,
@@ -12,7 +11,6 @@ import { SubmissionMutation } from '../../graphql/mutations/SubmissionMutation';
import formatFields from '../../utils/formatFields';
import { Archive, ArchiveSource, getArchiveAsync } from '../ArchiveSource';
import BaseSubmitter, { SubmissionInput } from '../BaseSubmitter';
import { SubmissionContext } from '../context';
import {
ArchiveSourceSummaryFields,
formatArchiveSourceSummary,
@@ -24,6 +22,7 @@ import {
ServiceAccountSource,
getServiceAccountKeyResultAsync,
} from './ServiceAccountSource';
export interface AndroidSubmissionOptions
extends Pick<
AndroidSubmissionConfigInput,
@@ -36,58 +35,32 @@ export interface AndroidSubmissionOptions
}
interface ResolvedSourceOptions {
androidPackage: string;
archive: Archive;
serviceAccountKeyResult: ServiceAccountKeyResult;
}
export default class AndroidSubmitter extends BaseSubmitter<
Platform.ANDROID,
ResolvedSourceOptions,
AndroidSubmissionOptions
> {
constructor(ctx: SubmissionContext<Platform.ANDROID>, options: AndroidSubmissionOptions) {
const sourceOptionsResolver = {
// eslint-disable-next-line async-protect/async-suffix
archive: async () => await getArchiveAsync(this.options.archiveSource),
// eslint-disable-next-line async-protect/async-suffix
serviceAccountKeyResult: async () => {
const androidPackage = await getAndroidPackageAsync(this.options.androidPackageSource);
return await getServiceAccountKeyResultAsync(
this.ctx,
this.options.serviceAccountSource,
androidPackage
);
},
};
const sourceOptionsAnalytics = {
archive: {
attemptEvent: SubmissionEvent.GATHER_ARCHIVE_ATTEMPT,
successEvent: SubmissionEvent.GATHER_ARCHIVE_SUCCESS,
failureEvent: SubmissionEvent.GATHER_ARCHIVE_FAIL,
},
serviceAccountKeyResult: {
attemptEvent: SubmissionEvent.GATHER_CREDENTIALS_ATTEMPT,
successEvent: SubmissionEvent.GATHER_CREDENTIALS_SUCCESS,
failureEvent: SubmissionEvent.GATHER_CREDENTIALS_FAIL,
},
};
super(ctx, options, sourceOptionsResolver, sourceOptionsAnalytics);
}
public async createSubmissionInputAsync(
resolvedSourceOptions: ResolvedSourceOptions
): Promise<SubmissionInput<Platform.ANDROID>> {
const submissionConfig = await this.formatSubmissionConfig(this.options, resolvedSourceOptions);
async submitAsync(): Promise<SubmissionFragment> {
const resolvedSourceOptions = await this.resolveSourceOptionsAsync();
const submissionConfig = await this.formatSubmissionConfigAsync(
this.options,
resolvedSourceOptions
);
printSummary(
this.prepareSummaryData(this.options, resolvedSourceOptions),
SummaryHumanReadableKeys
);
return {
return await this.createSubmissionAsync({
projectId: this.options.projectId,
submissionConfig,
buildId: resolvedSourceOptions.archive.build?.id,
};
});
}
protected async createPlatformSubmissionAsync({
@@ -102,12 +75,28 @@ export default class AndroidSubmitter extends BaseSubmitter<
});
}
private formatSubmissionConfig(
private async resolveSourceOptionsAsync(): Promise<ResolvedSourceOptions> {
const androidPackage = await getAndroidPackageAsync(this.options.androidPackageSource);
const archive = await getArchiveAsync(this.options.archiveSource);
const serviceAccountKeyResult = await getServiceAccountKeyResultAsync(
this.ctx,
this.options.serviceAccountSource,
androidPackage
);
return {
androidPackage,
archive,
serviceAccountKeyResult,
};
}
private async formatSubmissionConfigAsync(
options: AndroidSubmissionOptions,
{ archive, serviceAccountKeyResult }: ResolvedSourceOptions
): AndroidSubmissionConfigInput {
{ archive, androidPackage, serviceAccountKeyResult }: ResolvedSourceOptions
): Promise<AndroidSubmissionConfigInput> {
const { track, releaseStatus, changesNotSentForReview } = options;
return {
applicationIdentifier: androidPackage,
archiveUrl: archive.url,
track,
changesNotSentForReview,
@@ -118,13 +107,14 @@ export default class AndroidSubmitter extends BaseSubmitter<
private prepareSummaryData(
options: AndroidSubmissionOptions,
{ archive, serviceAccountKeyResult }: ResolvedSourceOptions
{ archive, androidPackage, serviceAccountKeyResult }: ResolvedSourceOptions
): SummaryData {
const { projectId, track, releaseStatus, changesNotSentForReview } = options;
// structuring order affects table rows order
return {
projectId,
androidPackage,
track,
changesNotSentForReview: changesNotSentForReview ?? undefined,
releaseStatus: releaseStatus ?? undefined,
@@ -135,6 +125,7 @@ export default class AndroidSubmitter extends BaseSubmitter<
}
type SummaryData = {
androidPackage: string;
changesNotSentForReview?: boolean;
formattedServiceAccount: string;
projectId: string;
@@ -143,6 +134,7 @@ type SummaryData = {
} & ArchiveSourceSummaryFields;
const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
androidPackage: 'Android package',
archivePath: 'Archive path',
archiveUrl: 'Download URL',
changesNotSentForReview: 'Changes not sent for a review',
@@ -126,6 +126,7 @@ describe(AndroidSubmitCommand, () => {
appId: projectId,
config: {
archiveUrl: 'http://expo.dev/fake.apk',
applicationIdentifier: testProject.appJSON.expo.android?.package,
googleServiceAccountKeyJson: fakeFiles['/google-service-account.json'],
releaseStatus: SubmissionAndroidReleaseStatus.Draft,
track: SubmissionAndroidTrack.Internal,
@@ -159,6 +160,7 @@ describe(AndroidSubmitCommand, () => {
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith({
appId: projectId,
config: {
applicationIdentifier: testProject.appJSON.expo.android?.package,
googleServiceAccountKeyJson: fakeFiles['/google-service-account.json'],
releaseStatus: SubmissionAndroidReleaseStatus.Draft,
track: SubmissionAndroidTrack.Internal,
-18
View File
@@ -1,14 +1,10 @@
import { ExpoConfig } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { SubmitProfile } from '@expo/eas-json';
import { v4 as uuidv4 } from 'uuid';
import { TrackingContext } from '../analytics/common';
import { Analytics, SubmissionEvent } from '../analytics/events';
import { CredentialsContext } from '../credentials/context';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName } from '../project/projectUtils';
import { findAccountByName } from '../user/Account';
import { Actor } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
@@ -16,7 +12,6 @@ export interface SubmissionContext<T extends Platform> {
accountName: string;
archiveFlags: SubmitArchiveFlags;
credentialsCtx: CredentialsContext;
trackingCtx: TrackingContext;
exp: ExpoConfig;
nonInteractive: boolean;
platform: T;
@@ -50,22 +45,10 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
const user = await ensureLoggedInAsync();
const projectName = exp.slug;
const accountName = getProjectAccountName(exp, user);
const accountId = findAccountByName(user.accounts, accountName)?.id;
let credentialsCtx: CredentialsContext | undefined = params.credentialsCtx;
if (!credentialsCtx) {
credentialsCtx = new CredentialsContext({ projectDir, user, exp, nonInteractive });
}
const trackingCtx = {
tracking_id: uuidv4(),
platform: params.platform,
...(accountId && { account_id: accountId }),
account_name: accountName,
project_id: params.projectId,
};
Analytics.logEvent(SubmissionEvent.SUBMIT_COMMAND, trackingCtx);
return {
...rest,
accountName,
@@ -73,6 +56,5 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
exp,
projectName,
user,
trackingCtx,
};
}
@@ -1,9 +1,3 @@
import { Platform } from '@expo/eas-build-job';
import getenv from 'getenv';
import { promptAsync } from '../../prompts';
import UserSettings from '../../user/UserSettings';
import { SubmissionContext } from '../context';
export enum AppSpecificPasswordSourceType {
userDefined,
}
@@ -17,62 +11,15 @@ interface AppSpecificPasswordUserDefinedSource extends AppSpecificPasswordSource
appSpecificPassword: string;
}
export interface AppSpecificPasswordCredentials {
password: string;
appleIdUsername: string;
}
export type AppSpecificPasswordSource = AppSpecificPasswordUserDefinedSource;
export async function getAppSpecificPasswordLocallyAsync(
ctx: SubmissionContext<Platform.IOS>,
export async function getAppSpecificPasswordAsync(
source: AppSpecificPasswordSource
): Promise<AppSpecificPasswordCredentials> {
): Promise<string> {
if (source.sourceType === AppSpecificPasswordSourceType.userDefined) {
return {
password: source.appSpecificPassword,
appleIdUsername: await getAppleIdUsernameAsync(ctx),
};
return source.appSpecificPassword;
} else {
// exhaustive -- should never happen
throw new Error(`Unknown app specific password source type "${(source as any)?.sourceType}"`);
}
}
export async function getAppleIdUsernameAsync(
ctx: SubmissionContext<Platform.IOS>
): Promise<string> {
if (ctx.profile.appleId) {
return ctx.profile.appleId;
}
const envAppleId = getenv.string('EXPO_APPLE_ID', '');
if (envAppleId) {
return envAppleId;
}
if (ctx.credentialsCtx.appStore.authCtx?.appleId) {
return ctx.credentialsCtx.appStore.authCtx.appleId;
}
// Get the email address that was last used and set it as
// the default value for quicker authentication.
const lastAppleId = await UserSettings.getAsync('appleId', null);
if (ctx.nonInteractive) {
if (lastAppleId) {
return lastAppleId;
} else {
throw new Error('Set appleId in the submit profile (eas.json).');
}
}
const { appleId } = await promptAsync({
type: 'text',
name: 'appleId',
message: `Enter your Apple ID:`,
validate: (val: string) => !!val,
initial: lastAppleId ?? undefined,
});
return appleId;
}
@@ -1,21 +1,33 @@
import { Platform } from '@expo/eas-build-job';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { promptForAscApiKeyPathAsync } from '../../credentials/ios/actions/AscApiKeyUtils';
import {
AppStoreApiKeyPurpose,
promptForAscApiKeyPathAsync,
} from '../../credentials/ios/actions/AscApiKeyUtils';
import { SetUpAscApiKey } from '../../credentials/ios/actions/SetUpAscApiKey';
import { AscApiKeyPath, MinimalAscApiKey } from '../../credentials/ios/credentials';
import Log from '../../log';
import { getBundleIdentifierAsync } from '../../project/ios/bundleIdentifier';
import { findAccountByName } from '../../user/Account';
import { SubmissionContext } from '../context';
import { isExistingFileAsync } from '../utils/files';
export enum AscApiKeySourceType {
path,
prompt,
credentialsService,
}
interface AscApiKeySourceBase {
sourceType: AscApiKeySourceType;
}
interface AscApiKeyCredentialsServiceSource extends AscApiKeySourceBase {
sourceType: AscApiKeySourceType.credentialsService;
}
interface AscApiKeyPromptSource extends AscApiKeySourceBase {
sourceType: AscApiKeySourceType.prompt;
}
@@ -25,7 +37,10 @@ interface AscApiKeyEnvVarSource extends AscApiKeySourceBase {
path: AscApiKeyPath;
}
export type AscApiKeySource = AscApiKeyEnvVarSource | AscApiKeyPromptSource;
export type AscApiKeySource =
| AscApiKeyEnvVarSource
| AscApiKeyPromptSource
| AscApiKeyCredentialsServiceSource;
type AscApiKeySummary = {
source: 'local' | 'EAS servers';
@@ -43,7 +58,55 @@ export type AscApiKeyResult = {
summary: AscApiKeySummary;
};
export async function getAscApiKeyLocallyAsync(
export async function getAscApiKeyResultAsync(
ctx: SubmissionContext<Platform.IOS>,
source: AscApiKeySource
): Promise<AscApiKeyResult> {
if (source.sourceType === AscApiKeySourceType.credentialsService) {
return await getAscApiKeyFromCredentialsServiceAsync(ctx);
} else {
return await getAscApiKeyLocallyAsync(ctx, source);
}
}
async function getAscApiKeyFromCredentialsServiceAsync(
ctx: SubmissionContext<Platform.IOS>
): Promise<AscApiKeyResult> {
const bundleIdentifier = await getBundleIdentifierAsync(ctx.projectDir, ctx.exp);
Log.log(`Looking up credentials configuration for ${bundleIdentifier}...`);
const appLookupParams = {
account: nullthrows(
findAccountByName(ctx.user.accounts, ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.projectName,
bundleIdentifier,
};
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
const iosAppCredentials = await setupAscApiKeyAction.runAsync(ctx.credentialsCtx);
const ascKeyForSubmissions = nullthrows(
iosAppCredentials.appStoreConnectApiKeyForSubmissions,
`An EAS Submit ASC Api Key could not be found for ${iosAppCredentials.appleAppIdentifier.bundleIdentifier}`
);
const { id, keyIdentifier, name } = ascKeyForSubmissions;
Log.log(`Using Api Key ID: ${keyIdentifier}${name ? ` (${name})` : ''}`);
return {
result: {
ascApiKeyId: id,
},
summary: {
source: 'EAS servers',
keyId: keyIdentifier,
name: name ?? undefined,
},
};
}
async function getAscApiKeyLocallyAsync(
ctx: SubmissionContext<Platform.IOS>,
source: AscApiKeySource
): Promise<AscApiKeyResult> {
@@ -70,6 +133,8 @@ export async function getAscApiKeyPathAsync(
return await handlePathSourceAsync(ctx, source);
case AscApiKeySourceType.prompt:
return await handlePromptSourceAsync(ctx, source);
case AscApiKeySourceType.credentialsService:
throw new Error(`AscApiKeySourceType ${source} does not return a path.`);
}
}
@@ -1,67 +0,0 @@
import { Platform } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
import { SetUpSubmissionCredentials } from '../../credentials/ios/actions/SetUpSubmissionCredentials';
import Log from '../../log';
import { getBundleIdentifierAsync } from '../../project/ios/bundleIdentifier';
import { findAccountByName } from '../../user/Account';
import { SubmissionContext } from '../context';
import {
AppSpecificPasswordCredentials,
getAppleIdUsernameAsync,
} from './AppSpecificPasswordSource';
import { AscApiKeyResult } from './AscApiKeySource';
/**
* The Credentials Service will either return an ASC API Key or an App Specific Password
* When we no longer support the App Specific Password user prompt, refactor this into the AscApiKeySource
*/
export const CREDENTIALS_SERVICE_SOURCE = 'CREDENTIALS_SERVICE_SOURCE';
export type CredentialsServiceSource = typeof CREDENTIALS_SERVICE_SOURCE;
export async function getFromCredentialsServiceAsync(
ctx: SubmissionContext<Platform.IOS>
): Promise<
{ appSpecificPassword: AppSpecificPasswordCredentials } | { ascApiKeyResult: AscApiKeyResult }
> {
const bundleIdentifier = await getBundleIdentifierAsync(ctx.projectDir, ctx.exp);
Log.log(`Looking up credentials configuration for ${bundleIdentifier}...`);
const appLookupParams = {
account: nullthrows(
findAccountByName(ctx.user.accounts, ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.projectName,
bundleIdentifier,
};
const setupSubmissionCredentialsAction = new SetUpSubmissionCredentials(appLookupParams);
const ascOrAsp = await setupSubmissionCredentialsAction.runAsync(ctx.credentialsCtx);
const isAppSpecificPassword = typeof ascOrAsp === 'string';
if (isAppSpecificPassword) {
return {
appSpecificPassword: {
password: ascOrAsp,
appleIdUsername: await getAppleIdUsernameAsync(ctx),
},
};
} else {
const ascKeyForSubmissions = nullthrows(
ascOrAsp.appStoreConnectApiKeyForSubmissions,
`An EAS Submit ASC API Key could not be found for ${ascOrAsp.appleAppIdentifier.bundleIdentifier}`
);
const { id, keyIdentifier, name } = ascKeyForSubmissions;
return {
ascApiKeyResult: {
result: {
ascApiKeyId: id,
},
summary: {
source: 'EAS servers',
keyId: keyIdentifier,
name: name ?? undefined,
},
},
};
}
}
@@ -7,6 +7,8 @@ import wrapAnsi from 'wrap-ansi';
import { MissingCredentialsError } from '../../credentials/errors';
import { SubmissionFragment } from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import { promptAsync } from '../../prompts';
import UserSettings from '../../user/UserSettings';
import { ArchiveSource } from '../ArchiveSource';
import { resolveArchiveSource } from '../commons';
import { SubmissionContext } from '../context';
@@ -16,7 +18,6 @@ import {
AppSpecificPasswordSourceType,
} from './AppSpecificPasswordSource';
import { AscApiKeySource, AscApiKeySourceType } from './AscApiKeySource';
import { CREDENTIALS_SERVICE_SOURCE, CredentialsServiceSource } from './CredentialsServiceSource';
import IosSubmitter, { IosSubmissionOptions } from './IosSubmitter';
export default class IosSubmitCommand {
@@ -29,17 +30,11 @@ export default class IosSubmitCommand {
return await submitter.submitAsync();
}
private resolveCredentialSubmissionOptions():
private async resolveCredentialSubmissionOptionsAsync(): Promise<
| { appSpecificPasswordSource: Result<AppSpecificPasswordSource> }
| { ascApiKeySource: Result<AscApiKeySource> }
| { credentialsServiceSource: Result<CredentialsServiceSource> } {
const ascApiKeySource = this.resolveAscApiKeySource();
const shouldSkipAscApiKeySource =
!ascApiKeySource.ok && ascApiKeySource.enforceError() instanceof MissingCredentialsError;
if (!shouldSkipAscApiKeySource) {
return { ascApiKeySource };
}
> {
// if an App Specific Password env var is not specified, use ASC Api Key
const appSpecificPasswordSource = this.resolveAppSpecificPasswordSource();
const shouldSkipAppSpecificPasswordSource =
!appSpecificPasswordSource.ok &&
@@ -48,32 +43,28 @@ export default class IosSubmitCommand {
return { appSpecificPasswordSource: this.resolveAppSpecificPasswordSource() };
}
return {
credentialsServiceSource: result(CREDENTIALS_SERVICE_SOURCE),
};
const ascApiKeySource = this.resolveAscApiKeySource();
return { ascApiKeySource };
}
private async resolveSubmissionOptionsAsync(): Promise<IosSubmissionOptions> {
const archiveSource = this.resolveArchiveSource();
const credentialsSource = this.resolveCredentialSubmissionOptions();
const credentialsSource = await this.resolveCredentialSubmissionOptionsAsync();
const maybeAppSpecificPasswordSource =
'appSpecificPasswordSource' in credentialsSource
? credentialsSource.appSpecificPasswordSource
: null;
const maybeAscApiKeySource =
'ascApiKeySource' in credentialsSource ? credentialsSource.ascApiKeySource : null;
const maybeCredentialsServiceSource =
'credentialsServiceSource' in credentialsSource
? credentialsSource.credentialsServiceSource
: null;
const ascAppIdentifier = await this.resolveAscAppIdentifierAsync();
const appleIdUsername = await this.resolveAppleIdUsernameAsync();
const errored = [
archiveSource,
...(maybeAppSpecificPasswordSource ? [maybeAppSpecificPasswordSource] : []),
...(maybeAscApiKeySource ? [maybeAscApiKeySource] : []),
...(maybeCredentialsServiceSource ? [maybeCredentialsServiceSource] : []),
ascAppIdentifier,
appleIdUsername,
].filter(r => !r.ok);
if (errored.length > 0) {
const message = errored.map(err => err.reason?.message).join('\n');
@@ -83,6 +74,7 @@ export default class IosSubmitCommand {
return {
projectId: this.ctx.projectId,
appleIdUsername: appleIdUsername.enforceValue(),
ascAppIdentifier: ascAppIdentifier.enforceValue(),
archiveSource: archiveSource.enforceValue(),
...(maybeAppSpecificPasswordSource
@@ -95,11 +87,6 @@ export default class IosSubmitCommand {
ascApiKeySource: maybeAscApiKeySource.enforceValue(),
}
: null),
...(maybeCredentialsServiceSource
? {
credentialsServiceSource: maybeCredentialsServiceSource.enforceValue(),
}
: null),
};
}
@@ -142,11 +129,7 @@ export default class IosSubmitCommand {
});
}
return result(
new MissingCredentialsError(
'Set the ascApiKeyPath, ascApiKeyIssuerId and ascApiKeyId fields in eas.json.'
)
);
return result({ sourceType: AscApiKeySourceType.credentialsService });
}
private resolveArchiveSource(): Result<ArchiveSource> {
@@ -181,4 +164,40 @@ export default class IosSubmitCommand {
}
}
}
private async resolveAppleIdUsernameAsync(): Promise<Result<string>> {
if (this.ctx.profile.appleId) {
return result(this.ctx.profile.appleId);
}
const envAppleId = getenv.string('EXPO_APPLE_ID', '');
if (envAppleId) {
return result(envAppleId);
}
if (this.ctx.credentialsCtx.appStore.authCtx?.appleId) {
return result(this.ctx.credentialsCtx.appStore.authCtx.appleId);
}
// Get the email address that was last used and set it as
// the default value for quicker authentication.
const lastAppleId = await UserSettings.getAsync('appleId', null);
if (this.ctx.nonInteractive) {
if (lastAppleId) {
return result(lastAppleId);
} else {
return result(new Error('Set appleId in the submit profile (eas.json).'));
}
}
const { appleId } = await promptAsync({
type: 'text',
name: 'appleId',
message: `Enter your Apple ID:`,
validate: (val: string) => !!val,
initial: lastAppleId ?? undefined,
});
return result(appleId);
}
}
+36 -84
View File
@@ -1,34 +1,27 @@
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import { SubmissionEvent } from '../../analytics/events';
import { MinimalAscApiKey } from '../../credentials/ios/credentials';
import { IosSubmissionConfigInput, SubmissionFragment } from '../../graphql/generated';
import { SubmissionMutation } from '../../graphql/mutations/SubmissionMutation';
import formatFields from '../../utils/formatFields';
import { Archive, ArchiveSource, getArchiveAsync } from '../ArchiveSource';
import BaseSubmitter, { SubmissionInput } from '../BaseSubmitter';
import { SubmissionContext } from '../context';
import {
ArchiveSourceSummaryFields,
formatArchiveSourceSummary,
printSummary,
} from '../utils/summary';
import {
AppSpecificPasswordCredentials,
AppSpecificPasswordSource,
getAppSpecificPasswordLocallyAsync,
getAppSpecificPasswordAsync,
} from './AppSpecificPasswordSource';
import {
AscApiKeyFromExpoServers,
AscApiKeyResult,
AscApiKeySource,
getAscApiKeyLocallyAsync,
getAscApiKeyResultAsync,
} from './AscApiKeySource';
import {
CredentialsServiceSource,
getFromCredentialsServiceAsync,
} from './CredentialsServiceSource';
export interface IosSubmissionOptions
extends Pick<IosSubmissionConfigInput, 'appleIdUsername' | 'ascAppIdentifier'> {
@@ -36,77 +29,32 @@ export interface IosSubmissionOptions
archiveSource: ArchiveSource;
appSpecificPasswordSource?: AppSpecificPasswordSource;
ascApiKeySource?: AscApiKeySource;
credentialsServiceSource?: CredentialsServiceSource;
}
interface ResolvedSourceOptions {
archive: Archive;
credentials: {
appSpecificPassword?: AppSpecificPasswordCredentials;
ascApiKeyResult?: AscApiKeyResult;
};
appSpecificPassword?: string;
ascApiKeyResult?: AscApiKeyResult;
}
export default class IosSubmitter extends BaseSubmitter<
Platform.IOS,
ResolvedSourceOptions,
IosSubmissionOptions
> {
constructor(ctx: SubmissionContext<Platform.IOS>, options: IosSubmissionOptions) {
const sourceOptionsResolver = {
// eslint-disable-next-line async-protect/async-suffix
archive: async () => await getArchiveAsync(this.options.archiveSource),
// eslint-disable-next-line async-protect/async-suffix
credentials: async () => {
const maybeAppSpecificPassword = this.options.appSpecificPasswordSource
? await getAppSpecificPasswordLocallyAsync(
this.ctx,
this.options.appSpecificPasswordSource
)
: null;
const maybeAppStoreConnectApiKey = this.options.ascApiKeySource
? await getAscApiKeyLocallyAsync(this.ctx, this.options.ascApiKeySource)
: null;
const maybeAscOrAspFromCredentialsService = this.options.credentialsServiceSource
? await getFromCredentialsServiceAsync(this.ctx)
: null;
return {
...(maybeAppSpecificPassword ? { appSpecificPassword: maybeAppSpecificPassword } : null),
...(maybeAppStoreConnectApiKey ? { ascApiKeyResult: maybeAppStoreConnectApiKey } : null),
...(maybeAscOrAspFromCredentialsService ? maybeAscOrAspFromCredentialsService : null),
};
},
};
const sourceOptionsAnalytics = {
archive: {
attemptEvent: SubmissionEvent.GATHER_ARCHIVE_ATTEMPT,
successEvent: SubmissionEvent.GATHER_ARCHIVE_SUCCESS,
failureEvent: SubmissionEvent.GATHER_ARCHIVE_FAIL,
},
credentials: {
attemptEvent: SubmissionEvent.GATHER_CREDENTIALS_ATTEMPT,
successEvent: SubmissionEvent.GATHER_CREDENTIALS_SUCCESS,
failureEvent: SubmissionEvent.GATHER_CREDENTIALS_FAIL,
},
};
super(ctx, options, sourceOptionsResolver, sourceOptionsAnalytics);
}
public async createSubmissionInputAsync(
resolvedSourceOptions: ResolvedSourceOptions
): Promise<SubmissionInput<Platform.IOS>> {
const submissionConfig = this.formatSubmissionConfig(this.options, resolvedSourceOptions);
export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmissionOptions> {
async submitAsync(): Promise<SubmissionFragment> {
const resolvedSourceOptions = await this.resolveSourceOptionsAsync();
const submissionConfig = await this.formatSubmissionConfigAsync(
this.options,
resolvedSourceOptions
);
printSummary(
this.prepareSummaryData(this.options, resolvedSourceOptions),
SummaryHumanReadableKeys
);
return {
return await this.createSubmissionAsync({
projectId: this.options.projectId,
submissionConfig,
buildId: resolvedSourceOptions.archive.build?.id,
};
});
}
protected async createPlatformSubmissionAsync({
@@ -121,30 +69,35 @@ export default class IosSubmitter extends BaseSubmitter<
});
}
private formatSubmissionConfig(
private async resolveSourceOptionsAsync(): Promise<ResolvedSourceOptions> {
const archive = await getArchiveAsync(this.options.archiveSource);
const maybeAppSpecificPassword = this.options.appSpecificPasswordSource
? await getAppSpecificPasswordAsync(this.options.appSpecificPasswordSource)
: null;
const maybeAppStoreConnectApiKey = this.options.ascApiKeySource
? await getAscApiKeyResultAsync(this.ctx, this.options.ascApiKeySource)
: null;
return {
archive,
...(maybeAppSpecificPassword ? { appSpecificPassword: maybeAppSpecificPassword } : null),
...(maybeAppStoreConnectApiKey ? { ascApiKeyResult: maybeAppStoreConnectApiKey } : null),
};
}
private async formatSubmissionConfigAsync(
options: IosSubmissionOptions,
{ archive, credentials }: ResolvedSourceOptions
): IosSubmissionConfigInput {
const { appSpecificPassword, ascApiKeyResult } = credentials;
{ archive, appSpecificPassword, ascApiKeyResult }: ResolvedSourceOptions
): Promise<IosSubmissionConfigInput> {
const { appleIdUsername, ascAppIdentifier } = options;
return {
ascAppIdentifier,
appleIdUsername,
archiveUrl: archive.url,
...(appSpecificPassword ? this.formatAppSpecificPassword(appSpecificPassword) : null),
appleAppSpecificPassword: appSpecificPassword,
...(ascApiKeyResult?.result ? this.formatAscApiKeyResult(ascApiKeyResult.result) : null),
};
}
private formatAppSpecificPassword(
appSpecificPassword: AppSpecificPasswordCredentials
): Pick<IosSubmissionConfigInput, 'appleAppSpecificPassword' | 'appleIdUsername'> {
return {
appleAppSpecificPassword: appSpecificPassword.password,
appleIdUsername: appSpecificPassword.appleIdUsername,
};
}
private formatAscApiKeyResult(
result: MinimalAscApiKey | AscApiKeyFromExpoServers
): Pick<IosSubmissionConfigInput, 'ascApiKey'> | Pick<IosSubmissionConfigInput, 'ascApiKeyId'> {
@@ -161,16 +114,15 @@ export default class IosSubmitter extends BaseSubmitter<
private prepareSummaryData(
options: IosSubmissionOptions,
{ archive, credentials }: ResolvedSourceOptions
{ archive, ascApiKeyResult }: ResolvedSourceOptions
): SummaryData {
const { ascApiKeyResult, appSpecificPassword } = credentials;
const { ascAppIdentifier, projectId } = options;
const { appleIdUsername, ascAppIdentifier, projectId } = options;
// structuring order affects table rows order
return {
ascAppIdentifier,
appleIdUsername: appleIdUsername ?? undefined,
projectId,
...(appSpecificPassword ? { appleIdUsername: appSpecificPassword.appleIdUsername } : null),
...(ascApiKeyResult ? { formattedAscApiKey: formatAscApiKeySummary(ascApiKeyResult) } : null),
...formatArchiveSourceSummary(archive),
};
@@ -191,7 +143,7 @@ const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
archiveUrl: 'Archive URL',
archivePath: 'Archive Path',
formattedBuild: 'Build',
formattedAscApiKey: 'App Store Connect API Key',
formattedAscApiKey: 'App Store Connect Api Key',
};
function formatAscApiKeySummary({ summary }: AscApiKeyResult): string {
@@ -4,15 +4,18 @@ import { v4 as uuidv4 } from 'uuid';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { testCommonIosAppCredentialsFragment } from '../../../credentials/__tests__/fixtures-ios';
import { SetUpAscApiKey } from '../../../credentials/ios/actions/SetUpAscApiKey';
import { getCredentialsFromUserAsync } from '../../../credentials/utils/promptForCredentials';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { getBundleIdentifierAsync } from '../../../project/ios/bundleIdentifier';
import { promptAsync } from '../../../prompts';
import { SubmissionContext, createSubmissionContextAsync } from '../../context';
import {
AscApiKeySource,
AscApiKeySourceType,
getAscApiKeyLocallyAsync,
getAscApiKeyPathAsync,
getAscApiKeyResultAsync,
} from '../AscApiKeySource';
jest.mock('fs');
@@ -24,6 +27,7 @@ jest.mock('../../../user/User', () => ({
jest.mock('../../../user/Account', () => ({
findAccountByName: jest.fn(() => mockJester.accounts[0]),
}));
jest.mock('../../../project/ios/bundleIdentifier');
const testProject = createTestProject(mockJester, {
android: {
@@ -167,14 +171,14 @@ describe(getAscApiKeyPathAsync, () => {
});
});
describe(getAscApiKeyLocallyAsync, () => {
it('returns a local Asc API Key file with a AscApiKeySourceType.path source', async () => {
describe(getAscApiKeyResultAsync, () => {
it('returns a local Asc Api Key file with a AscApiKeySourceType.path source', async () => {
const ctx = await getIosSubmissionContextAsync();
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
const ascApiKeyResult = await getAscApiKeyLocallyAsync(ctx, source);
const ascApiKeyResult = await getAscApiKeyResultAsync(ctx, source);
expect(ascApiKeyResult).toMatchObject({
result: {
keyP8: 'super secret',
@@ -189,7 +193,7 @@ describe(getAscApiKeyLocallyAsync, () => {
});
});
it('returns a local Asc API Key file with a AscApiKeySourceType.prompt source', async () => {
it('returns a local Asc Api Key file with a AscApiKeySourceType.prompt source', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
@@ -201,7 +205,7 @@ describe(getAscApiKeyLocallyAsync, () => {
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.prompt,
};
const serviceAccountResult = await getAscApiKeyLocallyAsync(ctx, source);
const serviceAccountResult = await getAscApiKeyResultAsync(ctx, source);
expect(serviceAccountResult).toMatchObject({
result: {
keyP8: 'super secret',
@@ -215,4 +219,39 @@ describe(getAscApiKeyLocallyAsync, () => {
},
});
});
it('returns an Asc Api Key from server with a AscApiKeySourceType.credentialService source', async () => {
const ctx = await createSubmissionContextAsync({
platform: Platform.IOS,
projectDir: testProject.projectRoot,
projectId,
archiveFlags: {
url: 'http://expo.dev/fake.apk',
},
profile: {
language: 'en-US',
},
nonInteractive: true,
});
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.credentialsService,
};
jest
.spyOn(SetUpAscApiKey.prototype, 'runAsync')
.mockImplementation(async _ctx => testCommonIosAppCredentialsFragment);
asMock(getBundleIdentifierAsync).mockImplementation(() => 'com.hello.world');
const result = await getAscApiKeyResultAsync(ctx, source);
expect(result).toEqual({
result: {
ascApiKeyId: testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.id,
},
summary: {
keyId:
testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.keyIdentifier,
name: testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.name,
source: 'EAS servers',
},
});
});
});
@@ -1,98 +0,0 @@
import { Platform } from '@expo/eas-build-job';
import { v4 as uuidv4 } from 'uuid';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { testCommonIosAppCredentialsFragment } from '../../../credentials/__tests__/fixtures-ios';
import { SetUpSubmissionCredentials } from '../../../credentials/ios/actions/SetUpSubmissionCredentials';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { getBundleIdentifierAsync } from '../../../project/ios/bundleIdentifier';
import { promptAsync } from '../../../prompts';
import { createSubmissionContextAsync } from '../../context';
import { getFromCredentialsServiceAsync } from '../CredentialsServiceSource';
jest.mock('fs');
jest.mock('../../../prompts');
jest.mock('../../../credentials/utils/promptForCredentials');
jest.mock('../../../user/User', () => ({
getUserAsync: jest.fn(() => mockJester),
}));
jest.mock('../../../user/Account', () => ({
findAccountByName: jest.fn(() => mockJester.accounts[0]),
}));
jest.mock('../../../project/ios/bundleIdentifier');
const testProject = createTestProject(mockJester, {
android: {
package: 'com.expo.test.project',
},
});
const mockManifest = { exp: testProject.appJSON.expo };
jest.mock('@expo/config', () => ({
getConfig: jest.fn(() => mockManifest),
}));
const projectId = uuidv4();
beforeEach(() => {
jest.restoreAllMocks();
});
describe(getFromCredentialsServiceAsync, () => {
it('returns an App Specific Password from the credentialService source', async () => {
const ctx = await createSubmissionContextAsync({
platform: Platform.IOS,
projectDir: testProject.projectRoot,
projectId,
archiveFlags: {
url: 'http://expo.dev/fake.apk',
},
profile: {
language: 'en-US',
},
nonInteractive: false,
});
jest
.spyOn(SetUpSubmissionCredentials.prototype, 'runAsync')
.mockImplementation(async _ctx => 'super secret');
asMock(getBundleIdentifierAsync).mockImplementation(() => 'com.hello.world');
asMock(promptAsync).mockImplementationOnce(() => ({ appleId: '[email protected]' }));
const result = await getFromCredentialsServiceAsync(ctx);
expect(result).toEqual({
appSpecificPassword: { password: 'super secret', appleIdUsername: '[email protected]' },
});
});
it('returns an ASC API Key from the credentialService source', async () => {
const ctx = await createSubmissionContextAsync({
platform: Platform.IOS,
projectDir: testProject.projectRoot,
projectId,
archiveFlags: {
url: 'http://expo.dev/fake.apk',
},
profile: {
language: 'en-US',
},
nonInteractive: true,
});
jest
.spyOn(SetUpSubmissionCredentials.prototype, 'runAsync')
.mockImplementation(async _ctx => testCommonIosAppCredentialsFragment);
asMock(getBundleIdentifierAsync).mockImplementation(() => 'com.hello.world');
const result = await getFromCredentialsServiceAsync(ctx);
expect(result).toEqual({
ascApiKeyResult: {
result: {
ascApiKeyId: testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.id,
},
summary: {
keyId:
testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.keyIdentifier,
name: testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.name,
source: 'EAS servers',
},
},
});
});
});
+5 -17
View File
@@ -1,8 +1,6 @@
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import { withAnalyticsAsync } from '../analytics/common';
import { SubmissionEvent } from '../analytics/events';
import { AppPlatform, SubmissionFragment, SubmissionStatus } from '../graphql/generated';
import Log, { link } from '../log';
import { appPlatformDisplayNames, appPlatformEmojis } from '../platform';
@@ -15,21 +13,11 @@ import { waitForSubmissionsEndAsync } from './utils/wait';
export async function submitAsync<T extends Platform>(
ctx: SubmissionContext<T>
): Promise<SubmissionFragment> {
return await withAnalyticsAsync(
async () => {
const command =
ctx.platform === Platform.ANDROID
? new AndroidSubmitCommand(ctx as SubmissionContext<Platform.ANDROID>)
: new IosSubmitCommand(ctx as SubmissionContext<Platform.IOS>);
return command.runAsync();
},
{
attemptEvent: SubmissionEvent.SUBMIT_COMMAND_ATTEMPT,
successEvent: SubmissionEvent.SUBMIT_COMMAND_SUCCESS,
failureEvent: SubmissionEvent.SUBMIT_COMMAND_FAIL,
trackingCtx: ctx.trackingCtx,
}
);
const command =
ctx.platform === Platform.ANDROID
? new AndroidSubmitCommand(ctx as SubmissionContext<Platform.ANDROID>)
: new IosSubmitCommand(ctx as SubmissionContext<Platform.IOS>);
return command.runAsync();
}
export async function waitToCompleteAsync(
+1 -1
View File
@@ -1,6 +1,6 @@
import gql from 'graphql-tag';
import * as Analytics from '../analytics/rudderstackClient';
import * as Analytics from '../analytics';
import { apiClient } from '../api';
import { graphqlClient } from '../graphql/client';
import { CurrentUserQuery } from '../graphql/generated';
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with eas.json",
"version": "0.36.0",
"version": "0.34.1",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
-9
View File
@@ -1,9 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
SCRIPTS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
pushd $SCRIPTS_DIR >/dev/null 2>&1
yarn $@
popd >/dev/null 2>&1
+1 -2
View File
@@ -4,8 +4,7 @@
"version": "0.31.0",
"scripts": {
"changelog-entry": "ts-node src/changelogEntry.ts",
"release-changelog": "ts-node src/releaseChangelog.ts",
"update-local-plugin": "./src/updateLocalPlugin.sh"
"release-changelog": "ts-node src/releaseChangelog.ts"
},
"author": "Expo <[email protected]>",
"license": "MIT",
+1 -1
View File
@@ -10,7 +10,7 @@ const MAIN_CATEGORIES = Object.values(CATEGORY_HEADERS).map(text =>
);
const FORMAT_RELEASE_HEADING = (version: string): markdown.Token =>
markdown.createHeadingToken(
`[${version}](https://github.com/expo/eas-cli/releases/tag/v${version}) - ${dateFormat(
`[${version}](https://github.com/expo/eas-cli/releases/tag/${version}) - ${dateFormat(
'isoDate'
)}`,
2
-24
View File
@@ -1,24 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/../.. && pwd )"
LOCAL_TS_PATH="$ROOT_DIR/packages/eas-cli/src/build/local.ts"
echo "Checking if eas-cli-local-build-plugin upgrade is available"
plugin_version_latest=$(npm info eas-cli-local-build-plugin@latest version)
plugin_version_current=$(grep 'const PLUGIN_PACKAGE_VERSION' $LOCAL_TS_PATH | cut -d"'" -f2)
if [[ "$plugin_version_latest" == "$plugin_version_current" ]]; then
echo "eas-cli-local-build-plugin is already on the latest version - $plugin_version_latest"
else
echo "Bumping eas-cli-local-build-plugin from $plugin_version_current to $plugin_version_latest"
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "s/$plugin_version_current/$plugin_version_latest/g" $LOCAL_TS_PATH
else
sed -i "s/$plugin_version_current/$plugin_version_latest/g" $LOCAL_TS_PATH
fi
git add $LOCAL_TS_PATH
git commit -m "upgrade eas-cli-local-build-plugin to $plugin_version_latest"
fi