Compare commits

..
2 Commits
Author SHA1 Message Date
Dominik Sokal 9b36ae0ef8 convert to ESM 2021-10-11 16:15:21 +02:00
Dominik Sokal 1a4cb107af add clean script 2021-10-11 16:12:42 +02:00
205 changed files with 4023 additions and 7419 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
+2 -2
View File
@@ -28,8 +28,8 @@ jobs:
echo "Invalid /changelog-entry command"
exit 1
fi
CATEGORY=$( cut -d ' ' -f 1 <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
CATEGORY=$( cut -d ' ' -f 1 <<< "$PARAMS" | xargs )
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | xargs )
yarn changelog-entry "$CATEGORY" "$MESSAGE"
env:
GITHUB_PR_AUTHOR: ${{ github.event.issue.user.login }}
+4 -127
View File
@@ -10,142 +10,19 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 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))
- 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))
### 🧹 Chores
- Clean up credentials prompt method. ([#728](https://github.com/expo/eas-cli/pull/728) by [@quinlanj](https://github.com/quinlanj))
- Bump @expo/apple-utils to 0.0.0-alpha.26. ([#723](https://github.com/expo/eas-cli/pull/723) by [@brentvatne](https://github.com/brentvatne))
- Grammar: replace setup with set up. ([#735](https://github.com/expo/eas-cli/pull/735) by [@quinlanj](https://github.com/quinlanj))
- Improve VCS workflow migration experience. ([#732](https://github.com/expo/eas-cli/pull/732) by [@wkozyra95](https://github.com/wkozyra95))
## [0.34.1](https://github.com/expo/eas-cli/releases/tag/v0.34.1) - 2021-11-02
### 🐛 Bug fixes
- Don't show commit prompt in no-commit workflow after installing `expo-dev-client`. ([#722](https://github.com/expo/eas-cli/pull/722) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Add App Store Connect API Key graphql type and print support. ([#717](https://github.com/expo/eas-cli/pull/717) by [@quinlanj](https://github.com/quinlanj))
## [0.34.0](https://github.com/expo/eas-cli/releases/tag/v0.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))
- 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))
### 🐛 Bug fixes
- Better error message when eas.json is invalid. ([#707](https://github.com/expo/eas-cli/pull/707) by [@dsokal](https://github.com/dsokal))
- Fix credentials workflow for new users: add additionalTypenames for credentials. ([#703](https://github.com/expo/eas-cli/pull/703) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Add additionalTypenames for other queries. ([#704](https://github.com/expo/eas-cli/pull/704) by [@quinlanj](https://github.com/quinlanj))
- Fix errors after pulling down most recent graphql schema changes. ([#713](https://github.com/expo/eas-cli/pull/713) by [@quinlanj](https://github.com/quinlanj))
- Refactor credentials manager. ([#712](https://github.com/expo/eas-cli/pull/712) by [@quinlanj](https://github.com/quinlanj))
- Remove unneeded parameters. ([#710](https://github.com/expo/eas-cli/pull/710) by [@quinlanj](https://github.com/quinlanj))
- Enforce additionalTypenames for graphql queries. ([#709](https://github.com/expo/eas-cli/pull/709) by [@quinlanj](https://github.com/quinlanj))
## [0.33.1](https://github.com/expo/eas-cli/releases/tag/v0.33.1) - 2021-10-22
### 🎉 New features
- Compute runtime version using config-plugins. ([#697](https://github.com/expo/eas-cli/pull/697) by [@jkhales](https://github.com/jkhales))
### 🐛 Bug fixes
- [eas-cli] Skip validating updates scripts integration, which no longer exist in SDK 43+ ([#706](https://github.com/expo/eas-cli/pull/706) by [@brentvatne](https://github.com/brentvatne))
## [0.33.0](https://github.com/expo/eas-cli/releases/tag/v0.33.0) - 2021-10-20
### 🎉 New features
- Make "production" the default profile for building and submitting. ([#677](https://github.com/expo/eas-cli/pull/677)) by [@jonsamp](https://github.com/jonsamp)
- Previously, the default profile when running `eas build` or `eas submit` was "release". We're changing it to a more recognizable name that is consistent with our docs, which is now "production". You can always specify a profile manually with the `--profile` flag. For this major version, if you do not have a profile named "production", and still have a profile named "release", we will fall back to the "release" profile as the default, however you'll see a warning as we're going to remove that behavior in the next major release of EAS CLI.
- To upgrade, update **eas.json** to have a "production" profile in both the `build` and `submit` objects. If you already have a project set up, this will replace the existing "release" profile. After the change, eas.json should have the following profiles:
```json
{
"build": {
"production": { ... }
},
"submit": {
"production": { ... }
}
}
```
### 🐛 Bug fixes
- Skip the second prompt for Apple ID if the user is already signed in with Apple. ([#691](https://github.com/expo/eas-cli/pull/691) by [@dsokal](https://github.com/dsokal))
- Unify reading `app.json`. ([#692](https://github.com/expo/eas-cli/pull/692) by [@dsokal](https://github.com/dsokal))
## [0.32.0](https://github.com/expo/eas-cli/releases/tag/v0.32.0) - 2021-10-15
### 🎉 New features
- Add App Store Connect API Key support to iOS submissions. ([#678](https://github.com/expo/eas-cli/pull/678) by [@quinlanj](https://github.com/quinlanj))
- Create/list/revoke App Store Connect Api keys via Apple apis. ([#687](https://github.com/expo/eas-cli/pull/687) by [@quinlanj](https://github.com/quinlanj))
- Add ability to select a build from a list in `eas submit` interactive mode. ([#688](https://github.com/expo/eas-cli/pull/688) by [@barthap](https://github.com/barthap))
### 🐛 Bug fixes
- Fix printing App Store Connect URL after submission. ([#683](https://github.com/expo/eas-cli/pull/683) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
- Add App Store Connect API Key fields to `eas.json`. ([#684](https://github.com/expo/eas-cli/pull/684) by [@quinlanj](https://github.com/quinlanj))
- Enable no-underscore-dangle eslint rule. ([#686](https://github.com/expo/eas-cli/pull/686) by [@dsokal](https://github.com/dsokal))
## [0.31.1](https://github.com/expo/eas-cli/releases/tag/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 +33,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 +412,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/commit/) for that version. In most cases, it will be the latest `Publish` commit.
- Find the `eas-cli-local-build-plugin` version published in that commit.
![example publish commit](./.gh-assets/eas-build-publish-commit.png)
- Run `npm dist-tags add eas-cli-local-build-plugin@VERSION latest` with the version from the previous step.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.36.1"
"version": "0.31.1"
}
+2 -2
View File
@@ -9,9 +9,9 @@
"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"
"clean": "lerna run clean"
},
"devDependencies": {
"@expo/oclif-dev-cli": "1.24.0-expo",
+29 -30
View File
@@ -73,7 +73,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/account/login.js)_
## `eas account:logout`
@@ -87,7 +87,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/account/logout.js)_
## `eas account:view`
@@ -101,7 +101,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/account/view.js)_
## `eas analytics [STATUS]`
@@ -112,7 +112,7 @@ USAGE
$ eas analytics [STATUS]
```
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/analytics.js)_
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/analytics.js)_
## `eas build`
@@ -138,15 +138,14 @@ OPTIONS
--non-interactive Run command in non-interactive mode
--profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined
in eas.json.
--profile=PROFILE_NAME [default: release] Name of the build profile from eas.json
--skip-project-configuration Skip project configuration
--[no-]wait Wait for build(s) to complete
```
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/build/index.js)_
## `eas build:cancel [BUILD_ID]`
@@ -157,7 +156,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/build/cancel.js)_
## `eas build:configure`
@@ -171,7 +170,7 @@ OPTIONS
-p, --platform=(android|ios|all) Platform to configure
```
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/build/configure.js)_
## `eas build:list`
@@ -204,7 +203,7 @@ OPTIONS
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/build/list.js)_
## `eas build:view [BUILD_ID]`
@@ -218,7 +217,7 @@ OPTIONS
--json Enable JSON output, non-JSON messages will be printed to stderr
```
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/build/view.js)_
## `eas config`
@@ -233,7 +232,7 @@ OPTIONS
--profile=profile
```
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/config.js)_
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/config.js)_
## `eas credentials`
@@ -244,7 +243,7 @@ USAGE
$ eas credentials
```
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/credentials.js)_
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/credentials.js)_
## `eas device:create`
@@ -255,7 +254,7 @@ USAGE
$ eas device:create
```
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/device/create.js)_
## `eas device:list`
@@ -269,7 +268,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/device/list.js)_
## `eas device:view [UDID]`
@@ -280,7 +279,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/device/view.js)_
## `eas diagnostics`
@@ -291,7 +290,7 @@ USAGE
$ eas diagnostics
```
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.36.1/packages/eas-cli/src/commands/diagnostics.js)_
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/diagnostics.js)_
## `eas help [COMMAND]`
@@ -319,7 +318,7 @@ USAGE
$ eas project:info
```
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/project/info.js)_
## `eas project:init`
@@ -333,7 +332,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/project/init.js)_
## `eas secret:create`
@@ -350,7 +349,7 @@ OPTIONS
--value=value Value of the secret
```
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/secret/create.js)_
## `eas secret:delete`
@@ -364,10 +363,10 @@ OPTIONS
--id=id ID of the secret to delete
DESCRIPTION
Unsure where to find the secret's ID? Run eas secret:list
Unsure where to find the secret's ID? Run eas secrets:list
```
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/secret/delete.js)_
## `eas secret:list`
@@ -378,7 +377,7 @@ USAGE
$ eas secret:list
```
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/secret/list.js)_
## `eas submit`
@@ -395,7 +394,7 @@ OPTIONS
--non-interactive Run command in non-interactive mode
--path=path Path to the .apk/.aab/.ipa file
--profile=profile Name of the submit profile from eas.json. Defaults to "production" if defined in
--profile=profile Name of the submit profile from eas.json. Defaults to "release" if defined in
eas.json.
--url=url App archive url
@@ -411,7 +410,7 @@ 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.31.1/packages/eas-cli/src/commands/submit.js)_
## `eas webhook:create`
@@ -430,7 +429,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.31.1/packages/eas-cli/src/commands/webhook/create.js)_
## `eas webhook:delete [ID]`
@@ -444,7 +443,7 @@ ARGUMENTS
ID ID of the webhook to delete
```
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/webhook/delete.js)_
## `eas webhook:list`
@@ -458,7 +457,7 @@ OPTIONS
--event=(BUILD) Event type that triggers the webhook
```
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.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.31.1/packages/eas-cli/src/commands/webhook/list.js)_
## `eas webhook:update`
@@ -478,7 +477,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.31.1/packages/eas-cli/src/commands/webhook/update.js)_
## `eas webhook:view ID`
@@ -492,5 +491,5 @@ ARGUMENTS
ID ID of the webhook to view
```
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.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.31.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}'
+1325 -1844
View File
File diff suppressed because it is too large. Load diff
+9 -8
View File
@@ -1,23 +1,24 @@
{
"name": "eas-cli",
"type": "module",
"description": "EAS command line tool",
"version": "0.36.1",
"version": "0.31.1",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.26",
"@expo/config": "6.0.3",
"@expo/config-plugins": "4.0.3",
"@expo/eas-build-job": "0.2.57",
"@expo/eas-json": "^0.36.0",
"@expo/apple-utils": "0.0.0-alpha.25",
"@expo/config": "6.0.2",
"@expo/config-plugins": "4.0.2",
"@expo/eas-build-job": "0.2.52",
"@expo/eas-json": "^0.31.0",
"@expo/json-file": "8.2.33",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.14",
"@expo/plugin-warn-if-update-available": "1.7.0",
"@expo/prebuild-config": "3.0.3",
"@expo/prebuild-config": "3.0.2",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.0",
"@expo/sdk-runtime-versions": "1.0.0",
@@ -177,6 +178,6 @@
"test": "jest",
"version": "yarn oclif-dev readme && node patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
"clean": "rm -rf dist build node_modules yarn-error.log"
"clean": "rm -rf build node_modules"
}
}
+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,14 @@ 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';
/**
* We use require() to exclude package.json from TypeScript's analysis since it lives outside
* the src directory and would change the directory structure of the emitted files
* under the build directory
*/
const packageJSON = require('../package.json');
const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
darwin: 'Mac',
@@ -91,7 +97,7 @@ export function logEvent(name: string, properties: Record<string, any> = {}): vo
ensureUserIdentified();
const { userId, deviceId } = identifyData ?? {};
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
const commonEventProperties = { source_version: packageJSON?.version, source: 'eas cli' };
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
rudderstackClient.track({
@@ -120,7 +126,7 @@ function getRudderStackContext(): Record<string, any> {
return {
os: { name: platform, version: os.release() },
device: { type: platform, model: platform },
app: { name: 'eas cli', version: easCliVersion ?? undefined },
app: { name: 'eas cli', version: packageJSON?.version ?? undefined },
};
}
-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`;
}
}
@@ -1,5 +1,6 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig, AndroidManifest } from '@expo/config-plugins';
import fs from 'fs-extra';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
@@ -9,6 +10,17 @@ import { ensureValidVersions } from '../utils/updates';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
const updatedBuildGradleContents =
AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
projectDir,
buildGradleContents
);
await fs.writeFile(buildGradlePath, updatedBuildGradleContents);
}
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
const androidManifest = await getAndroidManifestAsync(projectDir);
@@ -27,7 +39,7 @@ export async function syncUpdatesConfigurationAsync(
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
try {
await ensureUpdatesConfiguredAsync(projectDir);
await ensureUpdatesConfiguredAsync(projectDir, exp);
} catch (error) {
Log.error(
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
@@ -50,9 +62,15 @@ export async function syncUpdatesConfigurationAsync(
}
}
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in AndroidManifest.xml
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
const gradleScriptApply = AndroidConfig.Updates.formatApplyLineForBuildGradle(projectDir);
throw new Error(`Missing ${gradleScriptApply} in ${buildGradlePath}`);
}
const androidManifest = await getAndroidManifestAsync(projectDir);
if (!AndroidConfig.Updates.isMainApplicationMetaDataSet(androidManifest)) {
@@ -31,7 +31,7 @@ beforeEach(async () => {
await fs.mkdirp(os.tmpdir());
});
// bare workflow
// generic workflow
describe(bumpVersionAsync, () => {
it('throws an informative error when multiple flavors are detected in the android project', async () => {
await initProjectWithGradleFileAsync(
@@ -69,7 +69,7 @@ describe(bumpVersionAsync, () => {
});
it('bumps expo.android.versionCode and buildGradle versionCode when strategy = BumpStrategy.VERSION_CODE', async () => {
const nativeVersionCode = 100; // this should be overwritten by bumpVersionAsync
const fakeExp = initBareWorkflowProject({ versionCode: nativeVersionCode });
const fakeExp = initGenericProject({ versionCode: nativeVersionCode });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
@@ -91,7 +91,7 @@ describe(bumpVersionAsync, () => {
it('bumps expo.version and gradle versionCode when strategy = BumpStrategy.SHORT_VERSION', async () => {
const nativeVersionName = '1.0.0'; // this should be overwritten by bumpVersionAsync
const fakeExp = initBareWorkflowProject({ versionName: nativeVersionName });
const fakeExp = initGenericProject({ versionName: nativeVersionName });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
@@ -112,7 +112,7 @@ describe(bumpVersionAsync, () => {
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initBareWorkflowProject();
const fakeExp = initGenericProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
@@ -185,9 +185,9 @@ describe(bumpVersionInAppJsonAsync, () => {
});
describe(maybeResolveVersionsAsync, () => {
describe('bare project', () => {
describe('generic project', () => {
it('reads the versions from native code', async () => {
const exp = initBareWorkflowProject();
const exp = initGenericProject();
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/repo',
exp,
@@ -211,7 +211,7 @@ describe(maybeResolveVersionsAsync, () => {
});
});
function initBareWorkflowProject({
function initGenericProject({
versionCode = 123,
versionName = '3.0.0',
}: {
@@ -6,10 +6,10 @@ import { AndroidBuildProfile, AndroidVersionAutoIncrement } from '@expo/eas-json
import Log from '../../log';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject,
warnIfAndroidPackageDefinedInAppConfigForGenericProject,
} from '../../project/android/applicationId';
import { resolveWorkflowAsync } from '../../project/workflow';
import { getVcsClient } from '../../vcs';
import vcs from '../../vcs';
import { ConfigureContext } from '../context';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
@@ -21,12 +21,12 @@ export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void
return;
}
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
warnIfAndroidPackageDefinedInAppConfigForGenericProject(ctx.projectDir, ctx.exp);
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
const easGradlePath = AndroidConfig.EasBuild.getEasBuildGradlePath(ctx.projectDir);
await getVcsClient().trackFileAsync(easGradlePath);
await vcs.trackFileAsync(easGradlePath);
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
@@ -17,14 +17,15 @@ export function transformJob(job: Android.Job): AndroidJobInput {
artifactPath: job.artifactPath,
username: job.username,
buildType: job.buildType && transformBuildType(job.buildType),
developmentClient: job.developmentClient,
};
}
function transformBuildType(buildType: Android.BuildType): AndroidBuildType {
if (buildType === Android.BuildType.APK) {
return AndroidBuildType.Apk;
} else {
} else if (buildType === Android.BuildType.APP_BUNDLE) {
return AndroidBuildType.AppBundle;
} else {
return AndroidBuildType.DevelopmentClient;
}
}
@@ -6,7 +6,7 @@ import slash from 'slash';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { getVcsClient } from '../../vcs';
import vcs from '../../vcs';
import { BuildContext } from '../context';
interface JobData {
@@ -27,7 +27,7 @@ export async function prepareJobAsync(
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildProfile: AndroidBuildProfile = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
slash(path.relative(await vcs.getRootPathAsync(), ctx.projectDir)) || '.';
const { credentials } = jobData;
const buildCredentials = credentials
? {
@@ -70,11 +70,12 @@ export async function prepareJobAsync(
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
developmentClient: buildProfile.developmentClient,
gradleCommand: buildProfile.gradleCommand,
artifactPath: buildProfile.artifactPath,
buildType,
username,
buildType: buildProfile.developmentClient ? Android.BuildType.DEVELOPMENT_CLIENT : buildType,
};
return sanitizeJob(job);
+33 -16
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';
@@ -15,10 +13,12 @@ import { uploadAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
import { getVcsClient } from '../vcs';
import vcs from '../vcs';
import { BuildContext } from './context';
import { runLocalBuildAsync } from './local';
import { MetadataContext, collectMetadataAsync } from './metadata';
import { TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
import { printDeprecationWarnings } from './utils/printBuildInfo';
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
@@ -55,22 +55,20 @@ 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,
});
}
if (await getVcsClient().isCommitRequiredAsync()) {
if (await vcs.hasUncommittedChangesAsync()) {
Log.addNewLineIfNone();
await reviewAndCommitChangesAsync(
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
@@ -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 } = {}
+13 -26
View File
@@ -10,8 +10,7 @@ import { RequestedPlatform } from '../platform';
import { resolveWorkflowAsync } from '../project/workflow';
import { confirmAsync, promptAsync } from '../prompts';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs';
import vcs from '../vcs';
import { configureAndroidAsync } from './android/configure';
import { ConfigureContext } from './context';
import { configureIosAsync } from './ios/configure';
@@ -38,7 +37,7 @@ export async function ensureProjectConfiguredAsync(
projectDir,
platform: requestedPlatform,
});
if (await getVcsClient().isCommitRequiredAsync()) {
if (await vcs.hasUncommittedChangesAsync()) {
error(
'Build process requires clean working tree, please commit all your changes and run `eas build` again',
{ exit: 1 }
@@ -58,7 +57,7 @@ export async function configureAsync(options: {
platform: RequestedPlatform;
projectDir: string;
}): Promise<void> {
await getVcsClient().ensureRepoExistsAsync();
await vcs.ensureRepoExistsAsync();
await maybeBailOnRepoStatusAsync();
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
@@ -87,39 +86,31 @@ export async function configureAsync(options: {
await configureIosAsync(ctx);
}
if (await getVcsClient().isCommitRequiredAsync()) {
if (await vcs.hasUncommittedChangesAsync()) {
Log.newLine();
await reviewAndCommitChangesAsync(configureCommitMessage[options.platform]);
} else if (!(await getVcsClient().hasUncommittedChangesAsync())) {
} else {
Log.newLine();
Log.withTick('No changes were necessary, the project is already configured correctly.');
}
}
const EAS_JSON_MANAGED_DEFAULT: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
},
build: {
release: {},
development: {
developmentClient: true,
distribution: 'internal',
},
preview: {
distribution: 'internal',
},
production: {},
},
submit: {
production: {},
release: {},
},
};
const EAS_JSON_BARE_DEFAULT: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
},
const EAS_JSON_GENERIC_DEFAULT: EasJson = {
build: {
release: {},
development: {
distribution: 'internal',
android: {
@@ -129,13 +120,9 @@ const EAS_JSON_BARE_DEFAULT: EasJson = {
buildConfiguration: 'Debug',
},
},
preview: {
distribution: 'internal',
},
production: {},
},
submit: {
production: {},
release: {},
},
};
@@ -152,11 +139,11 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
const easJson =
ctx.hasAndroidNativeProject && ctx.hasIosNativeProject
? EAS_JSON_BARE_DEFAULT
? EAS_JSON_GENERIC_DEFAULT
: EAS_JSON_MANAGED_DEFAULT;
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
await getVcsClient().trackFileAsync(easJsonPath);
await vcs.trackFileAsync(easJsonPath);
Log.withTick('Generated eas.json');
}
@@ -190,7 +177,7 @@ async function reviewAndCommitChangesAsync(
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await getVcsClient().showDiffAsync();
await vcs.showDiffAsync();
await reviewAndCommitChangesAsync(initialCommitMessage, false);
}
}
+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,
@@ -1,16 +1,30 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import fs from 'fs-extra';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { getVcsClient } from '../../vcs';
import vcs from '../../vcs';
import { ensureValidVersions } from '../utils/updates';
import { readPlistAsync, writePlistAsync } from './plist';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
let xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, xcodeProject)) {
xcodeProject = IOSConfig.Updates.ensureBundleReactNativePhaseContainsConfigurationScript(
projectDir,
xcodeProject
);
await fs.writeFile(
IOSConfig.Paths.getPBXProjectPath(projectDir),
// eslint-disable-next-line node/no-sync
xcodeProject.writeSync()
);
}
let expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
@@ -27,7 +41,7 @@ export async function syncUpdatesConfigurationAsync(
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
try {
await ensureUpdatesConfiguredAsync(projectDir);
await ensureUpdatesConfiguredAsync(projectDir, exp);
} catch (error) {
Log.error(
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
@@ -48,9 +62,15 @@ export async function syncUpdatesConfigurationAsync(
}
}
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in Expo.plist
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
const xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, xcodeProject)) {
const script = 'expo-updates/scripts/create-manifest-ios.sh';
const buildPhase = '"Bundle React Native code and images"';
throw new Error(`Path to ${script} is missing in a ${buildPhase} build phase.`);
}
const expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistConfigurationSet(expoPlist)) {
throw new Error('Missing values in Expo.plist');
@@ -68,7 +88,7 @@ async function writeExpoPlistAsync(
): Promise<void> {
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
await writePlistAsync(expoPlistPath, expoPlist);
await getVcsClient().trackFileAsync(expoPlistPath);
await vcs.trackFileAsync(expoPlistPath);
}
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
@@ -49,10 +49,10 @@ describe(evaluateTemplateString, () => {
});
});
// bare workflow
// generic workflow
describe(bumpVersionAsync, () => {
it('bumps expo.ios.buildNumber and CFBundleVersion when strategy = BumpStrategy.BUILD_NUMBER', async () => {
const fakeExp = initBareWorkflowProject();
const fakeExp = initGenericProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
@@ -74,7 +74,7 @@ describe(bumpVersionAsync, () => {
});
it('bumps expo.ios.buildNumber and CFBundleVersion for non default Info.plist location', async () => {
const fakeExp = initBareWorkflowProject({ infoPlistName: 'Info2.plist' });
const fakeExp = initGenericProject({ infoPlistName: 'Info2.plist' });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
@@ -98,7 +98,7 @@ describe(bumpVersionAsync, () => {
});
it('bumps expo.version and CFBundleShortVersionString when strategy = BumpStrategy.SHORT_VERSION', async () => {
const fakeExp = initBareWorkflowProject();
const fakeExp = initGenericProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
@@ -120,7 +120,7 @@ describe(bumpVersionAsync, () => {
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initBareWorkflowProject();
const fakeExp = initGenericProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
@@ -194,9 +194,9 @@ describe(bumpVersionInAppJsonAsync, () => {
});
describe(readBuildNumberAsync, () => {
describe('bare project', () => {
describe('generic project', () => {
it('reads the build number from native code', async () => {
const exp = initBareWorkflowProject();
const exp = initGenericProject();
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
expect(buildNumber).toBe('1');
});
@@ -212,14 +212,14 @@ describe(readBuildNumberAsync, () => {
});
describe(readShortVersionAsync, () => {
describe('bare project', () => {
describe('generic project', () => {
it('reads the short version from native code', async () => {
const exp = initBareWorkflowProject();
const exp = initGenericProject();
const appVersion = await readShortVersionAsync('/repo', exp, {});
expect(appVersion).toBe('1.0.0');
});
it('evaluates interpolated build number', async () => {
const exp = initBareWorkflowProject({
const exp = initGenericProject({
appVersion: '$(CURRENT_PROJECT_VERSION)',
});
const buildNumber = await readShortVersionAsync('/repo', exp, {
@@ -231,7 +231,7 @@ describe(readShortVersionAsync, () => {
describe('managed project', () => {
it('reads the version from app config', async () => {
const exp = initBareWorkflowProject();
const exp = initGenericProject();
const appVersion = await readShortVersionAsync('/repo', exp, {});
expect(appVersion).toBe('1.0.0');
});
@@ -286,7 +286,7 @@ describe(getInfoPlistPath, () => {
});
});
function initBareWorkflowProject({
function initGenericProject({
appVersion = '1.0.0',
version = '1',
infoPlistName = 'Info.plist',
+2 -2
View File
@@ -6,7 +6,7 @@ import type { XCBuildConfiguration } from 'xcode';
import Log from '../../log';
import {
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject,
warnIfBundleIdentifierDefinedInAppConfigForGenericProject,
} from '../../project/ios/bundleIdentifier';
import { resolveWorkflowAsync } from '../../project/workflow';
import { ConfigureContext } from '../context';
@@ -20,7 +20,7 @@ export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
return;
}
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
warnIfBundleIdentifierDefinedInAppConfigForGenericProject(ctx.projectDir, ctx.exp);
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
@@ -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,
+28 -3
View File
@@ -1,7 +1,12 @@
import { Env, Ios } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
import {
DistributionType,
IosBuildType,
IosJobInput,
IosJobSecretsInput,
} from '../../graphql/generated';
import { transformProjectArchive, transformWorkflow } from '../graphql';
export function transformJob(job: Ios.Job): IosJobInput {
@@ -11,18 +16,30 @@ export function transformJob(job: Ios.Job): IosJobInput {
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
updates: job.updates,
distribution: job.distribution && transformDistributionType(job.distribution),
secrets: transformIosSecrets(job.secrets),
builderEnvironment: job.builderEnvironment,
cache: job.cache,
scheme: job.scheme,
buildConfiguration: job.buildConfiguration,
artifactPath: job.artifactPath,
buildType: job.buildType && transformBuildType(job.buildType),
username: job.username,
developmentClient: job.developmentClient,
simulator: job.simulator,
};
}
function transformDistributionType(distributionType: Ios.DistributionType): DistributionType {
if (distributionType === 'store') {
return DistributionType.Store;
} else if (distributionType === 'internal') {
return DistributionType.Internal;
} else {
return DistributionType.Simulator;
}
}
function transformIosSecrets(secrets: {
buildCredentials?: Ios.BuildCredentials;
env?: Env;
@@ -44,3 +61,11 @@ function transformIosSecrets(secrets: {
environmentSecrets: secrets.env,
};
}
function transformBuildType(buildType: Ios.BuildType): IosBuildType {
if (buildType === Ios.BuildType.DEVELOPMENT_CLIENT) {
return IosBuildType.DevelopmentClient;
} else {
return IosBuildType.Release;
}
}
+8 -4
View File
@@ -5,7 +5,7 @@ import slash from 'slash';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { getVcsClient } from '../../vcs';
import vcs from '../../vcs';
import { BuildContext } from '../context';
interface JobData {
@@ -25,7 +25,7 @@ export async function prepareJobAsync(
jobData: JobData
): Promise<Job> {
const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
slash(path.relative(await vcs.getRootPathAsync(), ctx.projectDir)) || '.';
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
if (jobData.credentials) {
@@ -40,6 +40,7 @@ export async function prepareJobAsync(
platform: Platform.IOS,
projectArchive: jobData.projectArchive,
projectRootDirectory,
distribution: ctx.buildProfile.simulator ? 'simulator' : ctx.buildProfile.distribution,
builderEnvironment: {
image: ctx.buildProfile.image,
node: ctx.buildProfile.node,
@@ -60,11 +61,14 @@ export async function prepareJobAsync(
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
developmentClient: ctx.buildProfile.developmentClient,
simulator: ctx.buildProfile.simulator,
scheme: jobData.buildScheme,
buildConfiguration: ctx.buildProfile.buildConfiguration,
artifactPath: ctx.buildProfile.artifactPath,
buildType: ctx.buildProfile.developmentClient
? Ios.BuildType.DEVELOPMENT_CLIENT
: Ios.BuildType.RELEASE,
username,
};
return sanitizeJob(job);
+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;
}
+10 -5
View File
@@ -7,8 +7,7 @@ import { GradleBuildContext } from '../project/android/gradle';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
import { getUsername } from '../project/projectUtils';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs';
import vcs from '../vcs';
import {
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
@@ -22,6 +21,13 @@ import {
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
import { isExpoUpdatesInstalled } from './utils/updates';
/**
* We use require() to exclude package.json from TypeScript's analysis since it lives outside
* the src directory and would change the directory structure of the emitted files
* under the build directory
*/
const packageJSON = require('../../package.json');
export type MetadataContext<T extends Platform> = T extends Platform.ANDROID
? AndroidMetadataContext
: IosMetadataContext;
@@ -47,7 +53,7 @@ export async function collectMetadataAsync<T extends Platform>(
const metadata = {
trackingContext: ctx.trackingCtx,
...(await maybeResolveVersionsAsync(ctx, platformContext)),
cliVersion: easCliVersion,
cliVersion: packageJSON.version,
workflow: ctx.workflow,
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
@@ -57,8 +63,7 @@ export async function collectMetadataAsync<T extends Platform>(
appName: ctx.exp.name,
appIdentifier: await resolveAppIdentifierAsync(ctx, platformContext),
buildProfile: ctx.buildProfileName,
gitCommitHash: await getVcsClient().getCommitHashAsync(),
isGitWorkingTreeDirty: await getVcsClient().hasUncommittedChangesAsync(),
gitCommitHash: await vcs.getCommitHashAsync(),
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
+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);
},
};
+3 -12
View File
@@ -1,6 +1,6 @@
import * as ExpoConfig from '@expo/config';
import JsonFile from '@expo/json-file';
import assert from 'assert';
import fs from 'fs-extra';
export async function updateAppJsonConfigAsync(
{
@@ -15,19 +15,10 @@ export async function updateAppJsonConfigAsync(
const paths = ExpoConfig.getConfigFilePaths(projectDir);
assert(paths.staticConfigPath, "can't update dynamic config");
const rawStaticConfig = readAppJson(paths.staticConfigPath);
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
rawStaticConfig.expo = rawStaticConfig.expo ?? {};
modifyConfig(rawStaticConfig.expo);
await JsonFile.writeAsync(paths.staticConfigPath, rawStaticConfig, { json5: false });
await fs.writeJson(paths.staticConfigPath, rawStaticConfig, { spaces: 2 });
modifyConfig(exp);
}
// TODO: remove this once @expo/config exports getStaticConfig
export function readAppJson(appJsonPath: string): any {
const config = JsonFile.read(appJsonPath, { json5: true });
if (config) {
return config as any;
}
throw new Error(`Failed to read app.json`);
}
+24 -24
View File
@@ -1,5 +1,5 @@
import { Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import resolveFrom from 'resolve-from';
@@ -9,38 +9,40 @@ import Log, { learnMore } from '../../log';
import { appPlatformDisplayNames } from '../../platform';
import { resolveWorkflowAsync } from '../../project/workflow';
import { confirmAsync } from '../../prompts';
import { expoCommandAsync } from '../../utils/expoCli';
import { ProfileData } from '../../utils/profiles';
import { getVcsClient } from '../../vcs';
import { expoCommandAsync } from '../../utils/expoCommand';
import zipObject from '../../utils/expodash/zipObject';
import { reviewAndCommitChangesAsync } from './repository';
export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir,
platforms,
profile,
nonInteractive = false,
buildProfiles = [],
}: {
projectDir: string;
platforms: Platform[];
profile: string;
nonInteractive?: boolean;
buildProfiles?: ProfileData<BuildProfile>[];
}): Promise<void> {
if (await isExpoDevClientInstalledAsync(projectDir)) {
return;
}
const buildProfilesWithDevelopmentClientRequired = buildProfiles.filter(
buildProfile => buildProfile.profile.developmentClient
const easJsonReader = new EasJsonReader(projectDir);
const devClientPerPlatformList = await Promise.all(
platforms.map(async platform => {
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, profile);
return buildProfile.developmentClient ?? false;
})
);
const isDevelopmentClientRequired = buildProfilesWithDevelopmentClientRequired.some(Boolean);
if (!isDevelopmentClientRequired) {
const isDevClientRequired = devClientPerPlatformList.some(i => i);
if (!isDevClientRequired) {
return;
}
const platformsToCheck = buildProfilesWithDevelopmentClientRequired.map(
({ platform }) => platform
);
const devClientPerPlatform = zipObject(platforms, devClientPerPlatformList);
const platformsToCheck = platforms.filter(platform => devClientPerPlatform[platform]);
const workflowPerPlatformList = await Promise.all(
platformsToCheck.map(platform => resolveWorkflowAsync(projectDir, platform))
);
@@ -65,15 +67,15 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
if (install) {
await installExpoDevClientAsync(projectDir, { nonInteractive });
} else {
error(`Install ${chalk.bold('expo-dev-client')} manually and come back later.`, {
error(`Install ${chalk.bold('expo-dev-client')} on your own and come back later.`, {
exit: 1,
});
}
} else {
Log.warn(`You'll need to install ${chalk.bold('expo-dev-client')} manually.`);
Log.warn(`Unfortunately, you need to install ${chalk.bold('expo-dev-client')} on your own.`);
Log.warn(
learnMore('https://docs.expo.dev/clients/installation/', {
learnMoreMessage: 'See installation instructions on how to do it.',
learnMoreMessage: 'See installation instructions on how to do it',
dim: false,
})
);
@@ -108,9 +110,7 @@ async function installExpoDevClientAsync(
Log.newLine();
await expoCommandAsync(projectDir, ['install', 'expo-dev-client']);
Log.newLine();
if (await getVcsClient().isCommitRequiredAsync()) {
await reviewAndCommitChangesAsync('Install expo-dev-client', {
nonInteractive,
});
}
await reviewAndCommitChangesAsync('Install expo-dev-client', {
nonInteractive,
});
}
@@ -120,5 +120,11 @@ export function formatGraphQLBuild(build: BuildFragment): string {
}
const getActorName = (build: BuildFragment): string => {
return build.initiatingActor?.displayName || 'unknown';
if (!build.initiatingActor) {
return 'unknown';
} else if (build.initiatingActor.__typename === 'User') {
return build.initiatingActor.username;
} else {
return build.initiatingActor.firstName ?? 'unknown';
}
};
+7 -10
View File
@@ -10,10 +10,10 @@ import { confirmAsync, promptAsync } from '../../prompts';
import { formatBytes } from '../../utils/files';
import { getTmpDirectory } from '../../utils/paths';
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
import { getVcsClient } from '../../vcs';
import vcs from '../../vcs';
export async function maybeBailOnRepoStatusAsync(): Promise<void> {
if (!(await getVcsClient().isCommitRequiredAsync())) {
if (!(await vcs.hasUncommittedChangesAsync())) {
return;
}
Log.addNewLineIfNone();
@@ -33,7 +33,7 @@ export async function maybeBailOnRepoStatusAsync(): Promise<void> {
}
export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<void> {
if (!(await getVcsClient().isCommitRequiredAsync())) {
if (!(await vcs.hasUncommittedChangesAsync())) {
return;
}
Log.addNewLineIfNone();
@@ -70,7 +70,7 @@ export async function commitPromptAsync({
initial: initialCommitMessage,
validate: (input: string) => input !== '',
});
await getVcsClient().commitAsync({ commitAllFiles, commitMessage: message });
await vcs.commitAsync({ commitAllFiles, commitMessage: message });
}
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
@@ -95,7 +95,7 @@ export async function makeProjectTarballAsync(): Promise<{ path: string; size: n
startTimer(compressTimerLabel);
try {
await getVcsClient().makeShallowCopyAsync(shallowClonePath);
await vcs.makeShallowCopyAsync(shallowClonePath);
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
'.',
]);
@@ -133,10 +133,7 @@ export async function reviewAndCommitChangesAsync(
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
): Promise<void> {
if (process.env.EAS_BUILD_AUTOCOMMIT) {
await getVcsClient().commitAsync({
commitMessage: initialCommitMessage,
commitAllFiles: false,
});
await vcs.commitAsync({ commitMessage: initialCommitMessage, commitAllFiles: false });
Log.withTick('Committed changes.');
return;
}
@@ -169,7 +166,7 @@ export async function reviewAndCommitChangesAsync(
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes.');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await getVcsClient().showDiffAsync();
await vcs.showDiffAsync();
await reviewAndCommitChangesAsync(initialCommitMessage, {
nonInteractive,
askedFirstTime: false,
+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`;
}
+4 -4
View File
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
import path from 'path';
import Log, { learnMore } from '../log';
import { getVcsClient } from '../vcs';
import vcs from '../vcs';
import { BuildContext } from './context';
export function checkNodeEnvVariable(ctx: BuildContext<Platform>): void {
@@ -25,12 +25,12 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
if (!googleServicesFilePath) {
return;
}
const rootDir = path.normalize(await getVcsClient().getRootPathAsync());
const rootDir = path.normalize(await vcs.getRootPathAsync());
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
if (
(await fs.pathExists(absGoogleServicesFilePath)) &&
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
(await getVcsClient().isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
(await vcs.isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
) {
Log.warn(
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
@@ -41,7 +41,7 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
)}`
);
Log.warn(
`If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting your build profile's env.GOOGLE_SERVICES_FILE in eas.json to any non-empty string.`
'If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting GOOGLE_SERVICES_FILE in your build profile in eas.json to any non-falsy value.'
);
Log.newLine();
}
@@ -1,20 +1,13 @@
import { EasJsonReader } from '@expo/eas-json';
import { Command } from '@oclif/command';
import semver from 'semver';
import {
AnalyticsEvent,
flushAsync as flushAnalyticsAsync,
initAsync as initAnalyticsAsync,
logEvent,
} from '../analytics/rudderstackClient';
import Log from '../log';
import { findProjectRootAsync } from '../project/projectUtils';
} from '../analytics';
import { getUserAsync } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { setVcsClient } from '../vcs';
import GitClient from '../vcs/clients/git';
export default abstract class EasCommand extends Command {
/**
@@ -27,10 +20,7 @@ export default abstract class EasCommand extends Command {
// eslint-disable-next-line async-protect/async-suffix
async run(): Promise<any> {
EasJsonReader.setLog(Log);
await initAnalyticsAsync();
await this.applyCliConfigAsync();
if (this.requiresAuthentication) {
const { flags } = this.parse();
@@ -52,18 +42,4 @@ export default abstract class EasCommand extends Command {
await flushAnalyticsAsync();
return super.finally(err);
}
private async applyCliConfigAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const easJsonReader = new EasJsonReader(projectDir);
const config = await easJsonReader.getCliConfigAsync();
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
throw new Error(
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
);
}
if (config?.requireCommit) {
setVcsClient(new GitClient());
}
}
}
@@ -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(),
@@ -17,7 +17,7 @@ import {
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { getVcsClient } from '../../vcs';
import vcs from '../../vcs';
export async function createUpdateBranchOnAppAsync({
appId,
@@ -90,8 +90,7 @@ export default class BranchCreate extends EasCommand {
name: 'name',
message: 'Please name the branch:',
initial:
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substr(2, 4)}`,
(await vcs.getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`,
validate: value => (value ? true : validationMessage),
}));
}
@@ -43,8 +43,7 @@ async function getBranchInfoAsync({
{
appId,
name,
},
{ additionalTypenames: ['UpdateBranch'] }
}
)
.toPromise()
);
+1 -2
View File
@@ -44,8 +44,7 @@ export async function listBranchesAsync({
{
appId: projectId,
limit: BRANCHES_LIMIT,
},
{ additionalTypenames: ['UpdateBranch'] }
}
)
.toPromise()
);
+55 -26
View File
@@ -1,6 +1,5 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { ExpoConfig, getConfig, getDefaultTarget } from '@expo/config';
import { getRuntimeVersionForSDKVersion } from '@expo/sdk-runtime-versions';
import { flags } from '@oclif/command';
import assert from 'assert';
import chalk from 'chalk';
@@ -29,12 +28,11 @@ import {
collectAssetsAsync,
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync, selectAsync } from '../../prompts';
import { formatUpdate } from '../../update/utils';
import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
import { getVcsClient } from '../../vcs';
import vcs from '../../vcs';
import { createUpdateBranchOnAppAsync } from './create';
import { listBranchesAsync } from './list';
import { viewUpdateBranchAsync } from './view';
@@ -62,8 +60,7 @@ async function getUpdateGroupAsync({
`,
{
group,
},
{ additionalTypenames: ['Update'] }
}
)
.toPromise()
);
@@ -171,14 +168,13 @@ export default class BranchPublish extends EasCommand {
isPublicConfig: true,
});
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
const runtimeVersions = getRuntimeVersionObject(exp, platformFlag, projectDir);
const projectId = await getProjectIdAsync(exp);
if (!branchName && autoFlag) {
branchName =
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substr(2, 4)}`;
(await vcs.getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`;
}
if (!branchName) {
@@ -301,7 +297,7 @@ export default class BranchPublish extends EasCommand {
}
if (!message && autoFlag) {
message = (await getVcsClient().getLastCommitMessageAsync())?.trim();
message = (await vcs.getLastCommitMessageAsync())?.trim();
}
if (!message) {
@@ -315,7 +311,7 @@ export default class BranchPublish extends EasCommand {
message: `Please enter a publication message.`,
initial: republish
? `Republish "${oldMessage!}" - group: ${group}`
: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
: (await vcs.getLastCommitMessageAsync())?.trim(),
validate: value => (value ? true : validationMessage),
}));
}
@@ -392,26 +388,59 @@ export default class BranchPublish extends EasCommand {
}
}
async function getRuntimeVersionObjectAsync(
function getRuntimeVersionObject(
exp: ExpoConfig,
platformFlag: PlatformFlag,
platformFlag: string,
projectDir: string
): Promise<Record<string, string>> {
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
): Record<string, string> {
let { runtimeVersion: defaultRuntimeVersion, sdkVersion } = exp;
for (const platform of platforms) {
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
if (isPolicy) {
const isManaged = (await resolveWorkflowAsync(projectDir, platform)) === Workflow.MANAGED;
if (!isManaged) {
throw new Error('Runtime version policies are only supported in the managed workflow.');
}
// When a SDK version is supplied instead of a runtime version and we're in the managed workflow
// construct the runtimeVersion with special meaning indicating that the runtime is an
// Expo SDK preset runtime that can be launched in Expo Go.
const isManagedProject = getDefaultTarget(projectDir) === 'managed';
if (!defaultRuntimeVersion && sdkVersion && isManagedProject) {
Log.withTick('Generating runtime version from sdk version');
defaultRuntimeVersion = getRuntimeVersionForSDKVersion(sdkVersion);
}
const iOSRuntimeVersion = (exp as any).ios?.runtimeVersion; // TODO-JJ remove cast to any
const androidRuntimeVersion = (exp as any).android?.runtimeVersion; // TODO-JJ remove cast to any
let runtimeVersions: Record<string, string>;
switch (platformFlag) {
case 'ios': {
runtimeVersions = {
ios: iOSRuntimeVersion ?? defaultRuntimeVersion,
};
break;
}
case 'android': {
runtimeVersions = {
android: androidRuntimeVersion ?? defaultRuntimeVersion,
};
break;
}
case 'all': {
runtimeVersions = {
ios: iOSRuntimeVersion ?? defaultRuntimeVersion,
android: androidRuntimeVersion ?? defaultRuntimeVersion,
};
break;
}
default:
throw new Error('Platform flag must be "ios", "android", or "all"');
}
if (Object.values(runtimeVersions).some(runtime => !runtime)) {
throw new Error(
"Couldn't find a 'runtimeVersion' for every platform. Please specify it under the 'expo' key in 'app.json'"
);
}
if (Object.values(runtimeVersions).some(runtime => typeof runtime !== 'string')) {
throw new Error(
`Please ensure that all of the runtime versions defined in the app.json are strings.`
);
}
return Object.fromEntries(
platforms.map(platform => [platform, Updates.getRuntimeVersion(exp, platform)])
);
return runtimeVersions;
}
function formatUpdateTitle(
+1 -2
View File
@@ -58,8 +58,7 @@ export async function viewUpdateBranchAsync({
appId,
name,
limit: PAGE_LIMIT,
},
{ additionalTypenames: ['UpdateBranch', 'Update'] }
}
)
.toPromise()
);
+71 -111
View File
@@ -1,10 +1,13 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, EasJsonReader } from '@expo/eas-json';
import {
EasJsonReader,
hasMismatchedExtendsAsync,
isUsingDeprecatedFormatAsync,
migrateAsync,
} from '@expo/eas-json';
import { flags } from '@oclif/command';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import figures from 'figures';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { prepareAndroidBuildAsync } from '../../build/android/build';
@@ -14,7 +17,7 @@ import { BuildContext, createBuildContextAsync } from '../../build/context';
import { prepareIosBuildAsync } from '../../build/ios/build';
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from '../../build/utils/devClient';
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
import { ensureRepoIsCleanAsync, reviewAndCommitChangesAsync } from '../../build/utils/repository';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { CredentialsContext } from '../../credentials/context';
import {
@@ -24,7 +27,7 @@ import {
SubmissionFragment,
} from '../../graphql/generated';
import { toAppPlatform, toPlatform } from '../../graphql/types/AppPlatform';
import Log, { learnMore, link } from '../../log';
import Log from '../../log';
import {
RequestedPlatform,
appPlatformDisplayNames,
@@ -38,24 +41,21 @@ import {
} from '../../project/isEasEnabledForProject';
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
import { findProjectRootAsync } from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
import { confirmAsync } from '../../prompts';
import { createSubmissionContextAsync } from '../../submit/context';
import {
submitAsync,
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../../submit/submit';
import { printSubmissionDetailsUrls } from '../../submit/utils/urls';
import { easCliVersion } from '../../utils/easCli';
import { enableJsonOutput } from '../../utils/json';
import { ProfileData, getProfilesAsync } from '../../utils/profiles';
import { getVcsClient, setVcsClient } from '../../vcs';
import GitClient from '../../vcs/clients/git';
import vcs from '../../vcs';
interface RawBuildFlags {
platform?: string;
'skip-credentials-check': boolean;
'skip-project-configuration': boolean;
profile?: string;
profile: string;
'non-interactive': boolean;
local: boolean;
wait: boolean;
@@ -68,7 +68,7 @@ interface RawBuildFlags {
interface BuildFlags {
requestedPlatform: RequestedPlatform;
skipProjectConfiguration: boolean;
profile: string | null;
profile: string;
nonInteractive: boolean;
local: boolean;
wait: boolean;
@@ -99,8 +99,8 @@ export default class Build extends EasCommand {
description: 'Skip project configuration',
}),
profile: flags.string({
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
default: 'release',
description: 'Name of the build profile from eas.json',
helpValue: 'PROFILE_NAME',
}),
'non-interactive': flags.boolean({
@@ -146,66 +146,50 @@ export default class Build extends EasCommand {
const projectDir = await findProjectRootAsync();
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await getVcsClient().ensureRepoExistsAsync();
await vcs.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
const platforms = toPlatforms(requestedPlatform);
const easJsonReader = new EasJsonReader(projectDir);
const buildProfiles = await getProfilesAsync({
platforms,
profileName: flags.profile,
async readProfileAsync(platform, profileName) {
return await easJsonReader.readBuildProfileAsync(platform, profileName);
},
});
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
platforms,
projectDir,
profile: flags.profile,
nonInteractive: flags.nonInteractive,
buildProfiles,
});
const startedBuilds: {
build: BuildFragment;
buildProfile: ProfileData<BuildProfile>;
}[] = [];
const startedBuilds: BuildFragment[] = [];
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
for (const buildProfile of buildProfiles) {
for (const platform of platforms) {
const { build: maybeBuild, buildCtx } = await this.prepareAndStartBuildAsync({
projectDir,
platform,
flags,
moreBuilds: platforms.length > 1,
buildProfile,
});
if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile });
startedBuilds.push(maybeBuild);
}
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
buildCtxByPlatform[toAppPlatform(platform)] = buildCtx;
}
if (flags.local) {
return;
}
Log.newLine();
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
printLogsUrls(startedBuilds);
Log.newLine();
const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) {
for (const startedBuild of startedBuilds) {
for (const build of startedBuilds) {
const submission = await this.prepareAndStartSubmissionAsync({
build: startedBuild.build,
credentialsCtx: nullthrows(
buildCtxByPlatform[startedBuild.build.platform]?.credentialsCtx
),
build,
credentialsCtx: nullthrows(buildCtxByPlatform[build.platform]?.credentialsCtx),
flags,
moreBuilds: startedBuilds.length > 1,
projectDir,
buildProfile: startedBuild.buildProfile,
});
submissions.push(submission);
}
@@ -219,7 +203,7 @@ export default class Build extends EasCommand {
return;
}
const builds = await waitForBuildEndAsync(startedBuilds.map(({ build }) => build.id));
const builds = await waitForBuildEndAsync(startedBuilds.map(build => build.id));
printBuildResults(builds, flags.json);
const haveAllBuildsFailedOrCanceled = builds.every(
@@ -268,42 +252,45 @@ export default class Build extends EasCommand {
return {
requestedPlatform,
skipProjectConfiguration: flags['skip-project-configuration'],
profile: profile ?? null,
profile,
nonInteractive,
local: flags['local'],
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,
};
}
private async prepareAndStartBuildAsync({
private async prepareAndStartBuildAsync<T extends Platform>({
projectDir,
platform,
flags,
moreBuilds,
buildProfile,
}: {
projectDir: string;
platform: T;
flags: BuildFlags;
moreBuilds: boolean;
buildProfile: ProfileData<BuildProfile>;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<T> }> {
const easJsonReader = new EasJsonReader(projectDir);
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, flags.profile);
const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName,
buildProfileName: flags.profile,
clearCache: flags.clearCache,
buildProfile: buildProfile.profile,
buildProfile,
local: flags.local,
nonInteractive: flags.nonInteractive,
platform: buildProfile.platform,
platform,
projectDir,
skipProjectConfiguration: flags.skipProjectConfiguration,
});
if (moreBuilds) {
Log.newLine();
const appPlatform = toAppPlatform(buildProfile.platform);
const appPlatform = toAppPlatform(platform);
Log.log(
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
`${appPlatformDisplayNames[appPlatform]} build`
@@ -343,17 +330,16 @@ export default class Build extends EasCommand {
flags,
moreBuilds,
projectDir,
buildProfile,
}: {
build: BuildFragment;
credentialsCtx: CredentialsContext;
flags: BuildFlags;
moreBuilds: boolean;
projectDir: string;
buildProfile: ProfileData<BuildProfile>;
}): Promise<SubmissionFragment> {
const easJsonReader = new EasJsonReader(projectDir);
const platform = toPlatform(build.platform);
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, flags.profile);
const submitProfile = await easJsonReader.readSubmitProfileAsync(platform, flags.submitProfile);
const submissionCtx = await createSubmissionContextAsync({
platform,
@@ -362,7 +348,7 @@ export default class Build extends EasCommand {
profile: submitProfile,
archiveFlags: { id: build.id },
nonInteractive: flags.nonInteractive,
env: buildProfile.profile.env,
env: buildProfile.env,
credentialsCtx,
});
@@ -392,62 +378,36 @@ export async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
if (!(await fs.pathExists(easJsonPath))) {
return;
}
const easJsonReader = new EasJsonReader(projectDir);
const rawEasJson = await easJsonReader.readRawAsync();
if (rawEasJson?.cli) {
return;
}
if (await isUsingDeprecatedFormatAsync(projectDir)) {
const hasMismatchedExtendsKeys = await hasMismatchedExtendsAsync(projectDir);
if (nonInteractive) {
Log.error('We detected that your eas.json is using a deprecated format.');
Log.error(
'We will convert it automatically if run this command without --non-interactive flag. Alternatively, you can update eas.json manually according to https://docs.expo.dev/build/eas-json'
);
error('Unsupported eas.json format', { exit: 1 });
}
if (nonInteractive) {
Log.warn(
`${
figures.warning
} Action required: the default behavior of EAS CLI has changed and your eas.json must be updated to remove ambiguity around which Git integration workflow to use. Refer to ${link(
'https://expo.fyi/eas-vcs-workflow'
)} for more information.`
);
Log.warn(
'This warning will become an error in an upcoming EAS CLI release. For now, we will proceed with the old default behavior to avoid disruption of your builds.'
);
setVcsClient(new GitClient());
return;
}
Log.log(
`${chalk.bold(
'eas-cli@>=0.34.0 no longer requires that you commit changes to Git before starting a build.'
)} ${learnMore('https://expo.fyi/eas-vcs-workflow')}`
);
Log.log(
`If you want to continue using the Git integration, you can opt in with ${chalk.bold(
'cli.requireCommit'
)} in ${chalk.bold('eas.json')} or with the following prompt.`
);
Log.newLine();
const mode = await selectAsync('Select your preferred Git integration', [
{ title: 'Require changes to be committed in Git (old default)', value: 'requireCommit' },
{ title: 'Allow builds with dirty Git working tree (new default)', value: 'noCommit' },
]);
if (mode === 'requireCommit') {
setVcsClient(new GitClient());
await ensureRepoIsCleanAsync(nonInteractive);
}
rawEasJson.cli =
mode === 'requireCommit'
? { version: `>= ${easCliVersion}`, requireCommit: true }
: { version: `>= ${easCliVersion}` };
await fs.writeJSON(easJsonPath, rawEasJson, { spaces: 2 });
Log.withTick('Updated eas.json');
if (mode === 'requireCommit') {
await getVcsClient().trackFileAsync(easJsonPath);
await reviewAndCommitChangesAsync('Set cli.requireCommit to true in eas.json', {
nonInteractive,
const confirm = await confirmAsync({
message:
'We detected that your eas.json is using a deprecated format, do you want to migrate to the new format?',
});
if (confirm) {
await migrateAsync(projectDir);
if (hasMismatchedExtendsKeys) {
Log.warn(
'"extends" keyword can only be migrated automatically to the new format if both Android and iOS profiles extend base profiles with the same names for both platforms'
);
Log.warn(
'Migration was successful, but you need to manually adjust the extend rules for your profiles'
);
error('Fix eas.json manually', { exit: 1 });
}
} else {
Log.error(
"Aborting, update your eas.json according to https://docs.expo.dev/build/eas-json and run 'eas build' again"
);
error('Unsupported eas.json format', { exit: 1 });
}
}
}
@@ -49,8 +49,7 @@ async function getChannelByNameForAppAsync({
}
}
`,
{ appId, channelName },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch'] }
{ appId, channelName }
)
.toPromise()
);
@@ -58,8 +58,7 @@ async function getAllUpdateChannelForAppAsync({
}
}
`,
{ appId, offset: 0, limit: CHANNEL_LIMIT },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
{ appId, offset: 0, limit: CHANNEL_LIMIT }
)
.toPromise()
);
@@ -133,8 +133,7 @@ export async function getUpdateChannelByNameForAppAsync({
}
}
`,
{ appId, channelName },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
{ appId, channelName }
)
.toPromise()
);
+3 -2
View File
@@ -2,7 +2,8 @@ import envinfo from 'envinfo';
import EasCommand from '../commandUtils/EasCommand';
import Log from '../log';
import { easCliVersion } from '../utils/easCli';
const packageJSON = require('../../package.json');
export default class Diagnostics extends EasCommand {
static description = 'log environment info to the console';
@@ -29,7 +30,7 @@ export default class Diagnostics extends EasCommand {
npmGlobalPackages: ['eas-cli', 'expo-cli'],
},
{
title: `EAS CLI ${easCliVersion} environment info`,
title: `EAS CLI ${packageJSON.version} environment info`,
}
);
Log.log(info);
@@ -22,8 +22,7 @@ async function projectInfoByIdAsync(appId: string): Promise<AppInfoQuery> {
}
}
`,
{ appId },
{ additionalTypenames: ['App'] }
{ appId }
)
.toPromise()
);
@@ -23,7 +23,7 @@ import { promptAsync, toggleConfirmAsync } from '../../prompts';
export default class EnvironmentSecretDelete extends EasCommand {
static description = `Delete an environment secret by ID.
Unsure where to find the secret's ID? Run ${chalk.bold('eas secret:list')}`;
Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
static flags = {
id: flags.string({
+8 -16
View File
@@ -20,7 +20,6 @@ import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils
import { SubmitArchiveFlags, createSubmissionContextAsync } from '../submit/context';
import { submitAsync, waitToCompleteAsync } from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { getProfilesAsync } from '../utils/profiles';
interface RawFlags {
platform?: string;
@@ -55,7 +54,7 @@ See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit
}),
profile: flags.string({
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
'Name of the submit profile from eas.json. Defaults to "release" if defined in eas.json.',
}),
latest: flags.boolean({
description: 'Submit the latest build for specified platform',
@@ -102,30 +101,23 @@ See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit
return;
}
const platforms = toPlatforms(flags.requestedPlatform);
const easJsonReader = new EasJsonReader(projectDir);
const submissionProfiles = await getProfilesAsync({
platforms,
profileName: flags.profile,
async readProfileAsync(platform, profileName) {
return await easJsonReader.readSubmitProfileAsync(platform, profileName);
},
});
const platforms = toPlatforms(flags.requestedPlatform);
const submissions: SubmissionFragment[] = [];
for (const submissionProfile of submissionProfiles) {
for (const platform of platforms) {
const profile = await easJsonReader.readSubmitProfileAsync(platform, flags.profile);
const ctx = await createSubmissionContextAsync({
platform: submissionProfile.platform,
platform,
projectDir,
projectId,
profile: submissionProfile.profile,
profile,
archiveFlags: flags.archiveFlags,
nonInteractive: flags.nonInteractive,
});
if (submissionProfiles.length > 1) {
if (platforms.length > 1) {
Log.newLine();
const appPlatform = toAppPlatform(submissionProfile.platform);
const appPlatform = toAppPlatform(platform);
Log.log(
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
`${appPlatformDisplayNames[appPlatform]} submission`
+1 -2
View File
@@ -39,8 +39,7 @@ export async function viewUpdateAsync({
`,
{
groupId,
},
{ additionalTypenames: ['Update'] }
}
)
.toPromise()
);
@@ -4,7 +4,7 @@ import { AuthCtx } from '../ios/appstore/authenticate';
export const testAuthCtx: AuthCtx = {
appleId: 'test-apple-id',
appleIdPassword: 'test-apple-password',
team: { id: 'test-apple-team-identifier', name: 'test-team-name', inHouse: false },
team: { id: 'test-team-id', name: 'test-team-name', inHouse: false },
};
export function getAppstoreMock(): AppStoreApi {
@@ -22,9 +22,5 @@ export function getAppstoreMock(): AppStoreApi {
createProvisioningProfileAsync: jest.fn(),
revokeProvisioningProfileAsync: jest.fn(),
createOrReuseAdhocProvisioningProfileAsync: jest.fn(),
createAscApiKeyAsync: jest.fn(),
getAscApiKeyAsync: jest.fn(),
revokeAscApiKeyAsync: jest.fn(),
listAscApiKeysAsync: jest.fn(),
} as any;
}
@@ -1,9 +1,5 @@
import { UserRole } from '@expo/apple-utils';
import {
AppFragment,
AppStoreConnectApiKeyFragment,
AppStoreConnectUserRole,
AppleAppIdentifierFragment,
AppleDistributionCertificateFragment,
AppleProvisioningProfileFragment,
@@ -14,27 +10,12 @@ import {
IosDistributionType,
} from '../../graphql/generated';
import * as IosGraphqlClient from '../ios/api/GraphqlClient';
import {
AscApiKey,
DistributionCertificate,
ProvisioningProfile,
} from '../ios/appstore/Credentials.types';
import { DistributionCertificate, ProvisioningProfile } from '../ios/appstore/Credentials.types';
import { Target } from '../ios/types';
import { testProvisioningProfileBase64 } from './fixtures-base64-data';
const now = new Date();
export const testAscApiKey: AscApiKey = {
keyId: 'test-keyIdentifier',
issuerId: 'test-issuer-id-from-apple',
teamId: 'test-team-id',
teamName: 'test-team-name',
name: 'test-name',
roles: [UserRole.ADMIN],
isRevoked: false,
keyP8: 'test-key-p8',
};
export const testProvisioningProfile: ProvisioningProfile = {
provisioningProfileId: 'test-id',
provisioningProfile: testProvisioningProfileBase64,
@@ -57,17 +38,6 @@ export const testAppFragment: AppFragment = {
slug: 'testapp',
};
export const testAscApiKeyFragment: AppStoreConnectApiKeyFragment = {
id: 'test-asc-api-key-id',
appleTeam: { ...testAppleTeamFragment },
issuerIdentifier: 'test-issuerIdentifier',
keyIdentifier: 'test-keyIdentifier',
name: 'test-name',
roles: [AppStoreConnectUserRole.Admin],
updatedAt: now,
createdAt: now,
};
export const testPushKey: ApplePushKeyFragment = {
id: 'test-push-key-id',
keyIdentifier: 'test-key-identifier',
@@ -142,7 +112,6 @@ export const testCommonIosAppCredentialsFragment: CommonIosAppCredentialsFragmen
appleAppIdentifier: testAppleAppIdentifierFragment,
pushKey: testPushKey,
iosAppBuildCredentialsList: [testIosAppBuildCredentialsFragment],
appStoreConnectApiKeyForSubmissions: testAscApiKeyFragment,
};
export const testAppleTeam = {
@@ -170,9 +139,6 @@ export function getNewIosApiMock(): { [key in keyof typeof IosGraphqlClient]?: a
createPushKeyAsync: jest.fn(),
getPushKeyForAppAsync: jest.fn(),
deletePushKeyAsync: jest.fn(),
createAscApiKeyAsync: jest.fn(),
deleteAscApiKeyAsync: jest.fn(),
getAscApiKeysForAccountAsync: jest.fn(() => []),
};
}
@@ -5,7 +5,7 @@ import nullthrows from 'nullthrows';
import { AndroidAppBuildCredentialsFragment } from '../../graphql/generated';
import { CredentialsContext } from '../context';
import * as credentialsJsonReader from '../credentialsJson/read';
import { SetUpBuildCredentials } from './actions/SetUpBuildCredentials';
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
import { AppLookupParams } from './api/GraphqlClient';
import { Keystore } from './credentials';
@@ -34,7 +34,7 @@ export default class AndroidCredentialsProvider {
}
private async getRemoteAsync(): Promise<AndroidCredentials> {
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: this.options.app });
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: this.options.app });
const buildCredentials = await setupBuildCredentialsAction.runAsync(this.ctx);
return this.toAndroidCredentials(buildCredentials);
}
@@ -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');
}
}
@@ -25,7 +25,7 @@ interface Options {
* Sets up Build Credentials for Android
* @name: sets up build credentials for the specified configuration. If no name is specified, the default configuration is setup
*/
export class SetUpBuildCredentials {
export class SetupBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: CredentialsContext): Promise<AndroidAppBuildCredentialsFragment> {
@@ -14,7 +14,7 @@ import { AppLookupParams } from '../api/GraphqlClient';
import { getKeystoreWithType } from '../utils/keystoreNew';
import { BackupKeystore } from './DownloadKeystore';
export class SetUpBuildCredentialsFromCredentialsJson {
export class SetupBuildCredentialsFromCredentialsJson {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: CredentialsContext): Promise<AndroidAppBuildCredentialsFragment | null> {
@@ -13,7 +13,7 @@ import { AssignGoogleServiceAccountKey } from './AssignGoogleServiceAccountKey';
import { CreateGoogleServiceAccountKey } from './CreateGoogleServiceAccountKey';
import { UseExistingGoogleServiceAccountKey } from './UseExistingGoogleServiceAccountKey';
export class SetUpGoogleServiceAccountKey {
export class SetupGoogleServiceAccountKey {
constructor(private app: AppLookupParams) {}
public async runAsync(ctx: CredentialsContext): Promise<CommonAndroidAppCredentialsFragment> {
@@ -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: {
@@ -9,7 +9,7 @@ import { createCtxMock } from '../../../__tests__/fixtures-context';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { generateRandomKeystoreAsync } from '../../utils/keystore';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetUpBuildCredentials } from '../SetUpBuildCredentials';
import { SetupBuildCredentials } from '../SetupBuildCredentials';
jest.mock('../../../../ora');
jest.mock('../../../../project/ensureProjectExists');
@@ -17,7 +17,7 @@ jest.mock('../../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() =
jest.mock('../../../../prompts', () => ({ confirmAsync: jest.fn(() => true) }));
jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn() }));
describe('SetUpBuildCredentials', () => {
describe('SetupBuildCredentials', () => {
beforeEach(() => {
asMock(generateRandomKeystoreAsync).mockReset();
});
@@ -33,7 +33,7 @@ describe('SetUpBuildCredentials', () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
await setupBuildCredentialsAction.runAsync(ctx);
// expect keystore not to be created
@@ -48,7 +48,7 @@ describe('SetUpBuildCredentials', () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
await setupBuildCredentialsAction.runAsync(ctx);
// expect keystore to be created
@@ -63,7 +63,7 @@ describe('SetUpBuildCredentials', () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
);
@@ -14,7 +14,7 @@ import {
SelectAndroidBuildCredentialsResultType,
} from '../../../manager/SelectAndroidBuildCredentials';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetUpBuildCredentialsFromCredentialsJson } from '../SetUpBuildCredentialsFromCredentialsJson';
import { SetupBuildCredentialsFromCredentialsJson } from '../SetupBuildCredentialsFromCredentialsJson';
jest.mock('fs');
@@ -26,7 +26,7 @@ beforeEach(() => {
vol.reset();
});
describe(SetUpBuildCredentialsFromCredentialsJson, () => {
describe(SetupBuildCredentialsFromCredentialsJson, () => {
it('sets up a new build configuration from credentials.json upon user request', async () => {
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
return {
@@ -59,7 +59,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
'keystore.jks': 'some-binary-content',
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
appLookupParams
);
await setupBuildCredentialsAction.runAsync(ctx);
@@ -109,7 +109,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
'keystore.jks': 'some-binary-content',
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
appLookupParams
);
await setupBuildCredentialsAction.runAsync(ctx);
@@ -132,7 +132,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
nonInteractive: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
appLookupParams
);
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError();
@@ -10,7 +10,7 @@ import {
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetUpGoogleServiceAccountKey } from '../SetUpGoogleServiceAccountKey';
import { SetupGoogleServiceAccountKey } from '../SetupGoogleServiceAccountKey';
jest.mock('../../../../prompts');
jest.mock('fs');
@@ -22,7 +22,7 @@ beforeEach(() => {
vol.reset();
});
describe(SetUpGoogleServiceAccountKey, () => {
describe(SetupGoogleServiceAccountKey, () => {
it('skips setup when there is a Google Service Account Key already assigned to the project', async () => {
const ctx = createCtxMock({
nonInteractive: false,
@@ -34,7 +34,7 @@ describe(SetUpGoogleServiceAccountKey, () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
await setupGoogleServiceAccountKeyAction.runAsync(ctx);
expect(ctx.android.createGoogleServiceAccountKeyAsync).not.toHaveBeenCalled();
@@ -57,7 +57,7 @@ describe(SetUpGoogleServiceAccountKey, () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
await setupGoogleServiceAccountKeyAction.runAsync(ctx);
expect(ctx.android.createGoogleServiceAccountKeyAsync).toHaveBeenCalledTimes(1);
@@ -68,7 +68,7 @@ describe(SetUpGoogleServiceAccountKey, () => {
nonInteractive: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
await expect(setupGoogleServiceAccountKeyAction.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
);
@@ -29,9 +29,6 @@ export const GoogleServiceAccountKeyQuery = {
`,
{
accountName,
},
{
additionalTypenames: ['GoogleServiceAccountKey'],
}
)
.toPromise()
@@ -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;
}
}
@@ -6,8 +6,8 @@ import { AndroidAppBuildCredentialsFragment, IosDistributionType } from '../../g
import Log from '../../log';
import { findApplicationTarget, findTargetByName } from '../../project/ios/target';
import zipObject from '../../utils/expodash/zipObject';
import { getVcsClient } from '../../vcs';
import GitClient from '../../vcs/clients/git';
import vcs from '../../vcs';
import GitClient from '../../vcs/git';
import { CredentialsContext } from '../context';
import { App, Target, TargetCredentials } from '../ios/types';
import { readRawAsync } from './read';
@@ -294,9 +294,8 @@ async function updateFileAsync(
}
async function isFileUntrackedAsync(path: string): Promise<boolean> {
const vcsClient = getVcsClient();
if (vcsClient instanceof GitClient) {
return await vcsClient.isFileUntrackedAsync(path);
if (vcs instanceof GitClient) {
return await vcs.isFileUntrackedAsync(path);
}
return false;
}
@@ -5,15 +5,3 @@ export class MissingCredentialsNonInteractiveError extends Error {
);
}
}
export class MissingCredentialsError extends Error {
constructor(message?: string) {
super(message ?? 'Credentials are not set up.');
}
}
export class UnsupportedCredentialsChoiceError extends Error {
constructor(message: string, public choice: string) {
super(message);
}
}
@@ -9,8 +9,8 @@ import { CredentialsContext } from '../context';
import * as credentialsJsonReader from '../credentialsJson/read';
import { ensureAllTargetsAreConfigured } from '../credentialsJson/utils';
import { getAppFromContext } from './actions/BuildCredentialsUtils';
import { SetUpBuildCredentials } from './actions/SetUpBuildCredentials';
import { SetUpPushKey } from './actions/SetUpPushKey';
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
import { SetupPushKey } from './actions/SetupPushKey';
import { IosCapabilitiesOptions } from './appstore/ensureAppExists';
import { App, IosCredentials, Target } from './types';
import { isAdHocProfile } from './utils/provisioningProfile';
@@ -58,7 +58,7 @@ export default class IosCredentialsProvider {
}
private async getRemoteAsync(): Promise<IosCredentials> {
return await new SetUpBuildCredentials({
return await new SetupBuildCredentials({
app: this.options.app,
targets: this.options.targets,
distribution: this.options.distribution,
@@ -83,7 +83,7 @@ export default class IosCredentialsProvider {
parentBundleIdentifier: applicationTarget.parentBundleIdentifier,
};
const setupPushKeyAction = new SetUpPushKey(appLookupParams);
const setupPushKeyAction = new SetupPushKey(appLookupParams);
const isPushKeySetup = await setupPushKeyAction.isPushKeySetupAsync(ctx);
if (isPushKeySetup) {
Log.succeed(
@@ -93,7 +93,7 @@ export default class IosCredentialsProvider {
}
const confirmSetup = await confirmAsync({
message: `Would you like to set up Push Notifications for your project?`,
message: `Would you like to setup Push Notifications for your project?`,
});
if (!confirmSetup) {
return null;
@@ -1,254 +0,0 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import { nanoid } from 'nanoid';
import path from 'path';
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
import Log, { learnMore } from '../../../log';
import { confirmAsync, promptAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { fromNow } from '../../../utils/date';
import { CredentialsContext } from '../../context';
import {
getCredentialsFromUserAsync,
shouldAutoGenerateCredentialsAsync,
} from '../../utils/promptForCredentials';
import { AscApiKey } from '../appstore/Credentials.types';
import {
AscApiKeyPath,
MinimalAscApiKey,
ascApiKeyIdSchema,
ascApiKeyIssuerIdSchema,
} from '../credentials';
import { isAscApiKeyValidAndTrackedAsync } from '../validators/validateAscApiKey';
import { formatAppleTeam } from './AppleTeamUtils';
export enum AppStoreApiKeyPurpose {
SUBMISSION_SERVICE = 'EAS Submit',
}
export async function promptForAscApiKeyPathAsync(ctx: CredentialsContext): Promise<AscApiKeyPath> {
const { keyId, keyP8Path } = await promptForKeyP8AndIdAsync();
const bestEffortIssuerId = await getBestEffortIssuerIdAsync(ctx, keyId);
if (bestEffortIssuerId) {
Log.log(`Detected Issuer ID: ${bestEffortIssuerId}`);
return { keyId, issuerId: bestEffortIssuerId, keyP8Path };
}
const issuerId = await promptForIssuerIdAsync();
return { keyId, issuerId, keyP8Path };
}
export async function promptForIssuerIdAsync(): Promise<string> {
Log.log(chalk.bold('An App Store Connect Issuer ID is required'));
Log.log(
`If you're not sure what this is or how to find yours, ${learnMore(
'https://expo.fyi/asc-issuer-id'
)}`
);
// Do not perform uuid validation - Apple's issuerIds are not RFC4122 compliant
const { issuerId } = await getCredentialsFromUserAsync(ascApiKeyIssuerIdSchema);
return issuerId;
}
export async function getMinimalAscApiKeyAsync(ascApiKey: AscApiKey): Promise<MinimalAscApiKey> {
return {
...ascApiKey,
issuerId: ascApiKey.issuerId ?? (await promptForIssuerIdAsync()),
};
}
export async function provideOrGenerateAscApiKeyAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<MinimalAscApiKey> {
if (ctx.nonInteractive) {
throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
}
const userProvided = await promptForAscApiKeyAsync(ctx);
if (!userProvided) {
return await generateAscApiKeyAsync(ctx, purpose);
}
if (!ctx.appStore.authCtx) {
Log.warn('Unable to validate App Store Connect API Key, you are not authenticated with Apple.');
return userProvided;
}
const isValidAndTracked = await isAscApiKeyValidAndTrackedAsync(ctx, userProvided);
if (isValidAndTracked) {
return userProvided;
}
const useUserProvided = await confirmAsync({
message: `App Store Connect API Key with ID ${userProvided.keyId} is not valid on Apple's servers. Proceed anyway?`,
});
if (useUserProvided) {
return userProvided;
}
return await provideOrGenerateAscApiKeyAsync(ctx, purpose);
}
async function generateAscApiKeyAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<MinimalAscApiKey> {
//console.log('debug');
await ctx.appStore.ensureAuthenticatedAsync();
const ascApiKey = await ctx.appStore.createAscApiKeyAsync({
nickname: getAscApiKeyName(purpose),
});
return await getMinimalAscApiKeyAsync(ascApiKey);
}
export function getAscApiKeyName(purpose: AppStoreApiKeyPurpose): string {
const nameParts = ['[Expo]', purpose, nanoid(10)];
return nameParts.join(' ');
}
async function promptForAscApiKeyAsync(ctx: CredentialsContext): Promise<MinimalAscApiKey | null> {
const shouldAutoGenerateCredentials = await shouldAutoGenerateCredentialsAsync(ascApiKeyIdSchema);
if (shouldAutoGenerateCredentials) {
return null;
}
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
const { keyP8Path, keyId, issuerId } = ascApiKeyPath;
return { keyP8: await fs.readFile(keyP8Path, 'utf-8'), keyId, issuerId };
}
async function promptForKeyP8AndIdAsync(): Promise<Pick<AscApiKeyPath, 'keyP8Path' | 'keyId'>> {
Log.log(
chalk.bold('An App Store Connect Api key is required to upload your app to the Apple App Store')
);
Log.log(
`If you're not sure what this is or how to create one, ${learnMore(
'https://expo.fyi/creating-asc-api-key'
)}`
);
const { keyP8Path } = await promptAsync({
type: 'text',
name: 'keyP8Path',
message: 'Path to App Store Connect API Key:',
initial: 'AuthKey_ABCD.p8',
// eslint-disable-next-line async-protect/async-suffix
validate: async (filePath: string) => {
try {
const stats = await fs.stat(filePath);
if (stats.isFile()) {
return true;
}
return 'Input is not a file.';
} catch {
return 'File does not exist.';
}
},
});
const regex = /^AuthKey_(?<keyId>\w+)\.p8$/; // Common ASC Api file name downloaded from Apple
const bestEffortKeyId = path.basename(keyP8Path).match(regex)?.groups?.keyId;
const { keyId } = await getCredentialsFromUserAsync(ascApiKeyIdSchema, {
keyId: bestEffortKeyId,
});
return { keyId, keyP8Path };
}
async function getBestEffortIssuerIdAsync(
ctx: CredentialsContext,
ascApiKeyId: string
): Promise<string | null> {
if (!ctx.appStore.authCtx) {
return null;
}
const ascApiKeyInfo = await ctx.appStore.getAscApiKeyAsync(ascApiKeyId);
return ascApiKeyInfo?.issuerId ?? null;
}
export async function getAscApiKeysFromAccountAsync(
ctx: CredentialsContext,
account: Account,
{ filterDifferentAppleTeam }: { filterDifferentAppleTeam?: boolean } = {}
): Promise<AppStoreConnectApiKeyFragment[]> {
const ascApiKeysForAccount = await ctx.ios.getAscApiKeysForAccountAsync(account);
if (!filterDifferentAppleTeam) {
return ascApiKeysForAccount;
} else {
return filterKeysFromDifferentAppleTeam(ctx, ascApiKeysForAccount);
}
}
export async function selectAscApiKeysFromAccountAsync(
ctx: CredentialsContext,
account: Account,
{ filterDifferentAppleTeam }: { filterDifferentAppleTeam?: boolean } = {}
): Promise<AppStoreConnectApiKeyFragment | null> {
const ascApiKeysForAccount = await getAscApiKeysFromAccountAsync(ctx, account, {
filterDifferentAppleTeam,
});
if (ascApiKeysForAccount.length === 0) {
if (filterDifferentAppleTeam) {
const maybeAppleTeamId = ctx.appStore.authCtx?.team.id;
Log.warn(
`There are no App Store Connect API Keys in your EAS account${
maybeAppleTeamId ? ` matching Apple Team ID: ${maybeAppleTeamId}.` : '.'
}`
);
} else {
Log.warn(`There are no App Store Connect API Keys available in your EAS account.`);
}
return null;
}
return selectAscApiKeysAsync(ascApiKeysForAccount);
}
async function selectAscApiKeysAsync(
ascApiKeys: AppStoreConnectApiKeyFragment[]
): Promise<AppStoreConnectApiKeyFragment | null> {
const sortedAscApiKeys = sortAscApiKeysByUpdatedAtDesc(ascApiKeys);
const { chosenAscApiKey } = await promptAsync({
type: 'select',
name: 'chosenAscApiKey',
message: 'Select an API Key from the list:',
choices: sortedAscApiKeys.map(ascApiKey => ({
title: formatAscApiKey(ascApiKey),
value: ascApiKey,
})),
});
return chosenAscApiKey;
}
function filterKeysFromDifferentAppleTeam(
ctx: CredentialsContext,
keys: AppStoreConnectApiKeyFragment[]
): AppStoreConnectApiKeyFragment[] {
if (!ctx.appStore.authCtx) {
return keys;
}
const teamId = ctx.appStore.authCtx.team.id;
return keys.filter(key => !key.appleTeam || key.appleTeam?.appleTeamIdentifier === teamId);
}
export function sortAscApiKeysByUpdatedAtDesc(
keys: AppStoreConnectApiKeyFragment[]
): AppStoreConnectApiKeyFragment[] {
return keys.sort(
(keyA, keyB) => new Date(keyB.updatedAt).getTime() - new Date(keyA.updatedAt).getTime()
);
}
export function formatAscApiKey(ascApiKey: AppStoreConnectApiKeyFragment): string {
const { keyIdentifier, appleTeam, name, updatedAt } = ascApiKey;
let line: string = '';
line += `Key ID: ${keyIdentifier}`;
if (name) {
line += chalk.gray(`\n Name: ${name}`);
}
if (appleTeam) {
line += chalk.gray(`\n ${formatAppleTeam(appleTeam)}`);
}
line += chalk.gray(`\n Updated: ${fromNow(new Date(updatedAt))} ago`);
return line;
}
@@ -1,38 +0,0 @@
import {
AppStoreConnectApiKeyFragment,
CommonIosAppCredentialsFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { AppStoreApiKeyPurpose } from './AscApiKeyUtils';
export class AssignAscApiKey {
constructor(private app: AppLookupParams) {}
public async runAsync(
ctx: CredentialsContext,
ascApiKey: AppStoreConnectApiKeyFragment,
purpose: AppStoreApiKeyPurpose
): Promise<CommonIosAppCredentialsFragment> {
const appleTeam =
(await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app)) ?? ascApiKey.appleTeam ?? null;
const appCredentials = await ctx.ios.createOrGetIosAppCredentialsWithCommonFieldsAsync(
this.app,
{ appleTeam: appleTeam ?? undefined }
);
let updatedAppCredentials;
if (purpose === AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
updatedAppCredentials = await ctx.ios.updateIosAppCredentialsAsync(appCredentials, {
ascApiKeyIdForSubmissions: ascApiKey.id,
});
} else {
throw new Error(`${purpose} is not yet supported.`);
}
Log.succeed(
`App Store Connect API Key assigned to ${this.app.projectName}: ${this.app.bundleIdentifier} for ${purpose}.`
);
return updatedAppCredentials;
}
}
@@ -1,23 +0,0 @@
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { Account } from '../../../user/Account';
import { CredentialsContext } from '../../context';
import { AppStoreApiKeyPurpose, provideOrGenerateAscApiKeyAsync } from './AscApiKeyUtils';
export class CreateAscApiKey {
constructor(private account: Account) {}
public async runAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<AppStoreConnectApiKeyFragment> {
if (ctx.nonInteractive) {
throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
}
const ascApiKey = await provideOrGenerateAscApiKeyAsync(ctx, purpose);
const result = await ctx.ios.createAscApiKeyAsync(this.account, ascApiKey);
Log.succeed('Created App Store Connect API Key');
return result;
}
}
@@ -10,7 +10,7 @@ export class CreateDistributionCertificate {
public async runAsync(
ctx: CredentialsContext
): Promise<AppleDistributionCertificateMutationResult> {
const distCert = await provideOrGenerateDistributionCertificateAsync(ctx);
const distCert = await provideOrGenerateDistributionCertificateAsync(ctx, this.account.name);
const result = await ctx.ios.createDistributionCertificateAsync(this.account, distCert);
Log.succeed('Created distribution certificate');
return result;
@@ -12,7 +12,7 @@ export class CreatePushKey {
throw new Error(`A new push key cannot be created in non-interactive mode.`);
}
const pushKey = await provideOrGeneratePushKeyAsync(ctx);
const pushKey = await provideOrGeneratePushKeyAsync(ctx, this.account.name);
const result = await ctx.ios.createPushKeyAsync(this.account, pushKey);
Log.succeed('Created push key');
return result;
@@ -152,14 +152,15 @@ Please remember that Apple Distribution Certificates are not application specifi
`;
export async function provideOrGenerateDistributionCertificateAsync(
ctx: CredentialsContext
ctx: CredentialsContext,
accountName: string
): Promise<DistributionCertificate> {
if (!ctx.nonInteractive) {
const userProvided = await promptForDistCertAsync(ctx);
if (userProvided) {
if (!ctx.appStore.authCtx) {
Log.warn(
'Unable to validate distribution certificate, you are not authenticated with Apple.'
'Unable to validate distribution certificate due to insufficient Apple Credentials'
);
return userProvided;
} else {
@@ -167,11 +168,13 @@ export async function provideOrGenerateDistributionCertificateAsync(
if (!isValid) {
Log.warn("Provided Distribution Certificate is no longer valid on Apple's server");
}
return isValid ? userProvided : await provideOrGenerateDistributionCertificateAsync(ctx);
return isValid
? userProvided
: await provideOrGenerateDistributionCertificateAsync(ctx, accountName);
}
}
}
return await generateDistributionCertificateAsync(ctx);
return await generateDistributionCertificateAsync(ctx, accountName);
}
async function promptForDistCertAsync(
@@ -197,7 +200,8 @@ async function promptForDistCertAsync(
}
async function generateDistributionCertificateAsync(
ctx: CredentialsContext
ctx: CredentialsContext,
accountName: string
): Promise<DistributionCertificate> {
await ctx.appStore.ensureAuthenticatedAsync();
try {
@@ -242,7 +246,7 @@ async function generateDistributionCertificateAsync(
throw e;
}
}
return await generateDistributionCertificateAsync(ctx);
return await generateDistributionCertificateAsync(ctx, accountName);
}
function formatDistributionCertificateFromApple(
@@ -14,12 +14,15 @@ import { pushKeySchema } from '../credentials';
import { isPushKeyValidAndTrackedAsync } from '../validators/validatePushKey';
import { formatAppleTeam } from './AppleTeamUtils';
export async function provideOrGeneratePushKeyAsync(ctx: CredentialsContext): Promise<PushKey> {
export async function provideOrGeneratePushKeyAsync(
ctx: CredentialsContext,
accountName: string
): Promise<PushKey> {
if (!ctx.nonInteractive) {
const userProvided = await promptForPushKeyAsync(ctx);
if (userProvided) {
if (!ctx.appStore.authCtx) {
Log.warn('Unable to validate push key, you are not authenticated with Apple.');
Log.warn('Unable to validate push key due to insufficient Apple Credentials');
return userProvided;
} else {
const isValidAndTracked = await isPushKeyValidAndTrackedAsync(ctx, userProvided);
@@ -33,11 +36,11 @@ export async function provideOrGeneratePushKeyAsync(ctx: CredentialsContext): Pr
if (useUserProvided) {
return userProvided;
}
return await provideOrGeneratePushKeyAsync(ctx);
return await provideOrGeneratePushKeyAsync(ctx, accountName);
}
}
}
return await generatePushKeyAsync(ctx);
return await generatePushKeyAsync(ctx, accountName);
}
async function promptForPushKeyAsync(ctx: CredentialsContext): Promise<PushKey | null> {
@@ -60,7 +63,10 @@ async function promptForPushKeyAsync(ctx: CredentialsContext): Promise<PushKey |
return userProvided;
}
async function generatePushKeyAsync(ctx: CredentialsContext): Promise<PushKey> {
async function generatePushKeyAsync(
ctx: CredentialsContext,
accountName: string
): Promise<PushKey> {
await ctx.appStore.ensureAuthenticatedAsync();
try {
return await ctx.appStore.createPushKeyAsync();
@@ -100,7 +106,7 @@ async function generatePushKeyAsync(ctx: CredentialsContext): Promise<PushKey> {
throw e;
}
}
return await generatePushKeyAsync(ctx);
return await generatePushKeyAsync(ctx, accountName);
}
function formatPushKeyFromApple(pushKey: PushKeyStoreInfo): string {
@@ -1,53 +0,0 @@
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { CredentialsContext } from '../../context';
import { selectAscApiKeysFromAccountAsync } from './AscApiKeyUtils';
export class SelectAndRemoveAscApiKey {
constructor(private account: Account) {}
async runAsync(ctx: CredentialsContext): Promise<void> {
const selected = await selectAscApiKeysFromAccountAsync(ctx, this.account);
if (selected) {
await new RemoveAscApiKey(this.account, selected).runAsync(ctx);
Log.succeed('Removed App Store Connect API Key');
Log.newLine();
}
}
}
export class RemoveAscApiKey {
constructor(private account: Account, private ascApiKey: AppStoreConnectApiKeyFragment) {}
public async runAsync(ctx: CredentialsContext): Promise<void> {
if (ctx.nonInteractive) {
throw new Error(`Cannot remove App Store Connect API Keys in non-interactive mode.`);
}
// TODO(quin): add an extra edge on AppStoreConnectApiKey to find the apps using it
const confirm = await confirmAsync({
message: `Deleting this API Key may affect your projects that rely on it. Do you want to continue?`,
});
if (!confirm) {
Log.log('Aborting');
return;
}
Log.log('Removing API Key');
await ctx.ios.deleteAscApiKeyAsync(this.ascApiKey.id);
let shouldRevoke = false;
if (!ctx.nonInteractive) {
shouldRevoke = await confirmAsync({
message: `Do you also want to revoke this API Key on the Apple Developer Portal?`,
});
} else if (ctx.nonInteractive) {
Log.log('Skipping API Key revocation on the Apple Developer Portal.');
}
if (shouldRevoke) {
await ctx.appStore.revokeAscApiKeyAsync(this.ascApiKey.keyIdentifier);
}
}
}
@@ -1,148 +0,0 @@
import assert from 'assert';
import nullthrows from 'nullthrows';
import {
AppStoreConnectApiKeyFragment,
CommonIosAppCredentialsFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync, promptAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import {
MissingCredentialsNonInteractiveError,
UnsupportedCredentialsChoiceError,
} from '../../errors';
import { AppLookupParams } from '../api/GraphqlClient';
import { getValidAndTrackedAscApiKeysAsync } from '../validators/validateAscApiKey';
import {
AppStoreApiKeyPurpose,
formatAscApiKey,
getAscApiKeysFromAccountAsync,
selectAscApiKeysFromAccountAsync,
sortAscApiKeysByUpdatedAtDesc,
} from './AscApiKeyUtils';
import { AssignAscApiKey } from './AssignAscApiKey';
import { CreateAscApiKey } from './CreateAscApiKey';
export enum SetupAscApiKeyChoice {
GENERATE = 'GENERATE',
USE_EXISTING = 'USE_EXISTING',
}
export class SetUpAscApiKey {
public choices: { title: string; value: string }[] = [
{
title: '[Choose an existing ASC API Key]',
value: SetupAscApiKeyChoice.USE_EXISTING,
},
{ title: '[Add a new ASC API Key]', value: SetupAscApiKeyChoice.GENERATE },
];
constructor(private app: AppLookupParams, private purpose: AppStoreApiKeyPurpose) {}
public async runAsync(ctx: CredentialsContext): Promise<CommonIosAppCredentialsFragment> {
const isKeySetup = await this.isAscApiKeySetupAsync(ctx, this.purpose);
if (isKeySetup) {
Log.succeed('App Store Connect API Key already set up.');
return nullthrows(
await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(this.app),
'iosAppCredentials cannot be null if App Store Connect API Key is already set up'
);
}
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'App Store Connect API Keys cannot be set up in --non-interactive mode.'
);
}
const keysForAccount = await getAscApiKeysFromAccountAsync(ctx, this.app.account, {
filterDifferentAppleTeam: true,
});
const maybeAutoselectedKey = await this.doBestEffortAutoselectAsync(ctx, keysForAccount);
if (maybeAutoselectedKey) {
return await new AssignAscApiKey(this.app).runAsync(ctx, maybeAutoselectedKey, this.purpose);
}
const availableChoices =
keysForAccount.length === 0
? this.choices.filter(choice => choice.value !== SetupAscApiKeyChoice.USE_EXISTING)
: this.choices;
const ascApiKey = await this.processChoicesAsync(ctx, this.purpose, availableChoices);
return await new AssignAscApiKey(this.app).runAsync(ctx, ascApiKey, this.purpose);
}
private async doBestEffortAutoselectAsync(
ctx: CredentialsContext,
keysForAccount: AppStoreConnectApiKeyFragment[]
): Promise<AppStoreConnectApiKeyFragment | null> {
if (!ctx.appStore.authCtx) {
return null;
}
if (keysForAccount.length === 0) {
return null;
}
// only provide autoselect if we can find a key that is certainly valid
const validKeys = await getValidAndTrackedAscApiKeysAsync(ctx, keysForAccount);
if (validKeys.length === 0) {
return null;
}
const [autoselectedKey] = sortAscApiKeysByUpdatedAtDesc(validKeys);
const useAutoselected = await confirmAsync({
message: `Reuse this App Store Connect API Key?\n${formatAscApiKey(autoselectedKey)}`,
});
if (useAutoselected) {
Log.log(`Using App Store Connect API Key with ID ${autoselectedKey.keyIdentifier}`);
return autoselectedKey;
} else {
return null;
}
}
private async isAscApiKeySetupAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<boolean> {
const appCredentials = await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(this.app);
if (purpose !== AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
throw new Error(`App Store Connect API Key setup is not yet supported for ${purpose}.`);
}
return !!appCredentials?.appStoreConnectApiKeyForSubmissions;
}
private async processChoicesAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose,
choices: { title: string; value: string }[]
): Promise<AppStoreConnectApiKeyFragment> {
assert(choices.length > 0, 'SetupAscApiKey: There must be at least one choice');
let choice: string;
if (choices.length === 1) {
choice = choices[0].value;
} else {
const result = await promptAsync({
type: 'select',
name: 'choice',
message: 'Select what to use for your project:',
choices,
});
choice = result.choice;
}
if (choice === SetupAscApiKeyChoice.GENERATE) {
return await new CreateAscApiKey(this.app.account).runAsync(ctx, purpose);
} else if (choice === SetupAscApiKeyChoice.USE_EXISTING) {
const selectedAscApiKey = await selectAscApiKeysFromAccountAsync(ctx, this.app.account, {
filterDifferentAppleTeam: true,
});
if (!selectedAscApiKey) {
return await this.processChoicesAsync(ctx, purpose, choices);
}
return selectedAscApiKey;
}
throw new UnsupportedCredentialsChoiceError(
`AscApiKey Setup does not support choice:${choice}`,
choice
);
}
}
@@ -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;
}
}
@@ -21,7 +21,7 @@ import { validateProvisioningProfileAsync } from '../validators/validateProvisio
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { assignBuildCredentialsAsync, getBuildCredentialsAsync } from './BuildCredentialsUtils';
import { chooseDevicesAsync, formatDeviceLabel } from './DeviceUtils';
import { SetUpDistributionCertificate } from './SetUpDistributionCertificate';
import { SetupDistributionCertificate } from './SetupDistributionCertificate';
enum ReuseAction {
Yes,
@@ -29,11 +29,11 @@ enum ReuseAction {
No,
}
export class SetUpAdhocProvisioningProfile {
export class SetupAdhocProvisioningProfile {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
const distCert = await new SetUpDistributionCertificate(
const distCert = await new SetupDistributionCertificate(
this.app,
IosDistributionType.AdHoc
).runAsync(ctx);
@@ -7,7 +7,7 @@ import { CredentialsContext } from '../../context';
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
import { App, IosAppBuildCredentialsMap, IosCredentials, Target } from '../types';
import { displayProjectCredentials } from '../utils/printCredentials';
import { SetUpTargetBuildCredentials } from './SetUpTargetBuildCredentials';
import { SetupTargetBuildCredentials } from './SetupTargetBuildCredentials';
interface Options {
app: App;
@@ -17,7 +17,7 @@ interface Options {
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
export class SetUpBuildCredentials {
export class SetupBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: CredentialsContext): Promise<IosCredentials> {
@@ -49,7 +49,7 @@ export class SetUpBuildCredentials {
} else {
Log.newLine();
}
const action = new SetUpTargetBuildCredentials({
const action = new SetupTargetBuildCredentials({
...this.options,
app: {
...this.options.app,
@@ -8,9 +8,9 @@ import { readIosCredentialsAsync } from '../../credentialsJson/read';
import { IosCredentials } from '../../credentialsJson/types';
import { ensureAllTargetsAreConfigured } from '../../credentialsJson/utils';
import { App, Target } from '../types';
import { SetUpTargetBuildCredentialsFromCredentialsJson } from './SetUpTargetBuildCredentialsFromCredentialsJson';
import { SetupTargetBuildCredentialsFromCredentialsJson } from './SetupTargetBuildCredentialsFromCredentialsJson';
export class SetUpBuildCredentialsFromCredentialsJson {
export class SetupBuildCredentialsFromCredentialsJson {
constructor(
private app: App,
private targets: Target[],
@@ -32,7 +32,7 @@ export class SetUpBuildCredentialsFromCredentialsJson {
);
Log.newLine();
}
await new SetUpTargetBuildCredentialsFromCredentialsJson(
await new SetupTargetBuildCredentialsFromCredentialsJson(
{
account: this.app.account,
projectName: this.app.projectName,
@@ -18,7 +18,7 @@ import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { CreateDistributionCertificate } from './CreateDistributionCertificate';
import { formatDistributionCertificate } from './DistributionCertificateUtils';
export class SetUpDistributionCertificate {
export class SetupDistributionCertificate {
private validDistCerts?: AppleDistributionCertificateFragment[];
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
@@ -4,8 +4,8 @@ import { promptAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
import { getAllBuildCredentialsAsync } from './BuildCredentialsUtils';
import { SetUpAdhocProvisioningProfile } from './SetUpAdhocProvisioningProfile';
import { SetUpProvisioningProfile } from './SetUpProvisioningProfile';
import { SetupAdhocProvisioningProfile } from './SetupAdhocProvisioningProfile';
import { SetupProvisioningProfile } from './SetupProvisioningProfile';
/**
* It's used when setting up credentials for internal distribution but `enterpriseProvisioning` is not set.
@@ -14,7 +14,7 @@ import { SetUpProvisioningProfile } from './SetUpProvisioningProfile';
* to choose if they want to set up an adhoc or universal distribution provisioning profile. Otherwise, always
* set up an adhoc provisioning profile.
*/
export class SetUpInternalProvisioningProfile {
export class SetupInternalProvisioningProfile {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
@@ -82,13 +82,13 @@ export class SetUpInternalProvisioningProfile {
private async setupAdhocProvisioningProfileAsync(
ctx: CredentialsContext
): Promise<IosAppBuildCredentialsFragment> {
return await new SetUpAdhocProvisioningProfile(this.app).runAsync(ctx);
return await new SetupAdhocProvisioningProfile(this.app).runAsync(ctx);
}
private async setupUniversalProvisioningProfileAsync(
ctx: CredentialsContext
): Promise<IosAppBuildCredentialsFragment> {
return await new SetUpProvisioningProfile(this.app, IosDistributionType.Enterprise).runAsync(
return await new SetupProvisioningProfile(this.app, IosDistributionType.Enterprise).runAsync(
ctx
);
}
@@ -20,12 +20,12 @@ import {
import { ConfigureProvisioningProfile } from './ConfigureProvisioningProfile';
import { CreateProvisioningProfile } from './CreateProvisioningProfile';
import { formatProvisioningProfileFromApple } from './ProvisioningProfileUtils';
import { SetUpDistributionCertificate } from './SetUpDistributionCertificate';
import { SetupDistributionCertificate } from './SetupDistributionCertificate';
/**
* Sets up either APP_STORE or ENTERPRISE provisioning profiles
*/
export class SetUpProvisioningProfile {
export class SetupProvisioningProfile {
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
async areBuildCredentialsSetupAsync(ctx: CredentialsContext): Promise<boolean> {
@@ -84,7 +84,7 @@ export class SetUpProvisioningProfile {
}
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
const distCert = await new SetUpDistributionCertificate(
const distCert = await new SetupDistributionCertificate(
this.app,
this.distributionType
).runAsync(ctx);
@@ -14,7 +14,7 @@ import {
selectPushKeyAsync,
} from './PushKeyUtils';
export class SetUpPushKey {
export class SetupPushKey {
constructor(private app: AppLookupParams) {}
async isPushKeySetupAsync(ctx: CredentialsContext): Promise<boolean> {
@@ -8,9 +8,9 @@ import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { AppLookupParams as GraphQLAppLookupParams } from '../api/GraphqlClient';
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
import { SetUpAdhocProvisioningProfile } from './SetUpAdhocProvisioningProfile';
import { SetUpInternalProvisioningProfile } from './SetUpInternalProvisioningProfile';
import { SetUpProvisioningProfile } from './SetUpProvisioningProfile';
import { SetupAdhocProvisioningProfile } from './SetupAdhocProvisioningProfile';
import { SetupInternalProvisioningProfile } from './SetupInternalProvisioningProfile';
import { SetupProvisioningProfile } from './SetupProvisioningProfile';
interface Options {
app: GraphQLAppLookupParams;
@@ -18,7 +18,7 @@ interface Options {
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
export class SetUpTargetBuildCredentials {
export class SetupTargetBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
@@ -39,7 +39,7 @@ export class SetUpTargetBuildCredentials {
try {
return await this.setupBuildCredentialsAsync(ctx);
} catch (error) {
Log.error('Failed to set up credentials.');
Log.error('Failed to setup credentials.');
throw error;
}
}
@@ -50,17 +50,17 @@ export class SetUpTargetBuildCredentials {
const { app, distribution, enterpriseProvisioning } = this.options;
if (distribution === 'internal') {
if (enterpriseProvisioning === 'adhoc') {
return await new SetUpAdhocProvisioningProfile(app).runAsync(ctx);
return await new SetupAdhocProvisioningProfile(app).runAsync(ctx);
} else if (enterpriseProvisioning === 'universal') {
return await new SetUpProvisioningProfile(
return await new SetupProvisioningProfile(
app,
GraphQLIosDistributionType.Enterprise
).runAsync(ctx);
} else {
return await new SetUpInternalProvisioningProfile(app).runAsync(ctx);
return await new SetupInternalProvisioningProfile(app).runAsync(ctx);
}
} else {
return await new SetUpProvisioningProfile(app, GraphQLIosDistributionType.AppStore).runAsync(
return await new SetupProvisioningProfile(app, GraphQLIosDistributionType.AppStore).runAsync(
ctx
);
}
@@ -22,7 +22,7 @@ import {
getProvisioningProfileAsync,
} from './BuildCredentialsUtils';
export class SetUpTargetBuildCredentialsFromCredentialsJson {
export class SetupTargetBuildCredentialsFromCredentialsJson {
constructor(
private app: AppLookupParams,
private distributionType: IosDistributionType,
@@ -5,7 +5,7 @@ import {
import { testDistCertFragmentOneDependency } from '../../../__tests__/fixtures-ios';
import { CredentialsContext } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
export class SetUpDistributionCertificate {
export class SetupDistributionCertificate {
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
public async runAsync(_ctx: CredentialsContext): Promise<AppleDistributionCertificateFragment> {
@@ -1,90 +0,0 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKey } from '../../../__tests__/fixtures-ios';
import { getCredentialsFromUserAsync } from '../../../utils/promptForCredentials';
import {
AppStoreApiKeyPurpose,
getAscApiKeyName,
promptForAscApiKeyPathAsync,
} from '../AscApiKeyUtils';
jest.mock('../../../../prompts');
jest.mock('../../../utils/promptForCredentials');
function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
return Object.keys(obj) as K[];
}
afterEach(() => {
asMock(promptAsync).mockClear();
asMock(getCredentialsFromUserAsync).mockClear();
});
describe(getAscApiKeyName, () => {
// Apple enforces a 30 char limit on this name
it('produces a name under 30 chars', async () => {
for (const value of enumKeys(AppStoreApiKeyPurpose)) {
const purpose = AppStoreApiKeyPurpose[value];
const name = getAscApiKeyName(purpose);
expect(name.length < 30).toBe(true);
}
});
});
describe(promptForAscApiKeyPathAsync, () => {
it('prompts for keyId, keyP8Path and issuerId when user is not authenticated to Apple', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => {
throw new Error('this should not be called');
}),
authCtx: null,
},
});
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
expect(ascApiKeyPath).toEqual({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
keyP8Path: '/asc-api-key.p8',
});
expect(promptAsync).toHaveBeenCalledTimes(1); // keyP8Path
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(2); // keyId, issuerId
});
it('prompts for keyId, keyP8Path and detects issuerId when user is authenticated to Apple', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
keyId: 'test-key-id',
}));
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
getAscApiKeyAsync: jest.fn(() => testAscApiKey),
},
});
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
expect(ascApiKeyPath).toEqual({
keyId: 'test-key-id',
issuerId: 'test-issuer-id-from-apple',
keyP8Path: '/asc-api-key.p8',
});
expect(promptAsync).toHaveBeenCalledTimes(1); // keyP8Path
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(1); // keyId
expect(asMock(ctx.appStore.getAscApiKeyAsync).mock.calls.length).toBe(1); // issuerId
});
});
@@ -1,46 +0,0 @@
import { findApplicationTarget } from '../../../../project/ios/target';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKeyFragment, testTargets } from '../../../__tests__/fixtures-ios';
import { AppStoreApiKeyPurpose } from '../AscApiKeyUtils';
import { AssignAscApiKey } from '../AssignAscApiKey';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
describe(AssignAscApiKey, () => {
it('assigns an App Store Connect API Key in Interactive Mode for EAS Submit', async () => {
const ctx = createCtxMock({
nonInteractive: false,
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const assignAscApiKeyAction = new AssignAscApiKey(appLookupParams);
await assignAscApiKeyAction.runAsync(
ctx,
testAscApiKeyFragment,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
// expect app credentials to be fetched/created, then updated
expect(ctx.ios.createOrGetIosAppCredentialsWithCommonFieldsAsync).toHaveBeenCalledTimes(1);
expect(ctx.ios.updateIosAppCredentialsAsync).toHaveBeenCalledTimes(1);
});
it('works in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const assignAscApiKeyAction = new AssignAscApiKey(appLookupParams);
await assignAscApiKeyAction.runAsync(
ctx,
testAscApiKeyFragment,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
// dont fail if users are running in non-interactive mode
await expect(
assignAscApiKeyAction.runAsync(
ctx,
testAscApiKeyFragment,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
)
).resolves.not.toThrowError();
});
});
@@ -1,44 +0,0 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKey, testTargets } from '../../../__tests__/fixtures-ios';
import { AppStoreApiKeyPurpose } from '../AscApiKeyUtils';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreateAscApiKey } from '../CreateAscApiKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(CreateAscApiKey, () => {
it('creates a App Store API Key in Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
createAscApiKeyAsync: jest.fn(() => testAscApiKey),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const createAscApiKeyAction = new CreateAscApiKey(appLookupParams.account);
await createAscApiKeyAction.runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
// expect api key to be created on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
// expect api key to be created on apple portal
expect(asMock(ctx.appStore.createAscApiKeyAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const createAscApiKeyAction = new CreateAscApiKey(appLookupParams.account);
await expect(
createAscApiKeyAction.runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE)
).rejects.toThrowError();
});
});
@@ -1,33 +0,0 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKeyFragment, testTargets } from '../../../__tests__/fixtures-ios';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { RemoveAscApiKey } from '../RemoveAscApiKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(RemoveAscApiKey, () => {
it('removes an Asc API Key', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const removeAscApiKeyAction = new RemoveAscApiKey(
appLookupParams.account,
testAscApiKeyFragment
);
await removeAscApiKeyAction.runAsync(ctx);
expect(ctx.ios.deleteAscApiKeyAsync).toHaveBeenCalledTimes(1);
expect(ctx.appStore.revokeAscApiKeyAsync).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const removeAscApiKeyAction = new RemoveAscApiKey(
appLookupParams.account,
testAscApiKeyFragment
);
await expect(removeAscApiKeyAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -1,146 +0,0 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync, promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import {
getNewIosApiMock,
testAscApiKey,
testAscApiKeyFragment,
testCommonIosAppCredentialsFragment,
testTargets,
} from '../../../__tests__/fixtures-ios';
import { AppStoreApiKeyPurpose } from '../AscApiKeyUtils';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetUpAscApiKey, SetupAscApiKeyChoice } from '../SetUpAscApiKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(SetUpAscApiKey, () => {
it('skips setting up a ASC API Key if it is already configured', async () => {
const ctx = createCtxMock({
nonInteractive: false,
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration not to be updated with a new ASC API Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
});
it('sets up a ASC API Key, creating a new one if there are no existing ASC API Keys', async () => {
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
createAscApiKeyAsync: jest.fn(() => testAscApiKey),
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(),
createAscApiKeyAsync: jest.fn(() => testAscApiKeyFragment),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await setupAscApiKeyAction.runAsync(ctx);
// expect to create a new ASC API Key on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
// expect configuration to be updated with a new ASC API Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a ASC API Key, use autoselected ASC API Key when there are existing keys', async () => {
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
listAscApiKeysAsync: jest.fn(() => [testAscApiKey]),
},
ios: {
...getNewIosApiMock(),
getAscApiKeysForAccountAsync: jest.fn(() => [testAscApiKeyFragment]),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers, we are using the existing one
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration to be updated with a new ASC API Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a ASC API Key, allowing user to choose existing keys', async () => {
const ctx = createCtxMock({
nonInteractive: false,
ios: {
...getNewIosApiMock(),
getAscApiKeysForAccountAsync: jest.fn(() => [testAscApiKeyFragment]),
},
});
asMock(promptAsync).mockImplementation(() => ({
choice: SetupAscApiKeyChoice.USE_EXISTING,
chosenAscApiKey: testAscApiKeyFragment,
}));
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers, we are using the existing one
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration to be updated with a new ASC API Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('works in Non Interactive Mode if ASC Key is configured', async () => {
const ctx = createCtxMock({
nonInteractive: true,
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await expect(setupAscApiKeyAction.runAsync(ctx)).resolves.not.toThrowError();
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const createAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await expect(createAscApiKeyAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -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();
}
});
});
@@ -1,4 +1,4 @@
import { doUDIDsMatch } from '../SetUpAdhocProvisioningProfile';
import { doUDIDsMatch } from '../SetupAdhocProvisioningProfile';
describe(doUDIDsMatch, () => {
it('return false if UDIDs do not match', () => {
@@ -4,13 +4,13 @@ import { promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAllBuildCredentialsAsync } from '../BuildCredentialsUtils';
import { SetUpAdhocProvisioningProfile } from '../SetUpAdhocProvisioningProfile';
import { SetUpInternalProvisioningProfile } from '../SetUpInternalProvisioningProfile';
import { SetUpProvisioningProfile } from '../SetUpProvisioningProfile';
import { SetupAdhocProvisioningProfile } from '../SetupAdhocProvisioningProfile';
import { SetupInternalProvisioningProfile } from '../SetupInternalProvisioningProfile';
import { SetupProvisioningProfile } from '../SetupProvisioningProfile';
jest.mock('../../../../prompts');
jest.mock('../SetUpAdhocProvisioningProfile');
jest.mock('../SetUpProvisioningProfile');
jest.mock('../SetupAdhocProvisioningProfile');
jest.mock('../SetupProvisioningProfile');
jest.mock('../BuildCredentialsUtils', () => ({ getAllBuildCredentialsAsync: jest.fn() }));
beforeEach(() => {
@@ -33,15 +33,15 @@ const testEnterpriseBuildCredentials: IosAppBuildCredentialsFragment = {
iosDistributionType: IosDistributionType.Enterprise,
};
describe(SetUpInternalProvisioningProfile, () => {
describe(SetupInternalProvisioningProfile, () => {
describe('interactive mode', () => {
describe('when authenticated with apple', () => {
it('runs the SetUpAdhocProvisioningProfile action for non-enterprise team', async () => {
it('runs the SetupAdhocProvisioningProfile action for non-enterprise team', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
const action = new SetUpInternalProvisioningProfile({
const action = new SetupInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -56,7 +56,7 @@ describe(SetUpInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetUpAdhocProvisioningProfile.prototype.runAsync = runAsync;
SetupAdhocProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
@@ -72,7 +72,7 @@ describe(SetUpInternalProvisioningProfile, () => {
return buildCredentials;
});
const action = new SetUpInternalProvisioningProfile({
const action = new SetupInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -90,7 +90,7 @@ describe(SetUpInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetUpProvisioningProfile.prototype.runAsync = runAsync;
SetupProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
@@ -110,7 +110,7 @@ describe(SetUpInternalProvisioningProfile, () => {
return buildCredentials;
});
const action = new SetUpInternalProvisioningProfile({
const action = new SetupInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -125,14 +125,14 @@ describe(SetUpInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetUpProvisioningProfile.prototype.runAsync = runAsync;
SetupProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
expect(runAsync).toHaveBeenCalled();
});
it('runs the SetUpAdhocProvisioningProfile action when adhoc credentials exist', async () => {
it('runs the SetupAdhocProvisioningProfile action when adhoc credentials exist', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
distributionType: IosDistributionType.Enterprise,
}));
@@ -141,7 +141,7 @@ describe(SetUpInternalProvisioningProfile, () => {
return buildCredentials;
});
const action = new SetUpInternalProvisioningProfile({
const action = new SetupInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -156,14 +156,14 @@ describe(SetUpInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetUpAdhocProvisioningProfile.prototype.runAsync = runAsync;
SetupAdhocProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
expect(runAsync).toHaveBeenCalled();
});
it('runs the SetUpProvisioningProfile action when enterprise credentials exist', async () => {
it('runs the SetupProvisioningProfile action when enterprise credentials exist', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
distributionType: IosDistributionType.Enterprise,
}));
@@ -174,7 +174,7 @@ describe(SetUpInternalProvisioningProfile, () => {
return buildCredentials;
});
const action = new SetUpInternalProvisioningProfile({
const action = new SetupInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -189,7 +189,7 @@ describe(SetUpInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetUpProvisioningProfile.prototype.runAsync = runAsync;
SetupProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
@@ -205,7 +205,7 @@ describe(SetUpInternalProvisioningProfile, () => {
return buildCredentials;
});
const action = new SetUpInternalProvisioningProfile({
const action = new SetupInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -235,7 +235,7 @@ describe(SetUpInternalProvisioningProfile, () => {
];
return buildCredentials;
});
const action = new SetUpInternalProvisioningProfile({
const action = new SetupInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -253,7 +253,7 @@ describe(SetUpInternalProvisioningProfile, () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
const action = new SetUpInternalProvisioningProfile({
const action = new SetupInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -264,12 +264,12 @@ describe(SetUpInternalProvisioningProfile, () => {
await expect(action.runAsync(ctx)).rejects.toThrow(/couldn't find any credentials/);
});
it('runs the SetUpAdhocProvisioningProfile action when adhoc credentials exist', async () => {
it('runs the SetupAdhocProvisioningProfile action when adhoc credentials exist', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [testAdhocBuildCredentials];
return buildCredentials;
});
const action = new SetUpInternalProvisioningProfile({
const action = new SetupInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -279,19 +279,19 @@ describe(SetUpInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetUpAdhocProvisioningProfile.prototype.runAsync = runAsync;
SetupAdhocProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
expect(runAsync).toHaveBeenCalled();
});
it('runs the SetUpProvisioningProfile action when enterprise credentials exist', async () => {
it('runs the SetupProvisioningProfile action when enterprise credentials exist', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [testEnterpriseBuildCredentials];
return buildCredentials;
});
const action = new SetUpInternalProvisioningProfile({
const action = new SetupInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -301,7 +301,7 @@ describe(SetUpInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetUpProvisioningProfile.prototype.runAsync = runAsync;
SetupProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
Loaded 100 of 205 files, more files were not shown because too many files have changed in this diff. Show more