mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
558c367038 | ||
|
|
4b88d00379 | ||
|
|
aad3b5ccd5 | ||
|
|
8b50806093 | ||
|
|
45c9bda2e9 | ||
|
|
23237210d1 | ||
|
|
e34382e6a3 | ||
|
|
b8ad2ee29a | ||
|
|
6bd62bac64 | ||
|
|
8b959fc6d8 | ||
|
|
6e615e20d6 | ||
|
|
efbc5fc4bc | ||
|
|
79c77ae835 | ||
|
|
618b6b1caf | ||
|
|
864658df80 | ||
|
|
4510e86b64 | ||
|
|
4af4490abc | ||
|
|
37c4040203 | ||
|
|
55f9be3b5c | ||
|
|
152b25b93c | ||
|
|
ea0d0bb4dc | ||
|
|
37e360aead | ||
|
|
20211e6265 | ||
|
|
226c888cc9 | ||
|
|
0e9f8ec272 | ||
|
|
8eb5d8028d | ||
|
|
2fb296d116 | ||
|
|
010c1ee439 | ||
|
|
dddc4a0e95 | ||
|
|
679ccf19d9 | ||
|
|
5ba3cc6623 | ||
|
|
c1a4731762 | ||
|
|
767577e386 | ||
|
|
d2f620dbc6 | ||
|
|
0aa556c29e | ||
|
|
10cf003758 | ||
|
|
0e6254453a | ||
|
|
587a29892f | ||
|
|
db8245d954 | ||
|
|
0088306bca | ||
|
|
b6fc718713 | ||
|
|
18e55dafd4 | ||
|
|
8e013ab793 | ||
|
|
b88e79f0f3 | ||
|
|
35b505120d | ||
|
|
6807d3080b | ||
|
|
0d1cfb3286 | ||
|
|
0b9a5621fe | ||
|
|
9bd15af02a | ||
|
|
aab3bf1330 | ||
|
|
9e95218523 | ||
|
|
bb98e6de03 | ||
|
|
2604228233 | ||
|
|
a7035d3469 | ||
|
|
f6dce0e17c | ||
|
|
ccfa6f9fc3 | ||
|
|
0ee8e2f967 | ||
|
|
94dc4c6e93 | ||
|
|
4144e387d6 | ||
|
|
de4e05e1e0 |
No files matched your search
@@ -11,7 +11,6 @@ module.exports = {
|
||||
ignoreDeclarationSort: true,
|
||||
},
|
||||
],
|
||||
'flowtype/no-types-missing-file-annotation': 'off',
|
||||
curly: 'warn',
|
||||
'import/no-cycle': 'error',
|
||||
'import/no-extraneous-dependencies': [
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB |
@@ -3,7 +3,6 @@
|
||||
# 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
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
if: ${{ github.event.issue.pull_request && github.event.issue.user.login == github.event.comment.user.login && startsWith(github.event.comment.body, '/changelog-entry ') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Checkout Pull Request
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
needs: [build-linux, build-mac, build-windows]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
scope: 'expo'
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
needs: publish-to-npm
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- uses: preactjs/compressed-size-action@v2
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
|
||||
@@ -12,3 +12,4 @@ node_modules
|
||||
coverage
|
||||
.history
|
||||
.DS_Store
|
||||
.idea
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"jsxBracketSameLine": true,
|
||||
"bracketSameLine": true,
|
||||
"trailingComma": "es5",
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
+91
-13
@@ -6,13 +6,91 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- `[EAS BUILD API]` Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701) and [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal))
|
||||
- Don't print logs to `stderr`. ([#708](https://github.com/expo/eas-cli/pull/708) by [@dsokal](https://github.com/dsokal))
|
||||
- Remove automatic migration for legacy `eas.json` format introduced in [v0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2). ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add required `cli` field in root of `eas.json` ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Require explicitly defined applicationId/bundleIdentifier for EAS Submit in case of bare projects consisting of multiple product flavors on Android or multiple schemes/targets on iOS. ([#765](https://github.com/expo/eas-cli/pull/765) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Auto create channel on publish. ([#766](https://github.com/expo/eas-cli/pull/766) by [@jkhales](https://github.com/jkhales))
|
||||
- Interactively configure Git `user.name` and `user.email`. ([#772](https://github.com/expo/eas-cli/pull/772) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Validate release channel in eas.json. ([#764](https://github.com/expo/eas-cli/pull/764) by [@dsokal](https://github.com/dsokal))
|
||||
- Make the missing profile error message more descriptive. ([#761](https://github.com/expo/eas-cli/pull/761) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Better error message when missing eas.json. ([#770](https://github.com/expo/eas-cli/pull/770) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.36.1](https://github.com/expo/eas-cli/releases/tag/v0.36.1) - 2021-11-15
|
||||
|
||||
### 🐛 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))
|
||||
|
||||
## [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))
|
||||
- Add applicationId/bundleIdentifier fields to the submit profile. ([#765](https://github.com/expo/eas-cli/pull/765) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 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))
|
||||
@@ -26,7 +104,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
- Remove unneeded parameters. ([#710](https://github.com/expo/eas-cli/pull/710) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Enforce additionalTypenames for graphql queries. ([#709](https://github.com/expo/eas-cli/pull/709) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.33.1](https://github.com/expo/eas-cli/releases/tag/0.33.1) - 2021-10-22
|
||||
## [0.33.1](https://github.com/expo/eas-cli/releases/tag/v0.33.1) - 2021-10-22
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
@@ -36,7 +114,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
- [eas-cli] Skip validating updates scripts integration, which no longer exist in SDK 43+ ([#706](https://github.com/expo/eas-cli/pull/706) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.33.0](https://github.com/expo/eas-cli/releases/tag/0.33.0) - 2021-10-20
|
||||
## [0.33.0](https://github.com/expo/eas-cli/releases/tag/v0.33.0) - 2021-10-20
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
@@ -59,11 +137,11 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
- Skip the second prompt for Apple ID if the user is already signed in with Apple. ([#691](https://github.com/expo/eas-cli/pull/691) by [@dsokal](https://github.com/dsokal))
|
||||
- Unify reading `app.json`. ([#692](https://github.com/expo/eas-cli/pull/692) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.32.0](https://github.com/expo/eas-cli/releases/tag/0.32.0) - 2021-10-15
|
||||
## [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))
|
||||
- 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))
|
||||
|
||||
@@ -73,16 +151,16 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add App Store Connect Api Key fields to `eas.json`. ([#684](https://github.com/expo/eas-cli/pull/684) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add App Store Connect API Key fields to `eas.json`. ([#684](https://github.com/expo/eas-cli/pull/684) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Enable no-underscore-dangle eslint rule. ([#686](https://github.com/expo/eas-cli/pull/686) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.31.1](https://github.com/expo/eas-cli/releases/tag/0.31.1) - 2021-10-08
|
||||
## [0.31.1](https://github.com/expo/eas-cli/releases/tag/v0.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/0.31.0) - 2021-10-08
|
||||
## [0.31.0](https://github.com/expo/eas-cli/releases/tag/v0.31.0) - 2021-10-08
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
@@ -93,7 +171,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/0.30.1) - 2021-10-06
|
||||
## [0.30.1](https://github.com/expo/eas-cli/releases/tag/v0.30.1) - 2021-10-06
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
@@ -472,7 +550,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- `[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 "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))
|
||||
|
||||
+1
-7
@@ -1,10 +1,4 @@
|
||||
## Releasing a new version
|
||||
|
||||
1. Run `yarn release` in the repository root folder.
|
||||
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.
|
||||

|
||||
- Run `npm dist-tags add eas-cli-local-build-plugin@VERSION latest` with the version from the previous step.
|
||||
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.
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.34.0"
|
||||
"version": "0.37.0"
|
||||
}
|
||||
+5
-4
@@ -8,9 +8,10 @@
|
||||
"start": "lerna run watch --parallel",
|
||||
"watch": "yarn start",
|
||||
"eas": "packages/eas-cli/bin/run",
|
||||
"lint": "eslint . --ext .ts --ext .js",
|
||||
"release": "lerna version",
|
||||
"test": "jest"
|
||||
"lint": "eslint .",
|
||||
"release": "./scripts/bin/run update-local-plugin && lerna version",
|
||||
"test": "jest",
|
||||
"clean": "lerna run clean && rm -rf node_modules coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/oclif-dev-cli": "1.24.0-expo",
|
||||
@@ -23,7 +24,7 @@
|
||||
"jest": "27.1.0",
|
||||
"jest-watch-typeahead": "0.6.4",
|
||||
"lerna": "4.0.0",
|
||||
"prettier": "2.3.2",
|
||||
"prettier": "2.4.1",
|
||||
"ts-jest": "27.0.5",
|
||||
"typescript": "4.4.2"
|
||||
},
|
||||
|
||||
@@ -33,7 +33,265 @@ eas --help COMMAND
|
||||
# Commands
|
||||
|
||||
<!-- commands -->
|
||||
* [`eas account:login`](#eas-accountlogin)
|
||||
* [`eas account:logout`](#eas-accountlogout)
|
||||
* [`eas account:view`](#eas-accountview)
|
||||
* [`eas analytics [STATUS]`](#eas-analytics-status)
|
||||
* [`eas build`](#eas-build)
|
||||
* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
|
||||
* [`eas build:configure`](#eas-buildconfigure)
|
||||
* [`eas build:list`](#eas-buildlist)
|
||||
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
|
||||
* [`eas config`](#eas-config)
|
||||
* [`eas credentials`](#eas-credentials)
|
||||
* [`eas device:create`](#eas-devicecreate)
|
||||
* [`eas device:list`](#eas-devicelist)
|
||||
* [`eas device:view [UDID]`](#eas-deviceview-udid)
|
||||
* [`eas diagnostics`](#eas-diagnostics)
|
||||
* [`eas help [COMMAND]`](#eas-help-command)
|
||||
* [`eas project:info`](#eas-projectinfo)
|
||||
* [`eas project:init`](#eas-projectinit)
|
||||
* [`eas secret:create`](#eas-secretcreate)
|
||||
* [`eas secret:delete`](#eas-secretdelete)
|
||||
* [`eas secret:list`](#eas-secretlist)
|
||||
* [`eas submit`](#eas-submit)
|
||||
* [`eas webhook:create`](#eas-webhookcreate)
|
||||
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
|
||||
* [`eas webhook:list`](#eas-webhooklist)
|
||||
* [`eas webhook:update`](#eas-webhookupdate)
|
||||
* [`eas webhook:view ID`](#eas-webhookview-id)
|
||||
|
||||
## `eas account:login`
|
||||
|
||||
log in with your Expo account
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas account:login
|
||||
|
||||
ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/account/login.js)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
log out
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas account:logout
|
||||
|
||||
ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/account/logout.js)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
show the username you are logged in as
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas account:view
|
||||
|
||||
ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/account/view.js)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
view or change analytics settings
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas analytics [STATUS]
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/analytics.js)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
start a build
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios|all)
|
||||
|
||||
--auto-submit Submit on build complete using the submit profile with the same name as the
|
||||
build profile
|
||||
|
||||
--auto-submit-with-profile=PROFILE_NAME Submit on build complete using the submit profile with provided name
|
||||
|
||||
--clear-cache Clear cache before the build
|
||||
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
|
||||
--local Run build locally [experimental]
|
||||
|
||||
--non-interactive Run command in non-interactive mode
|
||||
|
||||
--profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined
|
||||
in eas.json.
|
||||
|
||||
--skip-project-configuration Skip project configuration
|
||||
|
||||
--[no-]wait Wait for build(s) to complete
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/build/index.js)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
Cancel a build.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/build/cancel.js)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
Configure the project to support EAS Build.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:configure
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios|all) Platform to configure
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/build/configure.js)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
list all builds for your project
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:list
|
||||
|
||||
OPTIONS
|
||||
--appBuildVersion=appBuildVersion
|
||||
--appIdentifier=appIdentifier
|
||||
--appVersion=appVersion
|
||||
--buildProfile=buildProfile
|
||||
--channel=channel
|
||||
--distribution=(store|internal|simulator)
|
||||
--gitCommitHash=gitCommitHash
|
||||
|
||||
--json Enable JSON output, non-JSON messages will be printed
|
||||
to stderr
|
||||
|
||||
--limit=limit
|
||||
|
||||
--platform=(all|android|ios)
|
||||
|
||||
--runtimeVersion=runtimeVersion
|
||||
|
||||
--sdkVersion=sdkVersion
|
||||
|
||||
--status=(new|in-queue|in-progress|errored|finished|canceled)
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/build/list.js)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
view a build for your project
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:view [BUILD_ID]
|
||||
|
||||
OPTIONS
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/build/view.js)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
show the eas.json config
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas config
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios)
|
||||
--profile=profile
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/config.js)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
manage your credentials
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/credentials.js)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
register new Apple Devices to use for internal distribution
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/device/create.js)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
list all registered devices for your account
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas device:list
|
||||
|
||||
OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/device/list.js)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
view a device for your project
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/device/view.js)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
log environment info to the console
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas diagnostics
|
||||
```
|
||||
|
||||
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/diagnostics.js)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -51,4 +309,188 @@ OPTIONS
|
||||
```
|
||||
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.3/src/commands/help.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
information about the current project
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas project:info
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/project/info.js)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
create or link an EAS project
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas project:init
|
||||
|
||||
ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/project/init.js)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
Create an environment secret on the current project or owner account.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas secret:create
|
||||
|
||||
OPTIONS
|
||||
--force Delete and recreate existing secrets
|
||||
--name=name Name of the secret
|
||||
--scope=(account|project) [default: project] Scope for the secret
|
||||
--value=value Value of the secret
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/secret/create.js)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
Delete an environment secret by ID.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas secret:delete
|
||||
|
||||
OPTIONS
|
||||
--id=id ID of the secret to delete
|
||||
|
||||
DESCRIPTION
|
||||
Unsure where to find the secret's ID? Run eas secret:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/secret/delete.js)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
Lists environment secrets available for your current app
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas secret:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/secret/list.js)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
submit build archive to app store
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas submit
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios|all)
|
||||
--id=id ID of the build to submit
|
||||
--latest Submit the latest build for specified platform
|
||||
--non-interactive Run command in non-interactive mode
|
||||
--path=path Path to the .apk/.aab/.ipa file
|
||||
|
||||
--profile=profile Name of the submit profile from eas.json. Defaults to "production" if defined in
|
||||
eas.json.
|
||||
|
||||
--url=url App archive url
|
||||
|
||||
--verbose Always print logs from Submission Service
|
||||
|
||||
--[no-]wait Wait for submission to complete
|
||||
|
||||
DESCRIPTION
|
||||
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
|
||||
|
||||
ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/submit.js)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
Create a webhook on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas webhook:create
|
||||
|
||||
OPTIONS
|
||||
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
|
||||
|
||||
--secret=secret Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature'
|
||||
header.
|
||||
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/webhook/create.js)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
Delete a webhook on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas webhook:delete [ID]
|
||||
|
||||
ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/webhook/delete.js)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
List webhooks on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas webhook:list
|
||||
|
||||
OPTIONS
|
||||
--event=(BUILD) Event type that triggers the webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/webhook/list.js)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
Create a webhook on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas webhook:update
|
||||
|
||||
OPTIONS
|
||||
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
|
||||
--id=id (required) Webhook ID
|
||||
|
||||
--secret=secret Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature'
|
||||
header.
|
||||
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/webhook/update.js)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
View a webhook on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas webhook:view ID
|
||||
|
||||
ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/webhook/view.js)_
|
||||
<!-- commandsstop -->
|
||||
@@ -1,5 +1,5 @@
|
||||
overwrite: true
|
||||
schema: 'https://staging.exp.host/--/graphql'
|
||||
schema: 'https://staging-api.expo.dev/graphql'
|
||||
documents:
|
||||
- 'src/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
|
||||
Generated
+893
-442
File diff suppressed because it is too large.
Load diff
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.34.0",
|
||||
"version": "0.37.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "0.0.0-alpha.25",
|
||||
"@expo/apple-utils": "0.0.0-alpha.26",
|
||||
"@expo/config": "6.0.3",
|
||||
"@expo/config-plugins": "4.0.3",
|
||||
"@expo/eas-build-job": "0.2.57",
|
||||
"@expo/eas-json": "^0.34.0",
|
||||
"@expo/eas-json": "^0.37.0",
|
||||
"@expo/json-file": "8.2.33",
|
||||
"@expo/pkcs12": "0.0.4",
|
||||
"@expo/plist": "0.0.14",
|
||||
@@ -113,7 +113,6 @@
|
||||
"cli"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "build/index.js",
|
||||
"oclif": {
|
||||
"bin": "eas",
|
||||
"commands": "./build/commands",
|
||||
@@ -177,6 +176,7 @@
|
||||
"typecheck": "tsc",
|
||||
"test": "jest",
|
||||
"version": "yarn oclif-dev readme && node patch-readme && git add README.md",
|
||||
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml"
|
||||
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
|
||||
"clean": "rm -rf dist build node_modules yarn-error.log"
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { apiClient, getExpoApiBaseUrl } from '../api';
|
||||
describe('apiClient', () => {
|
||||
it('converts Expo APIv2 error to ApiV2Error', async () => {
|
||||
nock(getExpoApiBaseUrl())
|
||||
.post('/--/api/v2/test')
|
||||
.post('/v2/test')
|
||||
.reply(400, {
|
||||
errors: [
|
||||
{
|
||||
@@ -39,7 +39,7 @@ describe('apiClient', () => {
|
||||
});
|
||||
|
||||
it('does not convert non-APIv2 error to ApiV2Error', async () => {
|
||||
nock(getExpoApiBaseUrl()).post('/--/api/v2/test').reply(500, 'Something went wrong');
|
||||
nock(getExpoApiBaseUrl()).post('/v2/test').reply(500, 'Something went wrong');
|
||||
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -3,8 +3,8 @@ import os from 'os';
|
||||
import { URL } from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import UserSettings from './user/UserSettings';
|
||||
import { easCliVersion } from './utils/easCli';
|
||||
import UserSettings from '../user/UserSettings';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
|
||||
const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
|
||||
darwin: 'Mac',
|
||||
@@ -4,7 +4,7 @@ import ApiV2Error from './ApiV2Error';
|
||||
import { getAccessToken, getSessionSecret } from './user/sessionStorage';
|
||||
|
||||
export const apiClient = got.extend({
|
||||
prefixUrl: getExpoApiBaseUrl() + '/--/api/v2/',
|
||||
prefixUrl: getExpoApiBaseUrl() + '/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.exp.host`;
|
||||
return `https://staging-api.expo.dev`;
|
||||
} else if (process.env.EXPO_LOCAL) {
|
||||
return `http://127.0.0.1:3000`;
|
||||
} else {
|
||||
return `https://exp.host`;
|
||||
return `https://api.expo.dev`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ 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';
|
||||
@@ -17,8 +19,6 @@ import { getVcsClient } from '../vcs';
|
||||
import { BuildContext } from './context';
|
||||
import { runLocalBuildAsync } from './local';
|
||||
import { MetadataContext, collectMetadataAsync } from './metadata';
|
||||
import { TrackingContext } from './types';
|
||||
import Analytics, { Event } from './utils/analytics';
|
||||
import { printDeprecationWarnings } from './utils/printBuildInfo';
|
||||
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
||||
|
||||
@@ -55,15 +55,17 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
const credentialsResult = await withAnalyticsAsync(
|
||||
async () => await builder.ensureCredentialsAsync(ctx),
|
||||
{
|
||||
successEvent: Event.GATHER_CREDENTIALS_SUCCESS,
|
||||
failureEvent: Event.GATHER_CREDENTIALS_FAIL,
|
||||
attemptEvent: BuildEvent.GATHER_CREDENTIALS_ATTEMPT,
|
||||
successEvent: BuildEvent.GATHER_CREDENTIALS_SUCCESS,
|
||||
failureEvent: BuildEvent.GATHER_CREDENTIALS_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
if (!ctx.skipProjectConfiguration) {
|
||||
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(ctx), {
|
||||
successEvent: Event.CONFIGURE_PROJECT_SUCCESS,
|
||||
failureEvent: Event.CONFIGURE_PROJECT_FAIL,
|
||||
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
|
||||
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
|
||||
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
});
|
||||
}
|
||||
@@ -166,8 +168,9 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
return bucketKey;
|
||||
},
|
||||
{
|
||||
successEvent: Event.PROJECT_UPLOAD_SUCCESS,
|
||||
failureEvent: Event.PROJECT_UPLOAD_FAIL,
|
||||
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
|
||||
successEvent: BuildEvent.PROJECT_UPLOAD_SUCCESS,
|
||||
failureEvent: BuildEvent.PROJECT_UPLOAD_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
@@ -200,34 +203,14 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
|
||||
return build;
|
||||
},
|
||||
{
|
||||
successEvent: Event.BUILD_REQUEST_SUCCESS,
|
||||
failureEvent: Event.BUILD_REQUEST_FAIL,
|
||||
attemptEvent: BuildEvent.BUILD_REQUEST_ATTEMPT,
|
||||
successEvent: BuildEvent.BUILD_REQUEST_SUCCESS,
|
||||
failureEvent: BuildEvent.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 } = {}
|
||||
|
||||
@@ -5,6 +5,8 @@ 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';
|
||||
@@ -13,8 +15,6 @@ 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(Event.BUILD_COMMAND, trackingCtx);
|
||||
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
|
||||
|
||||
return {
|
||||
accountName,
|
||||
|
||||
@@ -4,9 +4,9 @@ import { IOSConfig } from '@expo/config-plugins';
|
||||
import Log from '../../log';
|
||||
import { getProjectAccountName } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { ensureValidVersions } from '../utils/updates';
|
||||
import { readPlistAsync, writePlistAsync } from './plist';
|
||||
|
||||
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
@@ -59,7 +59,7 @@ async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
|
||||
|
||||
async function readExpoPlistAsync(projectDir: string): Promise<IOSConfig.ExpoPlist> {
|
||||
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
|
||||
return (await readPlistAsync(expoPlistPath)) as IOSConfig.ExpoPlist;
|
||||
return ((await readPlistAsync(expoPlistPath)) ?? {}) as IOSConfig.ExpoPlist;
|
||||
}
|
||||
|
||||
async function writeExpoPlistAsync(
|
||||
|
||||
@@ -4,7 +4,7 @@ import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { readPlistAsync } from '../plist';
|
||||
import { readPlistAsync } from '../../../utils/plist';
|
||||
import {
|
||||
BumpStrategy,
|
||||
bumpVersionAsync,
|
||||
|
||||
@@ -20,7 +20,11 @@ export async function ensureIosCredentialsAsync(
|
||||
app: getAppFromContext(buildCtx.credentialsCtx),
|
||||
targets,
|
||||
iosCapabilitiesOptions: {
|
||||
entitlements: await resolveEntitlementsJsonAsync(buildCtx.projectDir, buildCtx.workflow),
|
||||
entitlements: await resolveEntitlementsJsonAsync(
|
||||
buildCtx.projectDir,
|
||||
buildCtx.workflow,
|
||||
buildCtx.buildProfile.env ?? {}
|
||||
),
|
||||
},
|
||||
distribution: buildCtx.buildProfile.distribution ?? 'store',
|
||||
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
|
||||
|
||||
@@ -8,9 +8,9 @@ import type { XCBuildConfiguration } from 'xcode';
|
||||
import Log from '../../log';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
|
||||
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
||||
import { bumpAppVersionAsync } from '../utils/version';
|
||||
import { readPlistAsync, writePlistAsync } from './plist';
|
||||
|
||||
export enum BumpStrategy {
|
||||
APP_VERSION,
|
||||
@@ -128,7 +128,10 @@ export async function maybeResolveVersionsAsync(
|
||||
appBuildVersion: await readBuildNumberAsync(projectDir, exp, buildSettings),
|
||||
appVersion: await readShortVersionAsync(projectDir, exp, buildSettings),
|
||||
};
|
||||
} catch {
|
||||
} catch (err: any) {
|
||||
Log.warn('Failed to read app versions.');
|
||||
Log.warn(err.message);
|
||||
Log.warn('Proceeding anyway...');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -174,7 +177,7 @@ async function readInfoPlistAsync(
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): Promise<IOSConfig.InfoPlist> {
|
||||
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
|
||||
return (await readPlistAsync(infoPlistPath)) as IOSConfig.InfoPlist;
|
||||
return ((await readPlistAsync(infoPlistPath)) ?? {}) as IOSConfig.InfoPlist;
|
||||
}
|
||||
|
||||
async function writeInfoPlistAsync({
|
||||
|
||||
@@ -1,45 +1,27 @@
|
||||
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.50';
|
||||
|
||||
export async function runLocalBuildAsync(job: Job): Promise<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
const { command, args } = getCommandAndArgs(job);
|
||||
await spawnAsync(command, args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
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],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,5 +12,3 @@ export enum BuildDistributionType {
|
||||
INTERNAL = 'internal',
|
||||
SIMULATOR = 'simulator',
|
||||
}
|
||||
|
||||
export type TrackingContext = Record<string, string | number | boolean>;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { logEvent } from '../../analytics';
|
||||
|
||||
export enum Event {
|
||||
BUILD_COMMAND = 'build cli build command',
|
||||
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
|
||||
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
|
||||
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
|
||||
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
|
||||
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
|
||||
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
|
||||
BUILD_REQUEST_SUCCESS = 'build cli build request success',
|
||||
BUILD_REQUEST_FAIL = 'build cli build request fail',
|
||||
|
||||
BUILD_STATUS_COMMAND = 'build cli build status',
|
||||
|
||||
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
|
||||
|
||||
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
|
||||
}
|
||||
|
||||
export default {
|
||||
logEvent(name: Event, properties: Record<string, any>) {
|
||||
logEvent(name, properties);
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import { error } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
import resolveFrom from 'resolve-from';
|
||||
@@ -11,6 +10,7 @@ import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import { expoCommandAsync } from '../../utils/expoCli';
|
||||
import { ProfileData } from '../../utils/profiles';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { reviewAndCommitChangesAsync } from './repository';
|
||||
|
||||
export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
@@ -20,7 +20,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
}: {
|
||||
projectDir: string;
|
||||
nonInteractive?: boolean;
|
||||
buildProfiles?: ProfileData<BuildProfile>[];
|
||||
buildProfiles?: ProfileData<'build'>[];
|
||||
}): Promise<void> {
|
||||
if (await isExpoDevClientInstalledAsync(projectDir)) {
|
||||
return;
|
||||
@@ -107,7 +107,9 @@ async function installExpoDevClientAsync(
|
||||
Log.newLine();
|
||||
await expoCommandAsync(projectDir, ['install', 'expo-dev-client']);
|
||||
Log.newLine();
|
||||
await reviewAndCommitChangesAsync('Install expo-dev-client', {
|
||||
nonInteractive,
|
||||
});
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
await reviewAndCommitChangesAsync('Install expo-dev-client', {
|
||||
nonInteractive,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -120,11 +120,5 @@ export function formatGraphQLBuild(build: BuildFragment): string {
|
||||
}
|
||||
|
||||
const getActorName = (build: BuildFragment): string => {
|
||||
if (!build.initiatingActor) {
|
||||
return 'unknown';
|
||||
} else if (build.initiatingActor.__typename === 'User') {
|
||||
return build.initiatingActor.username;
|
||||
} else {
|
||||
return build.initiatingActor.firstName ?? 'unknown';
|
||||
}
|
||||
return build.initiatingActor?.displayName || 'unknown';
|
||||
};
|
||||
@@ -136,6 +136,7 @@ export async function reviewAndCommitChangesAsync(
|
||||
await getVcsClient().commitAsync({
|
||||
commitMessage: initialCommitMessage,
|
||||
commitAllFiles: false,
|
||||
nonInteractive,
|
||||
});
|
||||
Log.withTick('Committed changes.');
|
||||
return;
|
||||
|
||||
@@ -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()}/--/api/v2/projects/${
|
||||
return `itms-services://?action=download-manifest;url=${getExpoApiBaseUrl()}/v2/projects/${
|
||||
build.project.id
|
||||
}/builds/${build.id}/manifest.plist`;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
flushAsync as flushAnalyticsAsync,
|
||||
initAsync as initAnalyticsAsync,
|
||||
logEvent,
|
||||
} from '../analytics';
|
||||
} from '../analytics/rudderstackClient';
|
||||
import Log from '../log';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { getUserAsync } from '../user/User';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { asMock } from '../../__tests__/utils';
|
||||
import { flushAsync, initAsync, logEvent } from '../../analytics';
|
||||
import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClient';
|
||||
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', () => {
|
||||
const { AnalyticsEvent } = jest.requireActual('../../analytics');
|
||||
jest.mock('../../analytics/rudderstackClient', () => {
|
||||
const { AnalyticsEvent } = jest.requireActual('../../analytics/rudderstackClient');
|
||||
return {
|
||||
AnalyticsEvent,
|
||||
logEvent: jest.fn(),
|
||||
|
||||
@@ -35,6 +35,7 @@ import { formatUpdate } from '../../update/utils';
|
||||
import uniqBy from '../../utils/expodash/uniqBy';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { createUpdateChannelOnAppAsync } from '../channel/create';
|
||||
import { createUpdateBranchOnAppAsync } from './create';
|
||||
import { listBranchesAsync } from './list';
|
||||
import { viewUpdateBranchAsync } from './view';
|
||||
@@ -70,6 +71,34 @@ async function getUpdateGroupAsync({
|
||||
return updatesByGroup;
|
||||
}
|
||||
|
||||
async function ensureChannelExistsAsync({
|
||||
appId,
|
||||
branchId,
|
||||
channelName,
|
||||
}: {
|
||||
appId: string;
|
||||
branchId: string;
|
||||
channelName: string;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
await createUpdateChannelOnAppAsync({
|
||||
appId,
|
||||
channelName,
|
||||
branchId,
|
||||
});
|
||||
Log.withTick(
|
||||
`Created a channel: ${chalk.bold(channelName)} pointed at branch: ${chalk.bold(channelName)}.`
|
||||
);
|
||||
} catch (e: any) {
|
||||
const isIgnorableError =
|
||||
e.graphQLErrors?.length === 1 &&
|
||||
e.graphQLErrors[0].extensions.errorCode === 'CHANNEL_ALREADY_EXISTS';
|
||||
if (!isIgnorableError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureBranchExistsAsync({
|
||||
appId,
|
||||
name: branchName,
|
||||
@@ -90,10 +119,13 @@ async function ensureBranchExistsAsync({
|
||||
const updateBranch = app?.byId.updateBranchByName;
|
||||
if (updateBranch) {
|
||||
const { id, updates } = updateBranch;
|
||||
await ensureChannelExistsAsync({ appId, branchId: id, channelName: branchName });
|
||||
return { id, updates };
|
||||
}
|
||||
|
||||
const newUpdateBranch = await createUpdateBranchOnAppAsync({ appId, name: branchName });
|
||||
Log.withTick(`Created branch: ${chalk.bold(branchName)}`);
|
||||
await ensureChannelExistsAsync({ appId, branchId: newUpdateBranch.id, channelName: branchName });
|
||||
return { id: newUpdateBranch.id, updates: [] };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile, EasJsonReader } from '@expo/eas-json';
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { flags } from '@oclif/command';
|
||||
import { error } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
@@ -14,7 +14,7 @@ import { BuildContext, createBuildContextAsync } from '../../build/context';
|
||||
import { prepareIosBuildAsync } from '../../build/ios/build';
|
||||
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from '../../build/utils/devClient';
|
||||
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
|
||||
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
|
||||
import { ensureRepoIsCleanAsync, reviewAndCommitChangesAsync } from '../../build/utils/repository';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import {
|
||||
@@ -32,10 +32,6 @@ import {
|
||||
selectRequestedPlatformAsync,
|
||||
toPlatforms,
|
||||
} from '../../platform';
|
||||
import {
|
||||
EAS_UNAVAILABLE_MESSAGE,
|
||||
isEasEnabledForProjectAsync,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { selectAsync } from '../../prompts';
|
||||
@@ -152,13 +148,11 @@ export default class Build extends EasCommand {
|
||||
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
|
||||
|
||||
const platforms = toPlatforms(requestedPlatform);
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const buildProfiles = await getProfilesAsync({
|
||||
type: 'build',
|
||||
projectDir,
|
||||
platforms,
|
||||
profileName: flags.profile,
|
||||
async readProfileAsync(platform, profileName) {
|
||||
return await easJsonReader.readBuildProfileAsync(platform, profileName);
|
||||
},
|
||||
});
|
||||
|
||||
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
@@ -169,7 +163,7 @@ export default class Build extends EasCommand {
|
||||
|
||||
const startedBuilds: {
|
||||
build: BuildFragment;
|
||||
buildProfile: ProfileData<BuildProfile>;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
}[] = [];
|
||||
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
|
||||
|
||||
@@ -274,7 +268,7 @@ export default class Build extends EasCommand {
|
||||
wait: flags['wait'],
|
||||
clearCache: flags['clear-cache'],
|
||||
json: flags['json'],
|
||||
autoSubmit: flags['auto-submit'],
|
||||
autoSubmit: flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined,
|
||||
submitProfile: flags['auto-submit-with-profile'] ?? profile,
|
||||
};
|
||||
}
|
||||
@@ -288,7 +282,7 @@ export default class Build extends EasCommand {
|
||||
projectDir: string;
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
buildProfile: ProfileData<BuildProfile>;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
|
||||
const buildCtx = await createBuildContextAsync({
|
||||
buildProfileName: buildProfile.profileName,
|
||||
@@ -316,10 +310,6 @@ export default class Build extends EasCommand {
|
||||
this.metroConfigValidated = true;
|
||||
}
|
||||
|
||||
if (!buildCtx.local && !(await isEasEnabledForProjectAsync(buildCtx.projectId))) {
|
||||
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
|
||||
}
|
||||
|
||||
const build = await this.startBuildAsync(buildCtx);
|
||||
return {
|
||||
build,
|
||||
@@ -350,7 +340,7 @@ export default class Build extends EasCommand {
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
projectDir: string;
|
||||
buildProfile: ProfileData<BuildProfile>;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
}): Promise<SubmissionFragment> {
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const platform = toPlatform(build.platform);
|
||||
@@ -392,7 +382,8 @@ export async function handleDeprecatedEasJsonAsync(
|
||||
projectDir: string,
|
||||
nonInteractive: boolean
|
||||
): Promise<void> {
|
||||
if (!(await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir)))) {
|
||||
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
|
||||
if (!(await fs.pathExists(easJsonPath))) {
|
||||
return;
|
||||
}
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
@@ -423,7 +414,7 @@ export async function handleDeprecatedEasJsonAsync(
|
||||
Log.log(
|
||||
`If you want to continue using the Git integration, you can opt in with ${chalk.bold(
|
||||
'cli.requireCommit'
|
||||
)} in ${chalk.bold('eas.json')} or with the following prompt`
|
||||
)} in ${chalk.bold('eas.json')} or with the following prompt.`
|
||||
);
|
||||
Log.newLine();
|
||||
|
||||
@@ -432,12 +423,21 @@ export async function handleDeprecatedEasJsonAsync(
|
||||
{ title: 'Allow builds with dirty Git working tree (new default)', value: 'noCommit' },
|
||||
]);
|
||||
|
||||
if (mode === 'requireCommit') {
|
||||
setVcsClient(new GitClient());
|
||||
await ensureRepoIsCleanAsync(nonInteractive);
|
||||
}
|
||||
|
||||
rawEasJson.cli =
|
||||
mode === 'requireCommit'
|
||||
? { version: `>= ${easCliVersion}`, requireCommit: true }
|
||||
: { version: `>= ${easCliVersion}` };
|
||||
await fs.writeJSON(EasJsonReader.formatEasJsonPath(projectDir), rawEasJson, { spaces: 2 });
|
||||
await fs.writeJSON(easJsonPath, rawEasJson, { spaces: 2 });
|
||||
Log.withTick('Updated eas.json');
|
||||
if (mode === 'requireCommit') {
|
||||
setVcsClient(new GitClient());
|
||||
await getVcsClient().trackFileAsync(easJsonPath);
|
||||
await reviewAndCommitChangesAsync('Set cli.requireCommit to true in eas.json', {
|
||||
nonInteractive,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import { promptAsync } from '../../prompts';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { createUpdateBranchOnAppAsync } from '../branch/create';
|
||||
|
||||
async function createUpdateChannelOnAppAsync({
|
||||
export async function createUpdateChannelOnAppAsync({
|
||||
appId,
|
||||
channelName,
|
||||
branchId,
|
||||
|
||||
@@ -9,10 +9,6 @@ import {
|
||||
EnvironmentSecretsQuery,
|
||||
} from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
@@ -57,12 +53,6 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
const { slug } = exp;
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!scope) {
|
||||
const validationMessage = 'Secret scope may not be empty.';
|
||||
|
||||
|
||||
@@ -10,10 +10,6 @@ import {
|
||||
EnvironmentSecretsQuery,
|
||||
} from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
@@ -23,7 +19,7 @@ import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
|
||||
export default class EnvironmentSecretDelete extends EasCommand {
|
||||
static description = `Delete an environment secret by ID.
|
||||
Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
Unsure where to find the secret's ID? Run ${chalk.bold('eas secret:list')}`;
|
||||
|
||||
static flags = {
|
||||
id: flags.string({
|
||||
@@ -37,12 +33,6 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectAccountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
let {
|
||||
flags: { id },
|
||||
} = this.parse(EnvironmentSecretDelete);
|
||||
|
||||
@@ -6,10 +6,6 @@ import dateFormat from 'dateformat';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EnvironmentSecretsQuery } from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
@@ -25,12 +21,6 @@ export default class EnvironmentSecretList extends EasCommand {
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectAccountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!projectDir) {
|
||||
throw new Error("Please run this command inside your project's directory");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { flags } from '@oclif/command';
|
||||
import { error } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
@@ -15,7 +14,6 @@ import {
|
||||
selectRequestedPlatformAsync,
|
||||
toPlatforms,
|
||||
} from '../platform';
|
||||
import { isEasEnabledForProjectAsync, warnEasUnavailable } from '../project/isEasEnabledForProject';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { SubmitArchiveFlags, createSubmissionContextAsync } from '../submit/context';
|
||||
import { submitAsync, waitToCompleteAsync } from '../submit/submit';
|
||||
@@ -96,20 +94,12 @@ See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const platforms = toPlatforms(flags.requestedPlatform);
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const submissionProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
projectDir,
|
||||
platforms,
|
||||
profileName: flags.profile,
|
||||
async readProfileAsync(platform, profileName) {
|
||||
return await easJsonReader.readSubmitProfileAsync(platform, profileName);
|
||||
},
|
||||
});
|
||||
|
||||
const submissions: SubmissionFragment[] = [];
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AuthCtx } from '../ios/appstore/authenticate';
|
||||
export const testAuthCtx: AuthCtx = {
|
||||
appleId: 'test-apple-id',
|
||||
appleIdPassword: 'test-apple-password',
|
||||
team: { id: 'test-team-id', name: 'test-team-name', inHouse: false },
|
||||
team: { id: 'test-apple-team-identifier', name: 'test-team-name', inHouse: false },
|
||||
};
|
||||
|
||||
export function getAppstoreMock(): AppStoreApi {
|
||||
@@ -22,5 +22,9 @@ export function getAppstoreMock(): AppStoreApi {
|
||||
createProvisioningProfileAsync: jest.fn(),
|
||||
revokeProvisioningProfileAsync: jest.fn(),
|
||||
createOrReuseAdhocProvisioningProfileAsync: jest.fn(),
|
||||
createAscApiKeyAsync: jest.fn(),
|
||||
getAscApiKeyAsync: jest.fn(),
|
||||
revokeAscApiKeyAsync: jest.fn(),
|
||||
listAscApiKeysAsync: jest.fn(),
|
||||
} as any;
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import { UserRole } from '@expo/apple-utils';
|
||||
|
||||
import {
|
||||
AppFragment,
|
||||
AppStoreConnectApiKeyFragment,
|
||||
AppStoreConnectUserRole,
|
||||
AppleAppIdentifierFragment,
|
||||
AppleDistributionCertificateFragment,
|
||||
AppleProvisioningProfileFragment,
|
||||
@@ -10,12 +14,27 @@ import {
|
||||
IosDistributionType,
|
||||
} from '../../graphql/generated';
|
||||
import * as IosGraphqlClient from '../ios/api/GraphqlClient';
|
||||
import { DistributionCertificate, ProvisioningProfile } from '../ios/appstore/Credentials.types';
|
||||
import {
|
||||
AscApiKey,
|
||||
DistributionCertificate,
|
||||
ProvisioningProfile,
|
||||
} from '../ios/appstore/Credentials.types';
|
||||
import { Target } from '../ios/types';
|
||||
import { testProvisioningProfileBase64 } from './fixtures-base64-data';
|
||||
|
||||
const now = new Date();
|
||||
|
||||
export const testAscApiKey: AscApiKey = {
|
||||
keyId: 'test-keyIdentifier',
|
||||
issuerId: 'test-issuer-id-from-apple',
|
||||
teamId: 'test-team-id',
|
||||
teamName: 'test-team-name',
|
||||
name: 'test-name',
|
||||
roles: [UserRole.ADMIN],
|
||||
isRevoked: false,
|
||||
keyP8: 'test-key-p8',
|
||||
};
|
||||
|
||||
export const testProvisioningProfile: ProvisioningProfile = {
|
||||
provisioningProfileId: 'test-id',
|
||||
provisioningProfile: testProvisioningProfileBase64,
|
||||
@@ -38,6 +57,17 @@ export const testAppFragment: AppFragment = {
|
||||
slug: 'testapp',
|
||||
};
|
||||
|
||||
export const testAscApiKeyFragment: AppStoreConnectApiKeyFragment = {
|
||||
id: 'test-asc-api-key-id',
|
||||
appleTeam: { ...testAppleTeamFragment },
|
||||
issuerIdentifier: 'test-issuerIdentifier',
|
||||
keyIdentifier: 'test-keyIdentifier',
|
||||
name: 'test-name',
|
||||
roles: [AppStoreConnectUserRole.Admin],
|
||||
updatedAt: now,
|
||||
createdAt: now,
|
||||
};
|
||||
|
||||
export const testPushKey: ApplePushKeyFragment = {
|
||||
id: 'test-push-key-id',
|
||||
keyIdentifier: 'test-key-identifier',
|
||||
@@ -112,6 +142,7 @@ export const testCommonIosAppCredentialsFragment: CommonIosAppCredentialsFragmen
|
||||
appleAppIdentifier: testAppleAppIdentifierFragment,
|
||||
pushKey: testPushKey,
|
||||
iosAppBuildCredentialsList: [testIosAppBuildCredentialsFragment],
|
||||
appStoreConnectApiKeyForSubmissions: testAscApiKeyFragment,
|
||||
};
|
||||
|
||||
export const testAppleTeam = {
|
||||
@@ -139,6 +170,9 @@ export function getNewIosApiMock(): { [key in keyof typeof IosGraphqlClient]?: a
|
||||
createPushKeyAsync: jest.fn(),
|
||||
getPushKeyForAppAsync: jest.fn(),
|
||||
deletePushKeyAsync: jest.fn(),
|
||||
createAscApiKeyAsync: jest.fn(),
|
||||
deleteAscApiKeyAsync: jest.fn(),
|
||||
getAscApiKeysForAccountAsync: jest.fn(() => []),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import nullthrows from 'nullthrows';
|
||||
import { AndroidAppBuildCredentialsFragment } from '../../graphql/generated';
|
||||
import { CredentialsContext } from '../context';
|
||||
import * as credentialsJsonReader from '../credentialsJson/read';
|
||||
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
|
||||
import { SetUpBuildCredentials } from './actions/SetUpBuildCredentials';
|
||||
import { AppLookupParams } from './api/GraphqlClient';
|
||||
import { Keystore } from './credentials';
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class AndroidCredentialsProvider {
|
||||
}
|
||||
|
||||
private async getRemoteAsync(): Promise<AndroidCredentials> {
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: this.options.app });
|
||||
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: this.options.app });
|
||||
const buildCredentials = await setupBuildCredentialsAction.runAsync(this.ctx);
|
||||
return this.toAndroidCredentials(buildCredentials);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -25,7 +25,7 @@ interface Options {
|
||||
* Sets up Build Credentials for Android
|
||||
* @name: sets up build credentials for the specified configuration. If no name is specified, the default configuration is setup
|
||||
*/
|
||||
export class SetupBuildCredentials {
|
||||
export class SetUpBuildCredentials {
|
||||
constructor(private options: Options) {}
|
||||
|
||||
async runAsync(ctx: CredentialsContext): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
+1
-1
@@ -14,7 +14,7 @@ import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { getKeystoreWithType } from '../utils/keystoreNew';
|
||||
import { BackupKeystore } from './DownloadKeystore';
|
||||
|
||||
export class SetupBuildCredentialsFromCredentialsJson {
|
||||
export class SetUpBuildCredentialsFromCredentialsJson {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
async runAsync(ctx: CredentialsContext): Promise<AndroidAppBuildCredentialsFragment | null> {
|
||||
+1
-1
@@ -13,7 +13,7 @@ import { AssignGoogleServiceAccountKey } from './AssignGoogleServiceAccountKey';
|
||||
import { CreateGoogleServiceAccountKey } from './CreateGoogleServiceAccountKey';
|
||||
import { UseExistingGoogleServiceAccountKey } from './UseExistingGoogleServiceAccountKey';
|
||||
|
||||
export class SetupGoogleServiceAccountKey {
|
||||
export class SetUpGoogleServiceAccountKey {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
public async runAsync(ctx: CredentialsContext): Promise<CommonAndroidAppCredentialsFragment> {
|
||||
@@ -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: {
|
||||
|
||||
+5
-5
@@ -9,7 +9,7 @@ import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../../errors';
|
||||
import { generateRandomKeystoreAsync } from '../../utils/keystore';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { SetupBuildCredentials } from '../SetupBuildCredentials';
|
||||
import { SetUpBuildCredentials } from '../SetUpBuildCredentials';
|
||||
|
||||
jest.mock('../../../../ora');
|
||||
jest.mock('../../../../project/ensureProjectExists');
|
||||
@@ -17,7 +17,7 @@ jest.mock('../../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() =
|
||||
jest.mock('../../../../prompts', () => ({ confirmAsync: jest.fn(() => true) }));
|
||||
jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn() }));
|
||||
|
||||
describe('SetupBuildCredentials', () => {
|
||||
describe('SetUpBuildCredentials', () => {
|
||||
beforeEach(() => {
|
||||
asMock(generateRandomKeystoreAsync).mockReset();
|
||||
});
|
||||
@@ -33,7 +33,7 @@ describe('SetupBuildCredentials', () => {
|
||||
},
|
||||
});
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
|
||||
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
|
||||
// expect keystore not to be created
|
||||
@@ -48,7 +48,7 @@ describe('SetupBuildCredentials', () => {
|
||||
},
|
||||
});
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
|
||||
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
|
||||
// expect keystore to be created
|
||||
@@ -63,7 +63,7 @@ describe('SetupBuildCredentials', () => {
|
||||
},
|
||||
});
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
|
||||
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
|
||||
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError(
|
||||
MissingCredentialsNonInteractiveError
|
||||
);
|
||||
+5
-5
@@ -14,7 +14,7 @@ import {
|
||||
SelectAndroidBuildCredentialsResultType,
|
||||
} from '../../../manager/SelectAndroidBuildCredentials';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { SetupBuildCredentialsFromCredentialsJson } from '../SetupBuildCredentialsFromCredentialsJson';
|
||||
import { SetUpBuildCredentialsFromCredentialsJson } from '../SetUpBuildCredentialsFromCredentialsJson';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
@@ -26,7 +26,7 @@ beforeEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
describe(SetupBuildCredentialsFromCredentialsJson, () => {
|
||||
describe(SetUpBuildCredentialsFromCredentialsJson, () => {
|
||||
it('sets up a new build configuration from credentials.json upon user request', async () => {
|
||||
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
|
||||
return {
|
||||
@@ -59,7 +59,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
|
||||
'keystore.jks': 'some-binary-content',
|
||||
});
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
|
||||
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams
|
||||
);
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
@@ -109,7 +109,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
|
||||
'keystore.jks': 'some-binary-content',
|
||||
});
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
|
||||
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams
|
||||
);
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
@@ -132,7 +132,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
|
||||
nonInteractive: true,
|
||||
});
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
|
||||
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams
|
||||
);
|
||||
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError();
|
||||
+5
-5
@@ -10,7 +10,7 @@ import {
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../../errors';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { SetupGoogleServiceAccountKey } from '../SetupGoogleServiceAccountKey';
|
||||
import { SetUpGoogleServiceAccountKey } from '../SetUpGoogleServiceAccountKey';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
jest.mock('fs');
|
||||
@@ -22,7 +22,7 @@ beforeEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
describe(SetupGoogleServiceAccountKey, () => {
|
||||
describe(SetUpGoogleServiceAccountKey, () => {
|
||||
it('skips setup when there is a Google Service Account Key already assigned to the project', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
@@ -34,7 +34,7 @@ describe(SetupGoogleServiceAccountKey, () => {
|
||||
},
|
||||
});
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
|
||||
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
|
||||
await setupGoogleServiceAccountKeyAction.runAsync(ctx);
|
||||
|
||||
expect(ctx.android.createGoogleServiceAccountKeyAsync).not.toHaveBeenCalled();
|
||||
@@ -57,7 +57,7 @@ describe(SetupGoogleServiceAccountKey, () => {
|
||||
},
|
||||
});
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
|
||||
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
|
||||
await setupGoogleServiceAccountKeyAction.runAsync(ctx);
|
||||
|
||||
expect(ctx.android.createGoogleServiceAccountKeyAsync).toHaveBeenCalledTimes(1);
|
||||
@@ -68,7 +68,7 @@ describe(SetupGoogleServiceAccountKey, () => {
|
||||
nonInteractive: true,
|
||||
});
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
|
||||
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
|
||||
await expect(setupGoogleServiceAccountKeyAction.runAsync(ctx)).rejects.toThrowError(
|
||||
MissingCredentialsNonInteractiveError
|
||||
);
|
||||
@@ -1,31 +1,35 @@
|
||||
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, { Event } from '../../../build/utils/analytics';
|
||||
import { Analytics, BuildEvent } from '../../../analytics/events';
|
||||
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 {
|
||||
@@ -35,7 +39,7 @@ enum KeystoreCreateStep {
|
||||
}
|
||||
|
||||
export async function generateRandomKeystoreAsync(projectId: string): Promise<KeystoreWithType> {
|
||||
const keystoreData = {
|
||||
const keystoreData: KeystoreParams = {
|
||||
keystorePassword: crypto.randomBytes(16).toString('hex'),
|
||||
keyPassword: crypto.randomBytes(16).toString('hex'),
|
||||
keyAlias: crypto.randomBytes(16).toString('hex'),
|
||||
@@ -44,23 +48,38 @@ export async function generateRandomKeystoreAsync(projectId: string): Promise<Ke
|
||||
}
|
||||
|
||||
async function createKeystoreAsync(
|
||||
credentials: {
|
||||
keystorePassword: string;
|
||||
keyAlias: string;
|
||||
keyPassword: string;
|
||||
},
|
||||
keystoreParams: KeystoreParams,
|
||||
projectId: string
|
||||
): Promise<KeystoreWithType> {
|
||||
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
|
||||
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
|
||||
project_id: projectId,
|
||||
step: KeystoreCreateStep.Attempt,
|
||||
type: AndroidKeystoreType.Jks,
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureKeytoolCommandExistsAsync();
|
||||
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;
|
||||
} catch (error: any) {
|
||||
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
|
||||
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
|
||||
project_id: projectId,
|
||||
step: KeystoreCreateStep.Fail,
|
||||
reason: error.message,
|
||||
@@ -68,7 +87,11 @@ 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 {
|
||||
@@ -78,13 +101,13 @@ async function createKeystoreAsync(
|
||||
'-storetype',
|
||||
'JKS',
|
||||
'-storepass',
|
||||
credentials.keystorePassword,
|
||||
keystoreParams.keystorePassword,
|
||||
'-keypass',
|
||||
credentials.keyPassword,
|
||||
keystoreParams.keyPassword,
|
||||
'-keystore',
|
||||
keystorePath,
|
||||
'-alias',
|
||||
credentials.keyAlias,
|
||||
keystoreParams.keyAlias,
|
||||
'-keyalg',
|
||||
'RSA',
|
||||
'-keysize',
|
||||
@@ -94,27 +117,49 @@ async function createKeystoreAsync(
|
||||
'-dname',
|
||||
`CN=,OU=,O=,L=,S=,C=US`,
|
||||
]);
|
||||
|
||||
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
|
||||
project_id: projectId,
|
||||
step: KeystoreCreateStep.Success,
|
||||
type: AndroidKeystoreType.Jks,
|
||||
});
|
||||
|
||||
return {
|
||||
...credentials,
|
||||
...keystoreParams,
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -11,3 +11,9 @@ export class MissingCredentialsError extends Error {
|
||||
super(message ?? 'Credentials are not set up.');
|
||||
}
|
||||
}
|
||||
|
||||
export class UnsupportedCredentialsChoiceError extends Error {
|
||||
constructor(message: string, public choice: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import { CredentialsContext } from '../context';
|
||||
import * as credentialsJsonReader from '../credentialsJson/read';
|
||||
import { ensureAllTargetsAreConfigured } from '../credentialsJson/utils';
|
||||
import { getAppFromContext } from './actions/BuildCredentialsUtils';
|
||||
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
|
||||
import { SetupPushKey } from './actions/SetupPushKey';
|
||||
import { SetUpBuildCredentials } from './actions/SetUpBuildCredentials';
|
||||
import { SetUpPushKey } from './actions/SetUpPushKey';
|
||||
import { IosCapabilitiesOptions } from './appstore/ensureAppExists';
|
||||
import { App, IosCredentials, Target } from './types';
|
||||
import { isAdHocProfile } from './utils/provisioningProfile';
|
||||
@@ -58,7 +58,7 @@ export default class IosCredentialsProvider {
|
||||
}
|
||||
|
||||
private async getRemoteAsync(): Promise<IosCredentials> {
|
||||
return await new SetupBuildCredentials({
|
||||
return await new SetUpBuildCredentials({
|
||||
app: this.options.app,
|
||||
targets: this.options.targets,
|
||||
distribution: this.options.distribution,
|
||||
@@ -83,7 +83,7 @@ export default class IosCredentialsProvider {
|
||||
parentBundleIdentifier: applicationTarget.parentBundleIdentifier,
|
||||
};
|
||||
|
||||
const setupPushKeyAction = new SetupPushKey(appLookupParams);
|
||||
const setupPushKeyAction = new SetUpPushKey(appLookupParams);
|
||||
const isPushKeySetup = await setupPushKeyAction.isPushKeySetupAsync(ctx);
|
||||
if (isPushKeySetup) {
|
||||
Log.succeed(
|
||||
|
||||
@@ -1,15 +1,123 @@
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import path from 'path';
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
|
||||
import Log, { learnMore } from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { getCredentialsFromUserAsync } from '../../utils/promptForCredentials';
|
||||
import { confirmAsync, promptAsync } from '../../../prompts';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { fromNow } from '../../../utils/date';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import {
|
||||
getCredentialsFromUserAsync,
|
||||
shouldAutoGenerateCredentialsAsync,
|
||||
} from '../../utils/promptForCredentials';
|
||||
import { AscApiKey } from '../appstore/Credentials.types';
|
||||
import { AscApiKeyPath, MinimalAscApiKey, ascApiKeyMetadataSchema } from '../credentials';
|
||||
import {
|
||||
AscApiKeyPath,
|
||||
MinimalAscApiKey,
|
||||
ascApiKeyIdSchema,
|
||||
ascApiKeyIssuerIdSchema,
|
||||
} from '../credentials';
|
||||
import { isAscApiKeyValidAndTrackedAsync } from '../validators/validateAscApiKey';
|
||||
import { formatAppleTeam } from './AppleTeamUtils';
|
||||
|
||||
export async function promptForAscApiKeyAsync(): Promise<AscApiKeyPath> {
|
||||
export enum AppStoreApiKeyPurpose {
|
||||
SUBMISSION_SERVICE = 'EAS Submit',
|
||||
}
|
||||
|
||||
export async function promptForAscApiKeyPathAsync(ctx: CredentialsContext): Promise<AscApiKeyPath> {
|
||||
const { keyId, keyP8Path } = await promptForKeyP8AndIdAsync();
|
||||
|
||||
const bestEffortIssuerId = await getBestEffortIssuerIdAsync(ctx, keyId);
|
||||
if (bestEffortIssuerId) {
|
||||
Log.log(`Detected Issuer ID: ${bestEffortIssuerId}`);
|
||||
return { keyId, issuerId: bestEffortIssuerId, keyP8Path };
|
||||
}
|
||||
const issuerId = await promptForIssuerIdAsync();
|
||||
return { keyId, issuerId, keyP8Path };
|
||||
}
|
||||
|
||||
export async function promptForIssuerIdAsync(): Promise<string> {
|
||||
Log.log(chalk.bold('An App Store Connect Issuer ID is required'));
|
||||
Log.log(
|
||||
`If you're not sure what this is or how to find yours, ${learnMore(
|
||||
'https://expo.fyi/asc-issuer-id'
|
||||
)}`
|
||||
);
|
||||
|
||||
// Do not perform uuid validation - Apple's issuerIds are not RFC4122 compliant
|
||||
const { issuerId } = await getCredentialsFromUserAsync(ascApiKeyIssuerIdSchema);
|
||||
return issuerId;
|
||||
}
|
||||
|
||||
export async function getMinimalAscApiKeyAsync(ascApiKey: AscApiKey): Promise<MinimalAscApiKey> {
|
||||
return {
|
||||
...ascApiKey,
|
||||
issuerId: ascApiKey.issuerId ?? (await promptForIssuerIdAsync()),
|
||||
};
|
||||
}
|
||||
|
||||
export async function provideOrGenerateAscApiKeyAsync(
|
||||
ctx: CredentialsContext,
|
||||
purpose: AppStoreApiKeyPurpose
|
||||
): Promise<MinimalAscApiKey> {
|
||||
if (ctx.nonInteractive) {
|
||||
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')
|
||||
);
|
||||
@@ -22,7 +130,7 @@ export async function promptForAscApiKeyAsync(): Promise<AscApiKeyPath> {
|
||||
const { keyP8Path } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'keyP8Path',
|
||||
message: 'Path to App Store Connect Api Key:',
|
||||
message: 'Path to App Store Connect API Key:',
|
||||
initial: 'AuthKey_ABCD.p8',
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
validate: async (filePath: string) => {
|
||||
@@ -39,33 +147,108 @@ export async function promptForAscApiKeyAsync(): Promise<AscApiKeyPath> {
|
||||
});
|
||||
const regex = /^AuthKey_(?<keyId>\w+)\.p8$/; // Common ASC Api file name downloaded from Apple
|
||||
const bestEffortKeyId = path.basename(keyP8Path).match(regex)?.groups?.keyId;
|
||||
const ascApiKeyMetadata = await getCredentialsFromUserAsync(ascApiKeyMetadataSchema, {
|
||||
const { keyId } = await getCredentialsFromUserAsync(ascApiKeyIdSchema, {
|
||||
keyId: bestEffortKeyId,
|
||||
});
|
||||
return { ...ascApiKeyMetadata, keyP8Path };
|
||||
return { keyId, keyP8Path };
|
||||
}
|
||||
|
||||
export async function promptForIssuerIdAsync(): Promise<string> {
|
||||
Log.log(chalk.bold('An App Store Connect Issuer ID is required'));
|
||||
Log.log(
|
||||
`If you're not sure what this is or how to find yours, ${learnMore(
|
||||
'https://expo.fyi/asc-issuer-id'
|
||||
)}`
|
||||
);
|
||||
async function getBestEffortIssuerIdAsync(
|
||||
ctx: CredentialsContext,
|
||||
ascApiKeyId: string
|
||||
): Promise<string | null> {
|
||||
if (!ctx.appStore.authCtx) {
|
||||
return null;
|
||||
}
|
||||
const ascApiKeyInfo = await ctx.appStore.getAscApiKeyAsync(ascApiKeyId);
|
||||
return ascApiKeyInfo?.issuerId ?? null;
|
||||
}
|
||||
|
||||
const { issuerId } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'issuerId',
|
||||
message: 'App Store Connect Issuer ID:',
|
||||
validate: (input: string) => uuid.validate(input),
|
||||
export async function getAscApiKeysFromAccountAsync(
|
||||
ctx: CredentialsContext,
|
||||
account: Account,
|
||||
{ filterDifferentAppleTeam }: { filterDifferentAppleTeam?: boolean } = {}
|
||||
): Promise<AppStoreConnectApiKeyFragment[]> {
|
||||
const ascApiKeysForAccount = await ctx.ios.getAscApiKeysForAccountAsync(account);
|
||||
|
||||
if (!filterDifferentAppleTeam) {
|
||||
return ascApiKeysForAccount;
|
||||
} else {
|
||||
return filterKeysFromDifferentAppleTeam(ctx, ascApiKeysForAccount);
|
||||
}
|
||||
}
|
||||
|
||||
export async function selectAscApiKeysFromAccountAsync(
|
||||
ctx: CredentialsContext,
|
||||
account: Account,
|
||||
{ filterDifferentAppleTeam }: { filterDifferentAppleTeam?: boolean } = {}
|
||||
): Promise<AppStoreConnectApiKeyFragment | null> {
|
||||
const ascApiKeysForAccount = await getAscApiKeysFromAccountAsync(ctx, account, {
|
||||
filterDifferentAppleTeam,
|
||||
});
|
||||
|
||||
return issuerId;
|
||||
if (ascApiKeysForAccount.length === 0) {
|
||||
if (filterDifferentAppleTeam) {
|
||||
const maybeAppleTeamId = ctx.appStore.authCtx?.team.id;
|
||||
Log.warn(
|
||||
`There are no App Store Connect API Keys in your EAS account${
|
||||
maybeAppleTeamId ? ` matching Apple Team ID: ${maybeAppleTeamId}.` : '.'
|
||||
}`
|
||||
);
|
||||
} else {
|
||||
Log.warn(`There are no App Store Connect API Keys available in your EAS account.`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return selectAscApiKeysAsync(ascApiKeysForAccount);
|
||||
}
|
||||
|
||||
export async function getMinimalAscApiKeyAsync(ascApiKey: AscApiKey): Promise<MinimalAscApiKey> {
|
||||
return {
|
||||
...ascApiKey,
|
||||
issuerId: ascApiKey.issuerId ?? (await promptForIssuerIdAsync()),
|
||||
};
|
||||
async function selectAscApiKeysAsync(
|
||||
ascApiKeys: AppStoreConnectApiKeyFragment[]
|
||||
): Promise<AppStoreConnectApiKeyFragment | null> {
|
||||
const sortedAscApiKeys = sortAscApiKeysByUpdatedAtDesc(ascApiKeys);
|
||||
const { chosenAscApiKey } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'chosenAscApiKey',
|
||||
message: 'Select an API Key from the list:',
|
||||
choices: sortedAscApiKeys.map(ascApiKey => ({
|
||||
title: formatAscApiKey(ascApiKey),
|
||||
value: ascApiKey,
|
||||
})),
|
||||
});
|
||||
return chosenAscApiKey;
|
||||
}
|
||||
|
||||
function filterKeysFromDifferentAppleTeam(
|
||||
ctx: CredentialsContext,
|
||||
keys: AppStoreConnectApiKeyFragment[]
|
||||
): AppStoreConnectApiKeyFragment[] {
|
||||
if (!ctx.appStore.authCtx) {
|
||||
return keys;
|
||||
}
|
||||
const teamId = ctx.appStore.authCtx.team.id;
|
||||
return keys.filter(key => !key.appleTeam || key.appleTeam?.appleTeamIdentifier === teamId);
|
||||
}
|
||||
|
||||
export function sortAscApiKeysByUpdatedAtDesc(
|
||||
keys: AppStoreConnectApiKeyFragment[]
|
||||
): AppStoreConnectApiKeyFragment[] {
|
||||
return keys.sort(
|
||||
(keyA, keyB) => new Date(keyB.updatedAt).getTime() - new Date(keyA.updatedAt).getTime()
|
||||
);
|
||||
}
|
||||
|
||||
export function formatAscApiKey(ascApiKey: AppStoreConnectApiKeyFragment): string {
|
||||
const { keyIdentifier, appleTeam, name, updatedAt } = ascApiKey;
|
||||
let line: string = '';
|
||||
line += `Key ID: ${keyIdentifier}`;
|
||||
|
||||
if (name) {
|
||||
line += chalk.gray(`\n Name: ${name}`);
|
||||
}
|
||||
if (appleTeam) {
|
||||
line += chalk.gray(`\n ${formatAppleTeam(appleTeam)}`);
|
||||
}
|
||||
|
||||
line += chalk.gray(`\n Updated: ${fromNow(new Date(updatedAt))} ago`);
|
||||
return line;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
AppStoreConnectApiKeyFragment,
|
||||
CommonIosAppCredentialsFragment,
|
||||
} from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
|
||||
import { AppStoreApiKeyPurpose } from './AscApiKeyUtils';
|
||||
|
||||
export class AssignAscApiKey {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
public async runAsync(
|
||||
ctx: CredentialsContext,
|
||||
ascApiKey: AppStoreConnectApiKeyFragment,
|
||||
purpose: AppStoreApiKeyPurpose
|
||||
): Promise<CommonIosAppCredentialsFragment> {
|
||||
const appleTeam =
|
||||
(await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app)) ?? ascApiKey.appleTeam ?? null;
|
||||
const appCredentials = await ctx.ios.createOrGetIosAppCredentialsWithCommonFieldsAsync(
|
||||
this.app,
|
||||
{ appleTeam: appleTeam ?? undefined }
|
||||
);
|
||||
let updatedAppCredentials;
|
||||
if (purpose === AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
|
||||
updatedAppCredentials = await ctx.ios.updateIosAppCredentialsAsync(appCredentials, {
|
||||
ascApiKeyIdForSubmissions: ascApiKey.id,
|
||||
});
|
||||
} else {
|
||||
throw new Error(`${purpose} is not yet supported.`);
|
||||
}
|
||||
Log.succeed(
|
||||
`App Store Connect API Key assigned to ${this.app.projectName}: ${this.app.bundleIdentifier} for ${purpose}.`
|
||||
);
|
||||
return updatedAppCredentials;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { AppStoreApiKeyPurpose, provideOrGenerateAscApiKeyAsync } from './AscApiKeyUtils';
|
||||
|
||||
export class CreateAscApiKey {
|
||||
constructor(private account: Account) {}
|
||||
|
||||
public async runAsync(
|
||||
ctx: CredentialsContext,
|
||||
purpose: AppStoreApiKeyPurpose
|
||||
): Promise<AppStoreConnectApiKeyFragment> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
|
||||
}
|
||||
|
||||
const ascApiKey = await provideOrGenerateAscApiKeyAsync(ctx, purpose);
|
||||
const result = await ctx.ios.createAscApiKeyAsync(this.account, ascApiKey);
|
||||
Log.succeed('Created App Store Connect API Key');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ export async function provideOrGenerateDistributionCertificateAsync(
|
||||
if (userProvided) {
|
||||
if (!ctx.appStore.authCtx) {
|
||||
Log.warn(
|
||||
'Unable to validate distribution certificate due to insufficient Apple Credentials'
|
||||
'Unable to validate distribution certificate, you are not authenticated with Apple.'
|
||||
);
|
||||
return userProvided;
|
||||
} else {
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function provideOrGeneratePushKeyAsync(ctx: CredentialsContext): Pr
|
||||
const userProvided = await promptForPushKeyAsync(ctx);
|
||||
if (userProvided) {
|
||||
if (!ctx.appStore.authCtx) {
|
||||
Log.warn('Unable to validate push key due to insufficient Apple Credentials');
|
||||
Log.warn('Unable to validate push key, you are not authenticated with Apple.');
|
||||
return userProvided;
|
||||
} else {
|
||||
const isValidAndTracked = await isPushKeyValidAndTrackedAsync(ctx, userProvided);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { selectAscApiKeysFromAccountAsync } from './AscApiKeyUtils';
|
||||
|
||||
export class SelectAndRemoveAscApiKey {
|
||||
constructor(private account: Account) {}
|
||||
|
||||
async runAsync(ctx: CredentialsContext): Promise<void> {
|
||||
const selected = await selectAscApiKeysFromAccountAsync(ctx, this.account);
|
||||
if (selected) {
|
||||
await new RemoveAscApiKey(this.account, selected).runAsync(ctx);
|
||||
Log.succeed('Removed App Store Connect API Key');
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoveAscApiKey {
|
||||
constructor(private account: Account, private ascApiKey: AppStoreConnectApiKeyFragment) {}
|
||||
|
||||
public async runAsync(ctx: CredentialsContext): Promise<void> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(`Cannot remove App Store Connect API Keys in non-interactive mode.`);
|
||||
}
|
||||
|
||||
// TODO(quin): add an extra edge on AppStoreConnectApiKey to find the apps using it
|
||||
const confirm = await confirmAsync({
|
||||
message: `Deleting this API Key may affect your projects that rely on it. Do you want to continue?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
Log.log('Aborting');
|
||||
return;
|
||||
}
|
||||
|
||||
Log.log('Removing API Key');
|
||||
await ctx.ios.deleteAscApiKeyAsync(this.ascApiKey.id);
|
||||
|
||||
let shouldRevoke = false;
|
||||
if (!ctx.nonInteractive) {
|
||||
shouldRevoke = await confirmAsync({
|
||||
message: `Do you also want to revoke this API Key on the Apple Developer Portal?`,
|
||||
});
|
||||
} else if (ctx.nonInteractive) {
|
||||
Log.log('Skipping API Key revocation on the Apple Developer Portal.');
|
||||
}
|
||||
if (shouldRevoke) {
|
||||
await ctx.appStore.revokeAscApiKeyAsync(this.ascApiKey.keyIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -21,7 +21,7 @@ import { validateProvisioningProfileAsync } from '../validators/validateProvisio
|
||||
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
|
||||
import { assignBuildCredentialsAsync, getBuildCredentialsAsync } from './BuildCredentialsUtils';
|
||||
import { chooseDevicesAsync, formatDeviceLabel } from './DeviceUtils';
|
||||
import { SetupDistributionCertificate } from './SetupDistributionCertificate';
|
||||
import { SetUpDistributionCertificate } from './SetUpDistributionCertificate';
|
||||
|
||||
enum ReuseAction {
|
||||
Yes,
|
||||
@@ -29,11 +29,11 @@ enum ReuseAction {
|
||||
No,
|
||||
}
|
||||
|
||||
export class SetupAdhocProvisioningProfile {
|
||||
export class SetUpAdhocProvisioningProfile {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
|
||||
const distCert = await new SetupDistributionCertificate(
|
||||
const distCert = await new SetUpDistributionCertificate(
|
||||
this.app,
|
||||
IosDistributionType.AdHoc
|
||||
).runAsync(ctx);
|
||||
@@ -0,0 +1,148 @@
|
||||
import assert from 'assert';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import {
|
||||
AppStoreConnectApiKeyFragment,
|
||||
CommonIosAppCredentialsFragment,
|
||||
} from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync, promptAsync } from '../../../prompts';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import {
|
||||
MissingCredentialsNonInteractiveError,
|
||||
UnsupportedCredentialsChoiceError,
|
||||
} from '../../errors';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { getValidAndTrackedAscApiKeysAsync } from '../validators/validateAscApiKey';
|
||||
import {
|
||||
AppStoreApiKeyPurpose,
|
||||
formatAscApiKey,
|
||||
getAscApiKeysFromAccountAsync,
|
||||
selectAscApiKeysFromAccountAsync,
|
||||
sortAscApiKeysByUpdatedAtDesc,
|
||||
} from './AscApiKeyUtils';
|
||||
import { AssignAscApiKey } from './AssignAscApiKey';
|
||||
import { CreateAscApiKey } from './CreateAscApiKey';
|
||||
|
||||
export enum SetupAscApiKeyChoice {
|
||||
GENERATE = 'GENERATE',
|
||||
USE_EXISTING = 'USE_EXISTING',
|
||||
}
|
||||
export class SetUpAscApiKey {
|
||||
public choices: { title: string; value: string }[] = [
|
||||
{
|
||||
title: '[Choose an existing 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
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -7,7 +7,7 @@ import { CredentialsContext } from '../../context';
|
||||
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
|
||||
import { App, IosAppBuildCredentialsMap, IosCredentials, Target } from '../types';
|
||||
import { displayProjectCredentials } from '../utils/printCredentials';
|
||||
import { SetupTargetBuildCredentials } from './SetupTargetBuildCredentials';
|
||||
import { SetUpTargetBuildCredentials } from './SetUpTargetBuildCredentials';
|
||||
|
||||
interface Options {
|
||||
app: App;
|
||||
@@ -17,7 +17,7 @@ interface Options {
|
||||
iosCapabilitiesOptions?: IosCapabilitiesOptions;
|
||||
}
|
||||
|
||||
export class SetupBuildCredentials {
|
||||
export class SetUpBuildCredentials {
|
||||
constructor(private options: Options) {}
|
||||
|
||||
async runAsync(ctx: CredentialsContext): Promise<IosCredentials> {
|
||||
@@ -49,7 +49,7 @@ export class SetupBuildCredentials {
|
||||
} else {
|
||||
Log.newLine();
|
||||
}
|
||||
const action = new SetupTargetBuildCredentials({
|
||||
const action = new SetUpTargetBuildCredentials({
|
||||
...this.options,
|
||||
app: {
|
||||
...this.options.app,
|
||||
+3
-3
@@ -8,9 +8,9 @@ import { readIosCredentialsAsync } from '../../credentialsJson/read';
|
||||
import { IosCredentials } from '../../credentialsJson/types';
|
||||
import { ensureAllTargetsAreConfigured } from '../../credentialsJson/utils';
|
||||
import { App, Target } from '../types';
|
||||
import { SetupTargetBuildCredentialsFromCredentialsJson } from './SetupTargetBuildCredentialsFromCredentialsJson';
|
||||
import { SetUpTargetBuildCredentialsFromCredentialsJson } from './SetUpTargetBuildCredentialsFromCredentialsJson';
|
||||
|
||||
export class SetupBuildCredentialsFromCredentialsJson {
|
||||
export class SetUpBuildCredentialsFromCredentialsJson {
|
||||
constructor(
|
||||
private app: App,
|
||||
private targets: Target[],
|
||||
@@ -32,7 +32,7 @@ export class SetupBuildCredentialsFromCredentialsJson {
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
await new SetupTargetBuildCredentialsFromCredentialsJson(
|
||||
await new SetUpTargetBuildCredentialsFromCredentialsJson(
|
||||
{
|
||||
account: this.app.account,
|
||||
projectName: this.app.projectName,
|
||||
+1
-1
@@ -18,7 +18,7 @@ import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
|
||||
import { CreateDistributionCertificate } from './CreateDistributionCertificate';
|
||||
import { formatDistributionCertificate } from './DistributionCertificateUtils';
|
||||
|
||||
export class SetupDistributionCertificate {
|
||||
export class SetUpDistributionCertificate {
|
||||
private validDistCerts?: AppleDistributionCertificateFragment[];
|
||||
|
||||
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
|
||||
+5
-5
@@ -4,8 +4,8 @@ import { promptAsync } from '../../../prompts';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { getAllBuildCredentialsAsync } from './BuildCredentialsUtils';
|
||||
import { SetupAdhocProvisioningProfile } from './SetupAdhocProvisioningProfile';
|
||||
import { SetupProvisioningProfile } from './SetupProvisioningProfile';
|
||||
import { SetUpAdhocProvisioningProfile } from './SetUpAdhocProvisioningProfile';
|
||||
import { SetUpProvisioningProfile } from './SetUpProvisioningProfile';
|
||||
|
||||
/**
|
||||
* It's used when setting up credentials for internal distribution but `enterpriseProvisioning` is not set.
|
||||
@@ -14,7 +14,7 @@ import { SetupProvisioningProfile } from './SetupProvisioningProfile';
|
||||
* to choose if they want to set up an adhoc or universal distribution provisioning profile. Otherwise, always
|
||||
* set up an adhoc provisioning profile.
|
||||
*/
|
||||
export class SetupInternalProvisioningProfile {
|
||||
export class SetUpInternalProvisioningProfile {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
|
||||
@@ -82,13 +82,13 @@ export class SetupInternalProvisioningProfile {
|
||||
private async setupAdhocProvisioningProfileAsync(
|
||||
ctx: CredentialsContext
|
||||
): Promise<IosAppBuildCredentialsFragment> {
|
||||
return await new SetupAdhocProvisioningProfile(this.app).runAsync(ctx);
|
||||
return await new SetUpAdhocProvisioningProfile(this.app).runAsync(ctx);
|
||||
}
|
||||
|
||||
private async setupUniversalProvisioningProfileAsync(
|
||||
ctx: CredentialsContext
|
||||
): Promise<IosAppBuildCredentialsFragment> {
|
||||
return await new SetupProvisioningProfile(this.app, IosDistributionType.Enterprise).runAsync(
|
||||
return await new SetUpProvisioningProfile(this.app, IosDistributionType.Enterprise).runAsync(
|
||||
ctx
|
||||
);
|
||||
}
|
||||
+3
-3
@@ -20,12 +20,12 @@ import {
|
||||
import { ConfigureProvisioningProfile } from './ConfigureProvisioningProfile';
|
||||
import { CreateProvisioningProfile } from './CreateProvisioningProfile';
|
||||
import { formatProvisioningProfileFromApple } from './ProvisioningProfileUtils';
|
||||
import { SetupDistributionCertificate } from './SetupDistributionCertificate';
|
||||
import { SetUpDistributionCertificate } from './SetUpDistributionCertificate';
|
||||
|
||||
/**
|
||||
* Sets up either APP_STORE or ENTERPRISE provisioning profiles
|
||||
*/
|
||||
export class SetupProvisioningProfile {
|
||||
export class SetUpProvisioningProfile {
|
||||
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
|
||||
|
||||
async areBuildCredentialsSetupAsync(ctx: CredentialsContext): Promise<boolean> {
|
||||
@@ -84,7 +84,7 @@ export class SetupProvisioningProfile {
|
||||
}
|
||||
|
||||
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
|
||||
const distCert = await new SetupDistributionCertificate(
|
||||
const distCert = await new SetUpDistributionCertificate(
|
||||
this.app,
|
||||
this.distributionType
|
||||
).runAsync(ctx);
|
||||
+1
-1
@@ -14,7 +14,7 @@ import {
|
||||
selectPushKeyAsync,
|
||||
} from './PushKeyUtils';
|
||||
|
||||
export class SetupPushKey {
|
||||
export class SetUpPushKey {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
async isPushKeySetupAsync(ctx: CredentialsContext): Promise<boolean> {
|
||||
@@ -0,0 +1,70 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -8,9 +8,9 @@ import Log from '../../../log';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { AppLookupParams as GraphQLAppLookupParams } from '../api/GraphqlClient';
|
||||
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
|
||||
import { SetupAdhocProvisioningProfile } from './SetupAdhocProvisioningProfile';
|
||||
import { SetupInternalProvisioningProfile } from './SetupInternalProvisioningProfile';
|
||||
import { SetupProvisioningProfile } from './SetupProvisioningProfile';
|
||||
import { SetUpAdhocProvisioningProfile } from './SetUpAdhocProvisioningProfile';
|
||||
import { SetUpInternalProvisioningProfile } from './SetUpInternalProvisioningProfile';
|
||||
import { SetUpProvisioningProfile } from './SetUpProvisioningProfile';
|
||||
|
||||
interface Options {
|
||||
app: GraphQLAppLookupParams;
|
||||
@@ -18,7 +18,7 @@ interface Options {
|
||||
enterpriseProvisioning?: IosEnterpriseProvisioning;
|
||||
iosCapabilitiesOptions?: IosCapabilitiesOptions;
|
||||
}
|
||||
export class SetupTargetBuildCredentials {
|
||||
export class SetUpTargetBuildCredentials {
|
||||
constructor(private options: Options) {}
|
||||
|
||||
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
|
||||
@@ -50,17 +50,17 @@ export class SetupTargetBuildCredentials {
|
||||
const { app, distribution, enterpriseProvisioning } = this.options;
|
||||
if (distribution === 'internal') {
|
||||
if (enterpriseProvisioning === 'adhoc') {
|
||||
return await new SetupAdhocProvisioningProfile(app).runAsync(ctx);
|
||||
return await new SetUpAdhocProvisioningProfile(app).runAsync(ctx);
|
||||
} else if (enterpriseProvisioning === 'universal') {
|
||||
return await new SetupProvisioningProfile(
|
||||
return await new SetUpProvisioningProfile(
|
||||
app,
|
||||
GraphQLIosDistributionType.Enterprise
|
||||
).runAsync(ctx);
|
||||
} else {
|
||||
return await new SetupInternalProvisioningProfile(app).runAsync(ctx);
|
||||
return await new SetUpInternalProvisioningProfile(app).runAsync(ctx);
|
||||
}
|
||||
} else {
|
||||
return await new SetupProvisioningProfile(app, GraphQLIosDistributionType.AppStore).runAsync(
|
||||
return await new SetUpProvisioningProfile(app, GraphQLIosDistributionType.AppStore).runAsync(
|
||||
ctx
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -22,7 +22,7 @@ import {
|
||||
getProvisioningProfileAsync,
|
||||
} from './BuildCredentialsUtils';
|
||||
|
||||
export class SetupTargetBuildCredentialsFromCredentialsJson {
|
||||
export class SetUpTargetBuildCredentialsFromCredentialsJson {
|
||||
constructor(
|
||||
private app: AppLookupParams,
|
||||
private distributionType: IosDistributionType,
|
||||
+1
-1
@@ -5,7 +5,7 @@ import {
|
||||
import { testDistCertFragmentOneDependency } from '../../../__tests__/fixtures-ios';
|
||||
import { CredentialsContext } from '../../../context';
|
||||
import { AppLookupParams } from '../../api/GraphqlClient';
|
||||
export class SetupDistributionCertificate {
|
||||
export class SetUpDistributionCertificate {
|
||||
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
|
||||
|
||||
public async runAsync(_ctx: CredentialsContext): Promise<AppleDistributionCertificateFragment> {
|
||||
@@ -0,0 +1,90 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { promptAsync } from '../../../../prompts';
|
||||
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { testAscApiKey } from '../../../__tests__/fixtures-ios';
|
||||
import { getCredentialsFromUserAsync } from '../../../utils/promptForCredentials';
|
||||
import {
|
||||
AppStoreApiKeyPurpose,
|
||||
getAscApiKeyName,
|
||||
promptForAscApiKeyPathAsync,
|
||||
} from '../AscApiKeyUtils';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
jest.mock('../../../utils/promptForCredentials');
|
||||
|
||||
function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
|
||||
return Object.keys(obj) as K[];
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
asMock(promptAsync).mockClear();
|
||||
asMock(getCredentialsFromUserAsync).mockClear();
|
||||
});
|
||||
|
||||
describe(getAscApiKeyName, () => {
|
||||
// Apple enforces a 30 char limit on this name
|
||||
it('produces a name under 30 chars', async () => {
|
||||
for (const value of enumKeys(AppStoreApiKeyPurpose)) {
|
||||
const purpose = AppStoreApiKeyPurpose[value];
|
||||
const name = getAscApiKeyName(purpose);
|
||||
expect(name.length < 30).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe(promptForAscApiKeyPathAsync, () => {
|
||||
it('prompts for keyId, keyP8Path and issuerId when user is not authenticated to Apple', async () => {
|
||||
asMock(promptAsync).mockImplementationOnce(() => ({
|
||||
keyP8Path: '/asc-api-key.p8',
|
||||
}));
|
||||
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
|
||||
keyId: 'test-key-id',
|
||||
issuerId: 'test-issuer-id',
|
||||
}));
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
ensureAuthenticatedAsync: jest.fn(() => {
|
||||
throw new Error('this should not be called');
|
||||
}),
|
||||
authCtx: null,
|
||||
},
|
||||
});
|
||||
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
|
||||
expect(ascApiKeyPath).toEqual({
|
||||
keyId: 'test-key-id',
|
||||
issuerId: 'test-issuer-id',
|
||||
keyP8Path: '/asc-api-key.p8',
|
||||
});
|
||||
expect(promptAsync).toHaveBeenCalledTimes(1); // keyP8Path
|
||||
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(2); // keyId, issuerId
|
||||
});
|
||||
it('prompts for keyId, keyP8Path and detects issuerId when user is authenticated to Apple', async () => {
|
||||
asMock(promptAsync).mockImplementationOnce(() => ({
|
||||
keyP8Path: '/asc-api-key.p8',
|
||||
}));
|
||||
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
|
||||
keyId: 'test-key-id',
|
||||
}));
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
|
||||
authCtx: testAuthCtx,
|
||||
getAscApiKeyAsync: jest.fn(() => testAscApiKey),
|
||||
},
|
||||
});
|
||||
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
|
||||
expect(ascApiKeyPath).toEqual({
|
||||
keyId: 'test-key-id',
|
||||
issuerId: 'test-issuer-id-from-apple',
|
||||
keyP8Path: '/asc-api-key.p8',
|
||||
});
|
||||
expect(promptAsync).toHaveBeenCalledTimes(1); // keyP8Path
|
||||
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(1); // keyId
|
||||
expect(asMock(ctx.appStore.getAscApiKeyAsync).mock.calls.length).toBe(1); // issuerId
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { findApplicationTarget } from '../../../../project/ios/target';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { testAscApiKeyFragment, testTargets } from '../../../__tests__/fixtures-ios';
|
||||
import { AppStoreApiKeyPurpose } from '../AscApiKeyUtils';
|
||||
import { AssignAscApiKey } from '../AssignAscApiKey';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
|
||||
describe(AssignAscApiKey, () => {
|
||||
it('assigns an App Store Connect API Key in Interactive Mode for EAS Submit', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const assignAscApiKeyAction = new AssignAscApiKey(appLookupParams);
|
||||
await assignAscApiKeyAction.runAsync(
|
||||
ctx,
|
||||
testAscApiKeyFragment,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
);
|
||||
|
||||
// expect app credentials to be fetched/created, then updated
|
||||
expect(ctx.ios.createOrGetIosAppCredentialsWithCommonFieldsAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.ios.updateIosAppCredentialsAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('works in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const assignAscApiKeyAction = new AssignAscApiKey(appLookupParams);
|
||||
await assignAscApiKeyAction.runAsync(
|
||||
ctx,
|
||||
testAscApiKeyFragment,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
);
|
||||
|
||||
// dont fail if users are running in non-interactive mode
|
||||
await expect(
|
||||
assignAscApiKeyAction.runAsync(
|
||||
ctx,
|
||||
testAscApiKeyFragment,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
)
|
||||
).resolves.not.toThrowError();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { findApplicationTarget } from '../../../../project/ios/target';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { testAscApiKey, testTargets } from '../../../__tests__/fixtures-ios';
|
||||
import { AppStoreApiKeyPurpose } from '../AscApiKeyUtils';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { CreateAscApiKey } from '../CreateAscApiKey';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
|
||||
describe(CreateAscApiKey, () => {
|
||||
it('creates a App Store API Key in Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
|
||||
authCtx: testAuthCtx,
|
||||
createAscApiKeyAsync: jest.fn(() => testAscApiKey),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const createAscApiKeyAction = new CreateAscApiKey(appLookupParams.account);
|
||||
await createAscApiKeyAction.runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
|
||||
|
||||
// expect api key to be created on expo servers
|
||||
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
|
||||
// expect api key to be created on apple portal
|
||||
expect(asMock(ctx.appStore.createAscApiKeyAsync).mock.calls.length).toBe(1);
|
||||
});
|
||||
it('errors in Non Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const createAscApiKeyAction = new CreateAscApiKey(appLookupParams.account);
|
||||
await expect(
|
||||
createAscApiKeyAction.runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE)
|
||||
).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { findApplicationTarget } from '../../../../project/ios/target';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { testAscApiKeyFragment, testTargets } from '../../../__tests__/fixtures-ios';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { RemoveAscApiKey } from '../RemoveAscApiKey';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
|
||||
describe(RemoveAscApiKey, () => {
|
||||
it('removes an Asc API Key', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: false });
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const removeAscApiKeyAction = new RemoveAscApiKey(
|
||||
appLookupParams.account,
|
||||
testAscApiKeyFragment
|
||||
);
|
||||
await removeAscApiKeyAction.runAsync(ctx);
|
||||
expect(ctx.ios.deleteAscApiKeyAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.appStore.revokeAscApiKeyAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: true });
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const removeAscApiKeyAction = new RemoveAscApiKey(
|
||||
appLookupParams.account,
|
||||
testAscApiKeyFragment
|
||||
);
|
||||
await expect(removeAscApiKeyAction.runAsync(ctx)).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { doUDIDsMatch } from '../SetupAdhocProvisioningProfile';
|
||||
import { doUDIDsMatch } from '../SetUpAdhocProvisioningProfile';
|
||||
|
||||
describe(doUDIDsMatch, () => {
|
||||
it('return false if UDIDs do not match', () => {
|
||||
@@ -0,0 +1,146 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { findApplicationTarget } from '../../../../project/ios/target';
|
||||
import { confirmAsync, promptAsync } from '../../../../prompts';
|
||||
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import {
|
||||
getNewIosApiMock,
|
||||
testAscApiKey,
|
||||
testAscApiKeyFragment,
|
||||
testCommonIosAppCredentialsFragment,
|
||||
testTargets,
|
||||
} from '../../../__tests__/fixtures-ios';
|
||||
import { AppStoreApiKeyPurpose } from '../AscApiKeyUtils';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { SetUpAscApiKey, SetupAscApiKeyChoice } from '../SetUpAscApiKey';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
|
||||
describe(SetUpAscApiKey, () => {
|
||||
it('skips setting up a ASC API Key if it is already configured', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
ios: {
|
||||
...getNewIosApiMock(),
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testCommonIosAppCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupAscApiKeyAction = new SetUpAscApiKey(
|
||||
appLookupParams,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
);
|
||||
await setupAscApiKeyAction.runAsync(ctx);
|
||||
|
||||
// expect not to create a new ASC API Key on expo servers
|
||||
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
|
||||
// expect configuration not to be updated with a new ASC API Key
|
||||
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
|
||||
});
|
||||
it('sets up a ASC API Key, creating a new one if there are no existing ASC API Keys', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
createAscApiKeyAsync: jest.fn(() => testAscApiKey),
|
||||
},
|
||||
ios: {
|
||||
...getNewIosApiMock(),
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(),
|
||||
createAscApiKeyAsync: jest.fn(() => testAscApiKeyFragment),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupAscApiKeyAction = new SetUpAscApiKey(
|
||||
appLookupParams,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
);
|
||||
await setupAscApiKeyAction.runAsync(ctx);
|
||||
|
||||
// expect to create a new ASC API Key on expo servers
|
||||
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
|
||||
// expect configuration to be updated with a new ASC API Key
|
||||
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
|
||||
});
|
||||
it('sets up a ASC API Key, use autoselected ASC API Key when there are existing keys', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
|
||||
authCtx: testAuthCtx,
|
||||
listAscApiKeysAsync: jest.fn(() => [testAscApiKey]),
|
||||
},
|
||||
ios: {
|
||||
...getNewIosApiMock(),
|
||||
getAscApiKeysForAccountAsync: jest.fn(() => [testAscApiKeyFragment]),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupAscApiKeyAction = new SetUpAscApiKey(
|
||||
appLookupParams,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
);
|
||||
await setupAscApiKeyAction.runAsync(ctx);
|
||||
|
||||
// expect not to create a new ASC API Key on expo servers, we are using the existing one
|
||||
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
|
||||
// expect configuration to be updated with a new ASC API Key
|
||||
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
|
||||
});
|
||||
it('sets up a ASC API Key, allowing user to choose existing keys', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
ios: {
|
||||
...getNewIosApiMock(),
|
||||
getAscApiKeysForAccountAsync: jest.fn(() => [testAscApiKeyFragment]),
|
||||
},
|
||||
});
|
||||
asMock(promptAsync).mockImplementation(() => ({
|
||||
choice: SetupAscApiKeyChoice.USE_EXISTING,
|
||||
chosenAscApiKey: testAscApiKeyFragment,
|
||||
}));
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupAscApiKeyAction = new SetUpAscApiKey(
|
||||
appLookupParams,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
);
|
||||
await setupAscApiKeyAction.runAsync(ctx);
|
||||
|
||||
// expect not to create a new ASC API Key on expo servers, we are using the existing one
|
||||
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
|
||||
// expect configuration to be updated with a new ASC API Key
|
||||
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
|
||||
});
|
||||
it('works in Non Interactive Mode if ASC Key is configured', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
ios: {
|
||||
...getNewIosApiMock(),
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testCommonIosAppCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupAscApiKeyAction = new SetUpAscApiKey(
|
||||
appLookupParams,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
);
|
||||
await expect(setupAscApiKeyAction.runAsync(ctx)).resolves.not.toThrowError();
|
||||
});
|
||||
it('errors in Non Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const createAscApiKeyAction = new SetUpAscApiKey(
|
||||
appLookupParams,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
);
|
||||
await expect(createAscApiKeyAction.runAsync(ctx)).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
+28
-28
@@ -4,13 +4,13 @@ import { promptAsync } from '../../../../prompts';
|
||||
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAllBuildCredentialsAsync } from '../BuildCredentialsUtils';
|
||||
import { SetupAdhocProvisioningProfile } from '../SetupAdhocProvisioningProfile';
|
||||
import { SetupInternalProvisioningProfile } from '../SetupInternalProvisioningProfile';
|
||||
import { SetupProvisioningProfile } from '../SetupProvisioningProfile';
|
||||
import { SetUpAdhocProvisioningProfile } from '../SetUpAdhocProvisioningProfile';
|
||||
import { SetUpInternalProvisioningProfile } from '../SetUpInternalProvisioningProfile';
|
||||
import { SetUpProvisioningProfile } from '../SetUpProvisioningProfile';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
jest.mock('../SetupAdhocProvisioningProfile');
|
||||
jest.mock('../SetupProvisioningProfile');
|
||||
jest.mock('../SetUpAdhocProvisioningProfile');
|
||||
jest.mock('../SetUpProvisioningProfile');
|
||||
jest.mock('../BuildCredentialsUtils', () => ({ getAllBuildCredentialsAsync: jest.fn() }));
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -33,15 +33,15 @@ const testEnterpriseBuildCredentials: IosAppBuildCredentialsFragment = {
|
||||
iosDistributionType: IosDistributionType.Enterprise,
|
||||
};
|
||||
|
||||
describe(SetupInternalProvisioningProfile, () => {
|
||||
describe(SetUpInternalProvisioningProfile, () => {
|
||||
describe('interactive mode', () => {
|
||||
describe('when authenticated with apple', () => {
|
||||
it('runs the SetupAdhocProvisioningProfile action for non-enterprise team', async () => {
|
||||
it('runs the SetUpAdhocProvisioningProfile action for non-enterprise team', async () => {
|
||||
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
|
||||
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
|
||||
return buildCredentials;
|
||||
});
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
const action = new SetUpInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
@@ -56,7 +56,7 @@ describe(SetupInternalProvisioningProfile, () => {
|
||||
});
|
||||
|
||||
const runAsync = jest.fn();
|
||||
SetupAdhocProvisioningProfile.prototype.runAsync = runAsync;
|
||||
SetUpAdhocProvisioningProfile.prototype.runAsync = runAsync;
|
||||
|
||||
await action.runAsync(ctx);
|
||||
|
||||
@@ -72,7 +72,7 @@ describe(SetupInternalProvisioningProfile, () => {
|
||||
return buildCredentials;
|
||||
});
|
||||
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
const action = new SetUpInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
@@ -90,7 +90,7 @@ describe(SetupInternalProvisioningProfile, () => {
|
||||
});
|
||||
|
||||
const runAsync = jest.fn();
|
||||
SetupProvisioningProfile.prototype.runAsync = runAsync;
|
||||
SetUpProvisioningProfile.prototype.runAsync = runAsync;
|
||||
|
||||
await action.runAsync(ctx);
|
||||
|
||||
@@ -110,7 +110,7 @@ describe(SetupInternalProvisioningProfile, () => {
|
||||
return buildCredentials;
|
||||
});
|
||||
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
const action = new SetUpInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
@@ -125,14 +125,14 @@ describe(SetupInternalProvisioningProfile, () => {
|
||||
});
|
||||
|
||||
const runAsync = jest.fn();
|
||||
SetupProvisioningProfile.prototype.runAsync = runAsync;
|
||||
SetUpProvisioningProfile.prototype.runAsync = runAsync;
|
||||
|
||||
await action.runAsync(ctx);
|
||||
|
||||
expect(runAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('runs the SetupAdhocProvisioningProfile action when adhoc credentials exist', async () => {
|
||||
it('runs the SetUpAdhocProvisioningProfile action when adhoc credentials exist', async () => {
|
||||
asMock(promptAsync).mockImplementationOnce(() => ({
|
||||
distributionType: IosDistributionType.Enterprise,
|
||||
}));
|
||||
@@ -141,7 +141,7 @@ describe(SetupInternalProvisioningProfile, () => {
|
||||
return buildCredentials;
|
||||
});
|
||||
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
const action = new SetUpInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
@@ -156,14 +156,14 @@ describe(SetupInternalProvisioningProfile, () => {
|
||||
});
|
||||
|
||||
const runAsync = jest.fn();
|
||||
SetupAdhocProvisioningProfile.prototype.runAsync = runAsync;
|
||||
SetUpAdhocProvisioningProfile.prototype.runAsync = runAsync;
|
||||
|
||||
await action.runAsync(ctx);
|
||||
|
||||
expect(runAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('runs the SetupProvisioningProfile action when enterprise credentials exist', async () => {
|
||||
it('runs the SetUpProvisioningProfile action when enterprise credentials exist', async () => {
|
||||
asMock(promptAsync).mockImplementationOnce(() => ({
|
||||
distributionType: IosDistributionType.Enterprise,
|
||||
}));
|
||||
@@ -174,7 +174,7 @@ describe(SetupInternalProvisioningProfile, () => {
|
||||
return buildCredentials;
|
||||
});
|
||||
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
const action = new SetUpInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
@@ -189,7 +189,7 @@ describe(SetupInternalProvisioningProfile, () => {
|
||||
});
|
||||
|
||||
const runAsync = jest.fn();
|
||||
SetupProvisioningProfile.prototype.runAsync = runAsync;
|
||||
SetUpProvisioningProfile.prototype.runAsync = runAsync;
|
||||
|
||||
await action.runAsync(ctx);
|
||||
|
||||
@@ -205,7 +205,7 @@ describe(SetupInternalProvisioningProfile, () => {
|
||||
return buildCredentials;
|
||||
});
|
||||
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
const action = new SetUpInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
@@ -235,7 +235,7 @@ describe(SetupInternalProvisioningProfile, () => {
|
||||
];
|
||||
return buildCredentials;
|
||||
});
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
const action = new SetUpInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
@@ -253,7 +253,7 @@ describe(SetupInternalProvisioningProfile, () => {
|
||||
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
|
||||
return buildCredentials;
|
||||
});
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
const action = new SetUpInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
@@ -264,12 +264,12 @@ describe(SetupInternalProvisioningProfile, () => {
|
||||
await expect(action.runAsync(ctx)).rejects.toThrow(/couldn't find any credentials/);
|
||||
});
|
||||
|
||||
it('runs the SetupAdhocProvisioningProfile action when adhoc credentials exist', async () => {
|
||||
it('runs the SetUpAdhocProvisioningProfile action when adhoc credentials exist', async () => {
|
||||
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
|
||||
const buildCredentials: IosAppBuildCredentialsFragment[] = [testAdhocBuildCredentials];
|
||||
return buildCredentials;
|
||||
});
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
const action = new SetUpInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
@@ -279,19 +279,19 @@ describe(SetupInternalProvisioningProfile, () => {
|
||||
});
|
||||
|
||||
const runAsync = jest.fn();
|
||||
SetupAdhocProvisioningProfile.prototype.runAsync = runAsync;
|
||||
SetUpAdhocProvisioningProfile.prototype.runAsync = runAsync;
|
||||
|
||||
await action.runAsync(ctx);
|
||||
|
||||
expect(runAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('runs the SetupProvisioningProfile action when enterprise credentials exist', async () => {
|
||||
it('runs the SetUpProvisioningProfile action when enterprise credentials exist', async () => {
|
||||
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
|
||||
const buildCredentials: IosAppBuildCredentialsFragment[] = [testEnterpriseBuildCredentials];
|
||||
return buildCredentials;
|
||||
});
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
const action = new SetUpInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
@@ -301,7 +301,7 @@ describe(SetupInternalProvisioningProfile, () => {
|
||||
});
|
||||
|
||||
const runAsync = jest.fn();
|
||||
SetupProvisioningProfile.prototype.runAsync = runAsync;
|
||||
SetUpProvisioningProfile.prototype.runAsync = runAsync;
|
||||
|
||||
await action.runAsync(ctx);
|
||||
|
||||
+8
-8
@@ -16,16 +16,16 @@ import {
|
||||
import { MissingCredentialsNonInteractiveError } from '../../../errors';
|
||||
import { validateProvisioningProfileAsync } from '../../validators/validateProvisioningProfile';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { SetupProvisioningProfile } from '../SetupProvisioningProfile';
|
||||
import { SetUpProvisioningProfile } from '../SetUpProvisioningProfile';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
jest.mock('../SetupDistributionCertificate');
|
||||
jest.mock('../SetUpDistributionCertificate');
|
||||
jest.mock('../ConfigureProvisioningProfile');
|
||||
jest.mock('../CreateProvisioningProfile');
|
||||
jest.mock('../../validators/validateProvisioningProfile');
|
||||
|
||||
describe('SetupProvisioningProfile', () => {
|
||||
describe('SetUpProvisioningProfile', () => {
|
||||
it('repairs existing Provisioning Profile with bad build credentials in Interactive Mode', async () => {
|
||||
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
|
||||
const ctx = createCtxMock({
|
||||
@@ -54,7 +54,7 @@ describe('SetupProvisioningProfile', () => {
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupProvisioningProfileAction = new SetupProvisioningProfile(
|
||||
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore
|
||||
);
|
||||
@@ -84,7 +84,7 @@ describe('SetupProvisioningProfile', () => {
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupProvisioningProfileAction = new SetupProvisioningProfile(
|
||||
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore
|
||||
);
|
||||
@@ -114,7 +114,7 @@ describe('SetupProvisioningProfile', () => {
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupProvisioningProfileAction = new SetupProvisioningProfile(
|
||||
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore
|
||||
);
|
||||
@@ -140,7 +140,7 @@ describe('SetupProvisioningProfile', () => {
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupProvisioningProfileAction = new SetupProvisioningProfile(
|
||||
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore
|
||||
);
|
||||
@@ -157,7 +157,7 @@ describe('SetupProvisioningProfile', () => {
|
||||
nonInteractive: true,
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupProvisioningProfileAction = new SetupProvisioningProfile(
|
||||
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore
|
||||
);
|
||||
+6
-6
@@ -5,12 +5,12 @@ import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appsto
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getNewIosApiMock, testPushKey, testTargets } from '../../../__tests__/fixtures-ios';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { SetupPushKey } from '../SetupPushKey';
|
||||
import { SetUpPushKey } from '../SetUpPushKey';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
|
||||
describe(SetupPushKey, () => {
|
||||
describe(SetUpPushKey, () => {
|
||||
it('skips setting up a Push Key if it is already configured', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
@@ -20,7 +20,7 @@ describe(SetupPushKey, () => {
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupPushKeyAction = new SetupPushKey(appLookupParams);
|
||||
const setupPushKeyAction = new SetUpPushKey(appLookupParams);
|
||||
await setupPushKeyAction.runAsync(ctx);
|
||||
|
||||
// expect not to create a new push key on expo servers
|
||||
@@ -38,7 +38,7 @@ describe(SetupPushKey, () => {
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupPushKeyAction = new SetupPushKey(appLookupParams);
|
||||
const setupPushKeyAction = new SetUpPushKey(appLookupParams);
|
||||
await setupPushKeyAction.runAsync(ctx);
|
||||
|
||||
// expect to create a new push key on expo servers
|
||||
@@ -62,7 +62,7 @@ describe(SetupPushKey, () => {
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupPushKeyAction = new SetupPushKey(appLookupParams);
|
||||
const setupPushKeyAction = new SetUpPushKey(appLookupParams);
|
||||
await setupPushKeyAction.runAsync(ctx);
|
||||
|
||||
// expect not to create a new push key on expo servers, we are using the existing one
|
||||
@@ -75,7 +75,7 @@ describe(SetupPushKey, () => {
|
||||
nonInteractive: true,
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const createPushKeyAction = new SetupPushKey(appLookupParams);
|
||||
const createPushKeyAction = new SetUpPushKey(appLookupParams);
|
||||
await expect(createPushKeyAction.runAsync(ctx)).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { findApplicationTarget } from '../../../../project/ios/target';
|
||||
import { confirmAsync, promptAsync } from '../../../../prompts';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import {
|
||||
getNewIosApiMock,
|
||||
testCommonIosAppCredentialsFragment,
|
||||
testTargets,
|
||||
} from '../../../__tests__/fixtures-ios';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { SetUpAscApiKey } from '../SetUpAscApiKey';
|
||||
import {
|
||||
PROMPT_FOR_APP_SPECIFIC_PASSWORD,
|
||||
SetUpSubmissionCredentials,
|
||||
} from '../SetUpSubmissionCredentials';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
|
||||
describe(SetUpSubmissionCredentials, () => {
|
||||
it('allows user to enter an App Specific Password', async () => {
|
||||
asMock(promptAsync)
|
||||
.mockImplementationOnce(() => ({
|
||||
choice: PROMPT_FOR_APP_SPECIFIC_PASSWORD,
|
||||
}))
|
||||
.mockImplementationOnce(() => ({
|
||||
appSpecificPassword: 'super secret',
|
||||
}));
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
ios: {
|
||||
...getNewIosApiMock(),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupAscApiKeyAction = new SetUpSubmissionCredentials(appLookupParams);
|
||||
const asp = await setupAscApiKeyAction.runAsync(ctx);
|
||||
|
||||
// prompt to choose ASP, then prompt to input ASP
|
||||
expect(asMock(promptAsync).mock.calls.length).toBe(2);
|
||||
expect(asp).toBe('super secret');
|
||||
});
|
||||
it('returns an ASC API key', async () => {
|
||||
try {
|
||||
jest
|
||||
.spyOn(SetUpAscApiKey.prototype, 'runAsync')
|
||||
.mockImplementation(async _ctx => testCommonIosAppCredentialsFragment);
|
||||
const ctx = createCtxMock();
|
||||
const appLookupParams = getAppLookupParamsFromContext(
|
||||
ctx,
|
||||
findApplicationTarget(testTargets)
|
||||
);
|
||||
const setupAscApiKeyAction = new SetUpSubmissionCredentials(appLookupParams);
|
||||
const asc = await setupAscApiKeyAction.runAsync(ctx);
|
||||
|
||||
expect(asc).toEqual(testCommonIosAppCredentialsFragment);
|
||||
} finally {
|
||||
jest.restoreAllMocks();
|
||||
}
|
||||
});
|
||||
});
|
||||
+6
-6
@@ -15,7 +15,7 @@ import {
|
||||
} from '../../../__tests__/fixtures-ios';
|
||||
import { IosTargetCredentials } from '../../../credentialsJson/types';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { SetupTargetBuildCredentialsFromCredentialsJson } from '../SetupTargetBuildCredentialsFromCredentialsJson';
|
||||
import { SetUpTargetBuildCredentialsFromCredentialsJson } from '../SetUpTargetBuildCredentialsFromCredentialsJson';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
jest.mock('../../../credentialsJson/read');
|
||||
@@ -25,7 +25,7 @@ beforeEach(() => {
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
});
|
||||
|
||||
describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
|
||||
describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
|
||||
const targetCredentials: IosTargetCredentials = {
|
||||
distributionCertificate: {
|
||||
certificateP12: testDistCert.certP12,
|
||||
@@ -53,7 +53,7 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupBuildCredentialsFromCredentialsJsonAction =
|
||||
new SetupTargetBuildCredentialsFromCredentialsJson(
|
||||
new SetUpTargetBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore,
|
||||
targetCredentials
|
||||
@@ -82,7 +82,7 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupBuildCredentialsFromCredentialsJsonAction =
|
||||
new SetupTargetBuildCredentialsFromCredentialsJson(
|
||||
new SetUpTargetBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore,
|
||||
targetCredentials
|
||||
@@ -115,7 +115,7 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupBuildCredentialsFromCredentialsJsonAction =
|
||||
new SetupTargetBuildCredentialsFromCredentialsJson(
|
||||
new SetUpTargetBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore,
|
||||
targetCredentials
|
||||
@@ -149,7 +149,7 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupBuildCredentialsFromCredentialsJsonAction =
|
||||
new SetupTargetBuildCredentialsFromCredentialsJson(
|
||||
new SetUpTargetBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore,
|
||||
targetCredentials
|
||||
@@ -1,7 +1,10 @@
|
||||
import { UserRole } from '@expo/apple-utils';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import {
|
||||
AppFragment,
|
||||
AppStoreConnectApiKeyFragment,
|
||||
AppStoreConnectUserRole,
|
||||
AppleAppIdentifierFragment,
|
||||
AppleDeviceFragment,
|
||||
AppleDistributionCertificateFragment,
|
||||
@@ -14,7 +17,9 @@ import {
|
||||
import { isWildcardBundleIdentifier } from '../../../project/ios/bundleIdentifier';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { DistributionCertificate, PushKey } from '../appstore/Credentials.types';
|
||||
import { MinimalAscApiKey } from '../credentials';
|
||||
import { AppleTeamMissingError } from '../errors';
|
||||
import { AppStoreConnectApiKeyMutation } from './graphql/mutations/AppStoreConnectApiKeyMutation';
|
||||
import { AppleAppIdentifierMutation } from './graphql/mutations/AppleAppIdentifierMutation';
|
||||
import {
|
||||
AppleDistributionCertificateMutation,
|
||||
@@ -29,6 +34,7 @@ import { AppleTeamMutation } from './graphql/mutations/AppleTeamMutation';
|
||||
import { IosAppBuildCredentialsMutation } from './graphql/mutations/IosAppBuildCredentialsMutation';
|
||||
import { IosAppCredentialsMutation } from './graphql/mutations/IosAppCredentialsMutation';
|
||||
import { AppQuery } from './graphql/queries/AppQuery';
|
||||
import { AppStoreConnectApiKeyQuery } from './graphql/queries/AppStoreConnectApiKeyQuery';
|
||||
import { AppleAppIdentifierQuery } from './graphql/queries/AppleAppIdentifierQuery';
|
||||
import { AppleDeviceQuery } from './graphql/queries/AppleDeviceQuery';
|
||||
import { AppleDistributionCertificateQuery } from './graphql/queries/AppleDistributionCertificateQuery';
|
||||
@@ -161,8 +167,10 @@ export async function updateIosAppCredentialsAsync(
|
||||
appCredentials: CommonIosAppCredentialsFragment,
|
||||
{
|
||||
applePushKeyId,
|
||||
ascApiKeyIdForSubmissions,
|
||||
}: {
|
||||
applePushKeyId?: string;
|
||||
ascApiKeyIdForSubmissions?: string;
|
||||
}
|
||||
): Promise<CommonIosAppCredentialsFragment> {
|
||||
let updatedAppCredentials = appCredentials;
|
||||
@@ -172,6 +180,13 @@ export async function updateIosAppCredentialsAsync(
|
||||
applePushKeyId
|
||||
);
|
||||
}
|
||||
if (ascApiKeyIdForSubmissions) {
|
||||
updatedAppCredentials =
|
||||
await IosAppCredentialsMutation.setAppStoreConnectApiKeyForSubmissionsAsync(
|
||||
updatedAppCredentials.id,
|
||||
ascApiKeyIdForSubmissions
|
||||
);
|
||||
}
|
||||
return updatedAppCredentials;
|
||||
}
|
||||
|
||||
@@ -413,5 +428,78 @@ export async function deletePushKeyAsync(pushKeyId: string): Promise<void> {
|
||||
return await ApplePushKeyMutation.deleteApplePushKeyAsync(pushKeyId);
|
||||
}
|
||||
|
||||
export async function createAscApiKeyAsync(
|
||||
account: Account,
|
||||
ascApiKey: MinimalAscApiKey
|
||||
): Promise<AppStoreConnectApiKeyFragment> {
|
||||
const maybeAppleTeam = ascApiKey.teamId
|
||||
? await createOrGetExistingAppleTeamAsync(account, {
|
||||
appleTeamIdentifier: ascApiKey.teamId,
|
||||
appleTeamName: ascApiKey.teamName,
|
||||
})
|
||||
: null;
|
||||
return await AppStoreConnectApiKeyMutation.createAppStoreConnectApiKeyAsync(
|
||||
{
|
||||
issuerIdentifier: ascApiKey.issuerId,
|
||||
keyIdentifier: ascApiKey.keyId,
|
||||
keyP8: ascApiKey.keyP8,
|
||||
name: ascApiKey.name ?? null,
|
||||
roles: ascApiKey.roles?.map(role => convertUserRoleToGraphqlType(role)) ?? null,
|
||||
appleTeamId: maybeAppleTeam ? maybeAppleTeam.id : null,
|
||||
},
|
||||
account.id
|
||||
);
|
||||
}
|
||||
|
||||
export async function getAscApiKeysForAccountAsync(
|
||||
account: Account
|
||||
): Promise<AppStoreConnectApiKeyFragment[]> {
|
||||
return await AppStoreConnectApiKeyQuery.getAllForAccountAsync(account.name);
|
||||
}
|
||||
|
||||
export async function getAscApiKeyForAppSubmissionsAsync(
|
||||
appLookupParams: AppLookupParams
|
||||
): Promise<AppStoreConnectApiKeyFragment | null> {
|
||||
const maybeIosAppCredentials = await getIosAppCredentialsWithCommonFieldsAsync(appLookupParams);
|
||||
return maybeIosAppCredentials?.appStoreConnectApiKeyForSubmissions ?? null;
|
||||
}
|
||||
|
||||
export async function deleteAscApiKeyAsync(ascApiKeyId: string): Promise<void> {
|
||||
return await AppStoreConnectApiKeyMutation.deleteAppStoreConnectApiKeyAsync(ascApiKeyId);
|
||||
}
|
||||
|
||||
function convertUserRoleToGraphqlType(userRole: UserRole): AppStoreConnectUserRole {
|
||||
switch (userRole) {
|
||||
case UserRole.ADMIN:
|
||||
return AppStoreConnectUserRole.Admin;
|
||||
case UserRole.ACCESS_TO_REPORTS:
|
||||
return AppStoreConnectUserRole.AccessToReports;
|
||||
case UserRole.ACCOUNT_HOLDER:
|
||||
return AppStoreConnectUserRole.AccountHolder;
|
||||
case UserRole.APP_MANAGER:
|
||||
return AppStoreConnectUserRole.AppManager;
|
||||
case UserRole.CLOUD_MANAGED_APP_DISTRIBUTION:
|
||||
return AppStoreConnectUserRole.CloudManagedAppDistribution;
|
||||
case UserRole.CLOUD_MANAGED_DEVELOPER_ID:
|
||||
return AppStoreConnectUserRole.CloudManagedDeveloperId;
|
||||
case UserRole.CREATE_APPS:
|
||||
return AppStoreConnectUserRole.CreateApps;
|
||||
case UserRole.CUSTOMER_SUPPORT:
|
||||
return AppStoreConnectUserRole.CustomerSupport;
|
||||
case UserRole.DEVELOPER:
|
||||
return AppStoreConnectUserRole.Developer;
|
||||
case UserRole.FINANCE:
|
||||
return AppStoreConnectUserRole.Finance;
|
||||
case UserRole.MARKETING:
|
||||
return AppStoreConnectUserRole.Marketing;
|
||||
case UserRole.READ_ONLY:
|
||||
return AppStoreConnectUserRole.ReadOnly;
|
||||
case UserRole.SALES:
|
||||
return AppStoreConnectUserRole.Sales;
|
||||
case UserRole.TECHNICAL:
|
||||
return AppStoreConnectUserRole.Technical;
|
||||
}
|
||||
}
|
||||
|
||||
const formatProjectFullName = ({ account, projectName }: AppLookupParams): string =>
|
||||
`@${account.name}/${projectName}`;
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import {
|
||||
AppStoreConnectApiKeyFragment,
|
||||
AppStoreConnectApiKeyInput,
|
||||
CreateAppStoreConnectApiKeyMutation,
|
||||
DeleteAppStoreConnectApiKeyMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AppStoreConnectApiKeyFragmentNode } from '../../../../../graphql/types/credentials/AppStoreConnectApiKey';
|
||||
|
||||
export const AppStoreConnectApiKeyMutation = {
|
||||
async createAppStoreConnectApiKeyAsync(
|
||||
appStoreConnectApiKeyInput: AppStoreConnectApiKeyInput,
|
||||
accountId: string
|
||||
): Promise<AppStoreConnectApiKeyFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateAppStoreConnectApiKeyMutation>(
|
||||
gql`
|
||||
mutation CreateAppStoreConnectApiKeyMutation(
|
||||
$appStoreConnectApiKeyInput: AppStoreConnectApiKeyInput!
|
||||
$accountId: ID!
|
||||
) {
|
||||
appStoreConnectApiKey {
|
||||
createAppStoreConnectApiKey(
|
||||
appStoreConnectApiKeyInput: $appStoreConnectApiKeyInput
|
||||
accountId: $accountId
|
||||
) {
|
||||
id
|
||||
...AppStoreConnectApiKeyFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(AppStoreConnectApiKeyFragmentNode)}
|
||||
`,
|
||||
{
|
||||
appStoreConnectApiKeyInput,
|
||||
accountId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(
|
||||
data.appStoreConnectApiKey.createAppStoreConnectApiKey,
|
||||
'GraphQL: `createAppStoreConnectApiKey` not defined in server response'
|
||||
);
|
||||
return data.appStoreConnectApiKey.createAppStoreConnectApiKey;
|
||||
},
|
||||
async deleteAppStoreConnectApiKeyAsync(appStoreConnectApiKeyId: string): Promise<void> {
|
||||
await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<DeleteAppStoreConnectApiKeyMutation>(
|
||||
gql`
|
||||
mutation DeleteAppStoreConnectApiKeyMutation($appStoreConnectApiKeyId: ID!) {
|
||||
appStoreConnectApiKey {
|
||||
deleteAppStoreConnectApiKey(id: $appStoreConnectApiKeyId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
appStoreConnectApiKeyId,
|
||||
},
|
||||
{
|
||||
additionalTypenames: ['AppStoreConnectApiKey', 'IosAppCredentials'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
},
|
||||
};
|
||||
+34
@@ -7,6 +7,7 @@ import {
|
||||
CommonIosAppCredentialsFragment,
|
||||
CreateIosAppCredentialsMutation,
|
||||
IosAppCredentialsInput,
|
||||
SetAppStoreConnectApiKeyForSubmissionsMutation,
|
||||
SetPushKeyMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { CommonIosAppCredentialsFragmentNode } from '../../../../../graphql/types/credentials/IosAppCredentials';
|
||||
@@ -80,4 +81,37 @@ export const IosAppCredentialsMutation = {
|
||||
);
|
||||
return data.iosAppCredentials.setPushKey;
|
||||
},
|
||||
async setAppStoreConnectApiKeyForSubmissionsAsync(
|
||||
iosAppCredentialsId: string,
|
||||
ascApiKeyId: string
|
||||
): Promise<CommonIosAppCredentialsFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<SetAppStoreConnectApiKeyForSubmissionsMutation>(
|
||||
gql`
|
||||
mutation SetAppStoreConnectApiKeyForSubmissionsMutation(
|
||||
$iosAppCredentialsId: ID!
|
||||
$ascApiKeyId: ID!
|
||||
) {
|
||||
iosAppCredentials {
|
||||
setAppStoreConnectApiKeyForSubmissions(
|
||||
id: $iosAppCredentialsId
|
||||
ascApiKeyId: $ascApiKeyId
|
||||
) {
|
||||
id
|
||||
...CommonIosAppCredentialsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(CommonIosAppCredentialsFragmentNode)}
|
||||
`,
|
||||
{
|
||||
iosAppCredentialsId,
|
||||
ascApiKeyId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.iosAppCredentials.setAppStoreConnectApiKeyForSubmissions;
|
||||
},
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import {
|
||||
AppStoreConnectApiKeyByAccountQuery,
|
||||
AppStoreConnectApiKeyFragment,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AppStoreConnectApiKeyFragmentNode } from '../../../../../graphql/types/credentials/AppStoreConnectApiKey';
|
||||
|
||||
export const AppStoreConnectApiKeyQuery = {
|
||||
async getAllForAccountAsync(accountName: string): Promise<AppStoreConnectApiKeyFragment[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<AppStoreConnectApiKeyByAccountQuery>(
|
||||
gql`
|
||||
query AppStoreConnectApiKeyByAccountQuery($accountName: String!) {
|
||||
account {
|
||||
byName(accountName: $accountName) {
|
||||
id
|
||||
appStoreConnectApiKeys {
|
||||
id
|
||||
...AppStoreConnectApiKeyFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(AppStoreConnectApiKeyFragmentNode)}
|
||||
`,
|
||||
{
|
||||
accountName,
|
||||
},
|
||||
{
|
||||
additionalTypenames: ['AppStoreConnectApiKey'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.account.byName.appStoreConnectApiKeys;
|
||||
},
|
||||
};
|
||||
+15
@@ -30,6 +30,21 @@ export const IosAppCredentialsQuery = {
|
||||
},
|
||||
__typename: 'ApplePushKey',
|
||||
},
|
||||
appStoreConnectApiKeyForSubmissions: {
|
||||
id: 'b8fe2225-3024-4ebe-9e36-7b9022d3b13e',
|
||||
appleTeam: {
|
||||
id: 'ca27f9f6-c7cb-40d1-8c41-c168e8548261',
|
||||
appleTeamIdentifier: '77KQ969CHE',
|
||||
appleTeamName: null,
|
||||
__typename: 'AppleTeam',
|
||||
},
|
||||
issuerIdentifier: '69a6de93-e4e4-47e3-e053-5b8c7c11a4d1',
|
||||
keyIdentifier: 'NYN468XNN3',
|
||||
name: 'Expo Submissions pl94k8_lnS',
|
||||
roles: ['ADMIN'],
|
||||
updatedAt: '2020-10-13T18:39:04.463Z',
|
||||
__typename: 'AppStoreConnectApiKey',
|
||||
},
|
||||
iosAppBuildCredentialsList: [
|
||||
{
|
||||
id: '6f8d0175-0caf-4922-b09b-eae757b83144',
|
||||
|
||||
@@ -150,7 +150,7 @@ export default class AppStoreApi {
|
||||
return await listAscApiKeysAsync(ctx);
|
||||
}
|
||||
|
||||
public async getAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
|
||||
public async getAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo | null> {
|
||||
const ctx = await this.ensureAuthenticatedAsync();
|
||||
return await getAscApiKeyAsync(ctx, keyId);
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ export type AscApiKeyInfo = {
|
||||
name: string;
|
||||
teamName?: string;
|
||||
roles: UserRole[];
|
||||
isRevoked: boolean;
|
||||
};
|
||||
|
||||
export type AscApiKey = AscApiKeyInfo & {
|
||||
|
||||
@@ -47,6 +47,7 @@ const mockAscApiKeyInfo = {
|
||||
roles: ['ADMIN'],
|
||||
teamId: 'test-id',
|
||||
teamName: 'test-name',
|
||||
isRevoked: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -18,14 +18,22 @@ export async function listAscApiKeysAsync(authCtx: AuthCtx): Promise<AscApiKeyIn
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAscApiKeyAsync(authCtx: AuthCtx, keyId: string): Promise<AscApiKeyInfo> {
|
||||
export async function getAscApiKeyAsync(
|
||||
authCtx: AuthCtx,
|
||||
keyId: string
|
||||
): Promise<AscApiKeyInfo | null> {
|
||||
const spinner = ora(`Fetching App Store Connect API Key.`).start();
|
||||
try {
|
||||
const context = getRequestContext(authCtx);
|
||||
const apiKey = await ApiKey.infoAsync(context, { id: keyId });
|
||||
spinner.succeed(`Fetched App Store Connect API Key (ID: ${keyId}).`);
|
||||
return getAscApiKeyInfo(apiKey, authCtx);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
const message = error?.message ?? '';
|
||||
if (message.includes("There is no resource of type 'apiKeys' with id")) {
|
||||
spinner.stop();
|
||||
return null;
|
||||
}
|
||||
Log.error(error);
|
||||
spinner.fail(`Failed to fetch App Store Connect API Key.`);
|
||||
throw error;
|
||||
@@ -64,8 +72,11 @@ export async function createAscApiKeyAsync(
|
||||
}
|
||||
throw new Error(`Failed to download .p8 file of ${humanReadableKey}.`);
|
||||
}
|
||||
// this object has more optional parameters populated
|
||||
const fullKey = await ApiKey.infoAsync(context, { id: key.id });
|
||||
spinner.succeed(`Created App Store Connect API Key.`);
|
||||
return {
|
||||
...getAscApiKeyInfo(key, authCtx),
|
||||
...getAscApiKeyInfo(fullKey, authCtx),
|
||||
keyP8,
|
||||
};
|
||||
} catch (err: any) {
|
||||
@@ -100,5 +111,6 @@ export function getAscApiKeyInfo(apiKey: ApiKey, authCtx: AuthCtx): AscApiKeyInf
|
||||
teamId: authCtx.team.id,
|
||||
teamName: authCtx.team.name,
|
||||
roles: apiKey.attributes.roles,
|
||||
isRevoked: !!apiKey.attributes.revokingDate,
|
||||
};
|
||||
}
|
||||
@@ -1,41 +1,53 @@
|
||||
import { IOSConfig, compileModsAsync } from '@expo/config-plugins';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { JSONObject } from '@expo/json-file';
|
||||
import plist from '@expo/plist';
|
||||
import { getPrebuildConfig } from '@expo/prebuild-config';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
export async function getManagedEntitlementsJsonAsync(projectDir: string): Promise<JSONObject> {
|
||||
let { exp } = getPrebuildConfig(projectDir, { platforms: ['ios'] });
|
||||
import { readPlistAsync } from '../../../utils/plist';
|
||||
|
||||
exp = await compileModsAsync(exp, {
|
||||
projectRoot: projectDir,
|
||||
platforms: ['ios'],
|
||||
introspect: true,
|
||||
});
|
||||
return exp.ios?.entitlements || {};
|
||||
export async function getManagedEntitlementsJsonAsync(
|
||||
projectDir: string,
|
||||
env: Record<string, string>
|
||||
): Promise<JSONObject> {
|
||||
const originalProcessEnv: NodeJS.ProcessEnv = process.env;
|
||||
try {
|
||||
process.env = {
|
||||
...process.env,
|
||||
...env,
|
||||
};
|
||||
const { exp } = getPrebuildConfig(projectDir, { platforms: ['ios'] });
|
||||
|
||||
const expWithMods = await compileModsAsync(exp, {
|
||||
projectRoot: projectDir,
|
||||
platforms: ['ios'],
|
||||
introspect: true,
|
||||
});
|
||||
return expWithMods.ios?.entitlements || {};
|
||||
} finally {
|
||||
process.env = originalProcessEnv;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveEntitlementsJsonAsync(
|
||||
projectDir: string,
|
||||
workflow: Workflow
|
||||
workflow: Workflow,
|
||||
env: Record<string, string>
|
||||
): Promise<JSONObject> {
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
return (await getEntitlementsJsonAsync(projectDir)) || {};
|
||||
} else if (workflow === Workflow.MANAGED) {
|
||||
return await getManagedEntitlementsJsonAsync(projectDir);
|
||||
return await getManagedEntitlementsJsonAsync(projectDir, env);
|
||||
} else {
|
||||
throw new Error(`Unknown workflow: ${workflow}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function getEntitlementsJsonAsync(projectDir: string): Promise<JSONObject | null> {
|
||||
try {
|
||||
const entitlementsPath = IOSConfig.Paths.getEntitlementsPath(projectDir);
|
||||
if (entitlementsPath) {
|
||||
const entitlementsContents = await fs.readFile(entitlementsPath, 'utf8');
|
||||
return plist.parse(entitlementsContents);
|
||||
}
|
||||
} catch {}
|
||||
return null;
|
||||
const entitlementsPath = IOSConfig.Paths.getEntitlementsPath(projectDir);
|
||||
if (entitlementsPath) {
|
||||
const plist = await readPlistAsync(entitlementsPath);
|
||||
return plist ? (plist as unknown as JSONObject) : null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -118,7 +118,7 @@ export interface AscApiKeyPath {
|
||||
issuerId: string;
|
||||
}
|
||||
|
||||
export const ascApiKeyMetadataSchema: CredentialSchema<Omit<MinimalAscApiKey, 'keyP8'>> = {
|
||||
export const ascApiKeyIdSchema: CredentialSchema<Pick<MinimalAscApiKey, 'keyId'>> = {
|
||||
name: 'App Store Connect API Key',
|
||||
questions: [
|
||||
{
|
||||
@@ -126,6 +126,12 @@ export const ascApiKeyMetadataSchema: CredentialSchema<Omit<MinimalAscApiKey, 'k
|
||||
type: 'string',
|
||||
question: 'Key ID:',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const ascApiKeyIssuerIdSchema: CredentialSchema<Pick<MinimalAscApiKey, 'issuerId'>> = {
|
||||
name: 'App Store Connect API Key',
|
||||
questions: [
|
||||
{
|
||||
field: 'issuerId',
|
||||
type: 'string',
|
||||
|
||||
+50
-42
@@ -2,47 +2,55 @@
|
||||
|
||||
exports[`print credentials prints the IosAppCredentials map 1`] = `
|
||||
Array [
|
||||
"iOS Credentials
|
||||
Project @quinlanj/test52
|
||||
Bundle Identifier com.quinlanj.test52
|
||||
Apple Team test-apple-identifier
|
||||
|
||||
Push Key
|
||||
Developer Portal ID 9839M6AY8W
|
||||
Apple Team 77KQ969CHE
|
||||
Updated 6 months ago
|
||||
|
||||
|
||||
App Store Configuration
|
||||
|
||||
Distribution Certificate
|
||||
Serial Number test-serial
|
||||
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
|
||||
Apple Team test-apple-identifier
|
||||
Updated 6 months ago
|
||||
|
||||
Provisioning Profile
|
||||
Developer Portal ID test-profile-id
|
||||
Status active
|
||||
Expiration Wed, 13 Oct 2021 18:28:44 UTC
|
||||
Apple Team test-apple-identifier
|
||||
Updated 5 months ago
|
||||
|
||||
|
||||
Ad Hoc Configuration
|
||||
|
||||
Distribution Certificate
|
||||
Serial Number test-serial
|
||||
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
|
||||
Apple Team test-apple-identifier
|
||||
Updated 6 months ago
|
||||
|
||||
Provisioning Profile
|
||||
Developer Portal ID test-profile-id
|
||||
Status active
|
||||
Expiration Wed, 13 Oct 2021 18:28:44 UTC
|
||||
Apple Team test-apple-identifier
|
||||
Updated 5 months ago
|
||||
",
|
||||
"iOS Credentials
|
||||
Project @quinlanj/test52
|
||||
Bundle Identifier com.quinlanj.test52
|
||||
Apple Team test-apple-identifier
|
||||
|
||||
Push Key
|
||||
Developer Portal ID 9839M6AY8W
|
||||
Apple Team 77KQ969CHE
|
||||
Updated 6 months ago
|
||||
|
||||
App Store Connect API Key
|
||||
Developer Portal ID NYN468XNN3
|
||||
Name Expo Submissions pl94k8_lnS
|
||||
Issuer ID 69a6de93-e4e4-47e3-e053-5b8c7c11a4d1
|
||||
Roles ADMIN
|
||||
Apple Team 77KQ969CHE
|
||||
Updated 6 months ago
|
||||
|
||||
|
||||
App Store Configuration
|
||||
|
||||
Distribution Certificate
|
||||
Serial Number test-serial
|
||||
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
|
||||
Apple Team test-apple-identifier
|
||||
Updated 6 months ago
|
||||
|
||||
Provisioning Profile
|
||||
Developer Portal ID test-profile-id
|
||||
Status active
|
||||
Expiration Wed, 13 Oct 2021 18:28:44 UTC
|
||||
Apple Team test-apple-identifier
|
||||
Updated 5 months ago
|
||||
|
||||
|
||||
Ad Hoc Configuration
|
||||
|
||||
Distribution Certificate
|
||||
Serial Number test-serial
|
||||
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
|
||||
Apple Team test-apple-identifier
|
||||
Updated 6 months ago
|
||||
|
||||
Provisioning Profile
|
||||
Developer Portal ID test-profile-id
|
||||
Status active
|
||||
Expiration Wed, 13 Oct 2021 18:28:44 UTC
|
||||
Apple Team test-apple-identifier
|
||||
Updated 5 months ago
|
||||
",
|
||||
]
|
||||
`;
|
||||
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
||||
import dateformat from 'dateformat';
|
||||
|
||||
import {
|
||||
AppStoreConnectApiKeyFragment,
|
||||
AppleDeviceFragment,
|
||||
ApplePushKeyFragment,
|
||||
IosAppBuildCredentialsFragment,
|
||||
@@ -91,7 +92,7 @@ export function displayIosCredentials(
|
||||
continue;
|
||||
}
|
||||
|
||||
const { appleTeam, pushKey } = targetAppCredentials;
|
||||
const { appleTeam, pushKey, appStoreConnectApiKeyForSubmissions } = targetAppCredentials;
|
||||
if (appleTeam) {
|
||||
const { appleTeamIdentifier, appleTeamName } = appleTeam;
|
||||
fields.push({
|
||||
@@ -105,6 +106,10 @@ export function displayIosCredentials(
|
||||
displayApplePushKey(pushKey, fields);
|
||||
}
|
||||
|
||||
if (appStoreConnectApiKeyForSubmissions) {
|
||||
displayAscApiKey(appStoreConnectApiKeyForSubmissions, fields);
|
||||
}
|
||||
|
||||
const sortedIosAppBuildCredentialsList = sortBuildCredentialsByDistributionType(
|
||||
targetAppCredentials.iosAppBuildCredentialsList
|
||||
);
|
||||
@@ -230,6 +235,35 @@ function displayApplePushKey(
|
||||
fields.push({ label: '', value: '' });
|
||||
}
|
||||
|
||||
function displayAscApiKey(
|
||||
maybeAscApiKey: AppStoreConnectApiKeyFragment | null,
|
||||
fields: { label: string; value: string }[]
|
||||
): void {
|
||||
fields.push({ label: 'App Store Connect API Key', value: '' });
|
||||
if (maybeAscApiKey) {
|
||||
const { keyIdentifier, issuerIdentifier, appleTeam, name, roles, updatedAt } = maybeAscApiKey;
|
||||
fields.push({ label: 'Developer Portal ID', value: keyIdentifier });
|
||||
if (name) {
|
||||
fields.push({ label: 'Name', value: name });
|
||||
}
|
||||
fields.push({ label: 'Issuer ID', value: issuerIdentifier });
|
||||
if (roles) {
|
||||
fields.push({ label: 'Roles', value: roles.join(',') });
|
||||
}
|
||||
if (appleTeam) {
|
||||
const { appleTeamIdentifier, appleTeamName } = appleTeam;
|
||||
fields.push({
|
||||
label: 'Apple Team',
|
||||
value: `${appleTeamIdentifier} ${appleTeamName ? `(${appleTeamName})` : ''}`,
|
||||
});
|
||||
}
|
||||
fields.push({ label: 'Updated', value: `${fromNow(new Date(updatedAt))} ago` });
|
||||
} else {
|
||||
fields.push({ label: '', value: 'None assigned yet' });
|
||||
}
|
||||
fields.push({ label: '', value: '' });
|
||||
}
|
||||
|
||||
function formatAppleDevice(device: AppleDeviceFragment): string {
|
||||
let deviceString = '';
|
||||
if (device.name) {
|
||||
|
||||
Loaded 100 of 155 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user