mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71d48c3cf2 | ||
|
|
7343e81065 | ||
|
|
1f1310bef6 | ||
|
|
27c69ec8e1 | ||
|
|
b81b896bc6 | ||
|
|
ef79db2ca5 | ||
|
|
88565eec62 | ||
|
|
e8a520e55f | ||
|
|
9a33f7bd93 | ||
|
|
23c70e237b | ||
|
|
7fcf41db78 | ||
|
|
36f4a4bd38 | ||
|
|
87541db80d | ||
|
|
80112c682f | ||
|
|
eb29d3dadb | ||
|
|
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 |
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
|
||||
|
||||
+14
-10
@@ -1,14 +1,18 @@
|
||||
# Build directories
|
||||
packages/*/build
|
||||
packages/eas-cli/dist
|
||||
|
||||
# Code editors
|
||||
.idea
|
||||
.history
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Node
|
||||
node_modules
|
||||
*-debug.log
|
||||
*-error.log
|
||||
|
||||
**/build/**
|
||||
!packages/eas-cli/src/commands/build/**
|
||||
!packages/eas-cli/src/build/**
|
||||
|
||||
dist
|
||||
/package-lock.json
|
||||
tmp
|
||||
node_modules
|
||||
# Tests
|
||||
coverage
|
||||
.history
|
||||
.DS_Store
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"jsxBracketSameLine": true,
|
||||
"bracketSameLine": true,
|
||||
"trailingComma": "es5",
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
+99
-22
@@ -8,24 +8,101 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add ASC Api Key generation workflow. ([#718](https://github.com/expo/eas-cli/pull/718) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add support for removal of ASC Api Keys. ([#721](https://github.com/expo/eas-cli/pull/721) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Allow users to assign an ASC Api Key to their project. ([#719](https://github.com/expo/eas-cli/pull/719) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add setup support for ASC Api Keys. ([#733](https://github.com/expo/eas-cli/pull/733) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Show initiating user display name when selecting a build to submit. ([#730](https://github.com/expo/eas-cli/pull/730) by [@barthap](https://github.com/barthap))
|
||||
- Handle Apple servers maintenance error in `eas submit`. ([#738](https://github.com/expo/eas-cli/pull/738) by [@barthap](https://github.com/barthap))
|
||||
- Integrate ASC Api Key with submissions workflow. ([#737](https://github.com/expo/eas-cli/pull/737) by [@quinlanj](https://github.com/quinlanj))
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix building iOS projects with whitespace in the project name. ([#792](https://github.com/expo/eas-cli/pull/792) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [0.38.0](https://github.com/expo/eas-cli/releases/tag/v0.38.0) - 2021-11-24
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Improve unknown capability syncing error message. ([#775](https://github.com/expo/eas-cli/pulls/775) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add submit webhooks. ([#777](https://github.com/expo/eas-cli/pull/777) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Compute runtime version policies. ([#783](https://github.com/expo/eas-cli/pull/783), [#785](https://github.com/expo/eas-cli/pull/785) by [@jkhales](https://github.com/jkhales))
|
||||
- Fix local builds with npm < 7. ([#787](https://github.com/expo/eas-cli/pull/787) by [@dsokal](https://github.com/dsokal))
|
||||
- Override `applicationId`/`bundleIdentifier` when auto-submitting after build. ([#780](https://github.com/expo/eas-cli/pull/780) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade `typescript` to 4.5.2. Upgrade oclif dependencies. ([#781](https://github.com/expo/eas-cli/pull/781) by [@dsokal](https://github.com/dsokal))
|
||||
- Make missing `git` command error message more descriptive. ([#784](https://github.com/expo/eas-cli/pull/784) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.37.0](https://github.com/expo/eas-cli/releases/tag/v0.37.0) - 2021-11-18
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- 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))
|
||||
- Bump `@expo/apple-utils` to 0.0.0-alpha.26. ([#723](https://github.com/expo/eas-cli/pull/723) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Grammar: replace setup with set up. ([#735](https://github.com/expo/eas-cli/pull/735) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Improve VCS workflow migration experience. ([#732](https://github.com/expo/eas-cli/pull/732) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.34.1](https://github.com/expo/eas-cli/releases/tag/0.34.1) - 2021-11-02
|
||||
## [0.34.1](https://github.com/expo/eas-cli/releases/tag/v0.34.1) - 2021-11-02
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
@@ -33,16 +110,16 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add App Store Connect Api Key graphql type and print support. ([#717](https://github.com/expo/eas-cli/pull/717) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add App Store Connect API Key graphql type and print support. ([#717](https://github.com/expo/eas-cli/pull/717) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.34.0](https://github.com/expo/eas-cli/releases/tag/0.34.0) - 2021-11-01
|
||||
## [0.34.0](https://github.com/expo/eas-cli/releases/tag/v0.34.0) - 2021-11-01
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- `[EAS BUILD API]` Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701) and [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal))
|
||||
- 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))
|
||||
- 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
|
||||
|
||||
@@ -57,7 +134,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
|
||||
|
||||
@@ -67,7 +144,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
|
||||
|
||||
@@ -90,11 +167,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))
|
||||
|
||||
@@ -104,16 +181,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
|
||||
|
||||
@@ -124,7 +201,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
|
||||
|
||||
@@ -503,7 +580,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/commits) for that version. In most cases, it will be the latest `Publish` commit.
|
||||
- Find the `eas-cli-local-build-plugin` version published in that commit.
|
||||

|
||||
- Run `npm dist-tags add eas-cli-local-build-plugin@VERSION latest` with the version from the previous step.
|
||||
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.1"
|
||||
"version": "0.38.1"
|
||||
}
|
||||
+7
-5
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"private": true,
|
||||
"workspaces": ["packages/*"],
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "lerna run build",
|
||||
"build:tarballs:linux": "yarn build && yarn workspace eas-cli oclif-dev pack --targets linux-x64",
|
||||
@@ -8,8 +10,8 @@
|
||||
"start": "lerna run watch --parallel",
|
||||
"watch": "yarn start",
|
||||
"eas": "packages/eas-cli/bin/run",
|
||||
"lint": "eslint . --ext .ts --ext .js",
|
||||
"release": "lerna version",
|
||||
"lint": "eslint .",
|
||||
"release": "./scripts/bin/run update-local-plugin && lerna version",
|
||||
"test": "jest",
|
||||
"clean": "lerna run clean && rm -rf node_modules coverage"
|
||||
},
|
||||
@@ -24,9 +26,9 @@
|
||||
"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"
|
||||
"typescript": "4.5.2"
|
||||
},
|
||||
"volta": {
|
||||
"node": "16.10.0",
|
||||
|
||||
+38
-39
@@ -73,7 +73,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/login.js)_
|
||||
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/account/login.js)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -87,7 +87,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/logout.js)_
|
||||
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/account/logout.js)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -101,7 +101,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/view.js)_
|
||||
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/account/view.js)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -112,7 +112,7 @@ USAGE
|
||||
$ eas analytics [STATUS]
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/analytics.js)_
|
||||
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/analytics.js)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -146,7 +146,7 @@ OPTIONS
|
||||
--[no-]wait Wait for build(s) to complete
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/index.js)_
|
||||
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/build/index.js)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -157,7 +157,7 @@ USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/cancel.js)_
|
||||
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/build/cancel.js)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -171,7 +171,7 @@ OPTIONS
|
||||
-p, --platform=(android|ios|all) Platform to configure
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/configure.js)_
|
||||
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/build/configure.js)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -204,7 +204,7 @@ OPTIONS
|
||||
--status=(new|in-queue|in-progress|errored|finished|canceled)
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/list.js)_
|
||||
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/build/list.js)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -218,7 +218,7 @@ OPTIONS
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/view.js)_
|
||||
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/build/view.js)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -233,7 +233,7 @@ OPTIONS
|
||||
--profile=profile
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/config.js)_
|
||||
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/config.js)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -244,7 +244,7 @@ USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/credentials.js)_
|
||||
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/credentials.js)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -255,7 +255,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/create.js)_
|
||||
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/device/create.js)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -269,7 +269,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/list.js)_
|
||||
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/device/list.js)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -280,7 +280,7 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/view.js)_
|
||||
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/device/view.js)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -291,7 +291,7 @@ USAGE
|
||||
$ eas diagnostics
|
||||
```
|
||||
|
||||
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/diagnostics.js)_
|
||||
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/diagnostics.js)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -308,7 +308,7 @@ OPTIONS
|
||||
--all see all commands in CLI
|
||||
```
|
||||
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.3/src/commands/help.ts)_
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.5/src/commands/help.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -319,7 +319,7 @@ USAGE
|
||||
$ eas project:info
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/project/info.js)_
|
||||
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/project/info.js)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -333,7 +333,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/project/init.js)_
|
||||
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/project/init.js)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
@@ -350,7 +350,7 @@ OPTIONS
|
||||
--value=value Value of the secret
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/create.js)_
|
||||
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/secret/create.js)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -367,7 +367,7 @@ DESCRIPTION
|
||||
Unsure where to find the secret's ID? Run eas secret:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/delete.js)_
|
||||
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/secret/delete.js)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -378,7 +378,7 @@ USAGE
|
||||
$ eas secret:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/list.js)_
|
||||
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/secret/list.js)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -405,14 +405,13 @@ OPTIONS
|
||||
--[no-]wait Wait for submission to complete
|
||||
|
||||
DESCRIPTION
|
||||
See how to configure submits with eas.json:
|
||||
https://docs.expo.dev/submit/eas-json/
|
||||
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
|
||||
|
||||
ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/submit.js)_
|
||||
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/submit.js)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -423,15 +422,15 @@ USAGE
|
||||
$ eas webhook:create
|
||||
|
||||
OPTIONS
|
||||
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
|
||||
--event=(BUILD|SUBMIT) 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.
|
||||
--secret=secret Secret used to create a hash signature of the request payload, provided in the
|
||||
'Expo-Signature' header.
|
||||
|
||||
--url=url Webhook URL
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/create.js)_
|
||||
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/webhook/create.js)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -445,7 +444,7 @@ ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/delete.js)_
|
||||
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/webhook/delete.js)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -456,10 +455,10 @@ USAGE
|
||||
$ eas webhook:list
|
||||
|
||||
OPTIONS
|
||||
--event=(BUILD) Event type that triggers the webhook
|
||||
--event=(BUILD|SUBMIT) Event type that triggers the webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/list.js)_
|
||||
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/webhook/list.js)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -470,16 +469,16 @@ USAGE
|
||||
$ eas webhook:update
|
||||
|
||||
OPTIONS
|
||||
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
|
||||
--id=id (required) Webhook ID
|
||||
--event=(BUILD|SUBMIT) 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.
|
||||
--secret=secret Secret used to create a hash signature of the request payload, provided in the
|
||||
'Expo-Signature' header.
|
||||
|
||||
--url=url Webhook URL
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/update.js)_
|
||||
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.38.1/packages/eas-cli/src/commands/webhook/update.js)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -493,5 +492,5 @@ ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/view.js)_
|
||||
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.38.1/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
+775
-184
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.34.1",
|
||||
"version": "0.38.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -9,24 +9,23 @@
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "0.0.0-alpha.26",
|
||||
"@expo/config": "6.0.3",
|
||||
"@expo/config-plugins": "4.0.3",
|
||||
"@expo/config": "6.0.8",
|
||||
"@expo/config-plugins": "4.0.8",
|
||||
"@expo/eas-build-job": "0.2.57",
|
||||
"@expo/eas-json": "^0.34.1",
|
||||
"@expo/json-file": "8.2.33",
|
||||
"@expo/pkcs12": "0.0.4",
|
||||
"@expo/plist": "0.0.14",
|
||||
"@expo/eas-json": "^0.38.0",
|
||||
"@expo/json-file": "8.2.34",
|
||||
"@expo/pkcs12": "0.0.6",
|
||||
"@expo/plist": "0.0.16",
|
||||
"@expo/plugin-warn-if-update-available": "1.7.0",
|
||||
"@expo/prebuild-config": "3.0.3",
|
||||
"@expo/prebuild-config": "3.0.8",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.0",
|
||||
"@expo/sdk-runtime-versions": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.5.0",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/command": "1.8.0",
|
||||
"@oclif/config": "1.17.0",
|
||||
"@oclif/command": "1.8.3",
|
||||
"@oclif/config": "1.17.1",
|
||||
"@oclif/errors": "1.3.5",
|
||||
"@oclif/plugin-help": "3.2.3",
|
||||
"@oclif/plugin-help": "3.2.5",
|
||||
"@urql/core": "2.3.1",
|
||||
"@urql/exchange-retry": "0.3.0",
|
||||
"chalk": "4.1.2",
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
getApplicationIdAsync,
|
||||
} from '../../project/android/applicationId';
|
||||
import { GradleBuildContext, resolveGradleBuildContextAsync } from '../../project/android/gradle';
|
||||
import { resolveGradleBuildContextAsync } from '../../project/android/gradle';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import {
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
JobData,
|
||||
prepareBuildRequestForPlatformAsync,
|
||||
} from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
@@ -28,9 +28,9 @@ import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
|
||||
export async function prepareAndroidBuildAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>
|
||||
): Promise<BuildRequestSender> {
|
||||
export async function createAndroidContextAsync(
|
||||
ctx: CommonContext<Platform.ANDROID>
|
||||
): Promise<AndroidBuildContext> {
|
||||
const { buildProfile } = ctx;
|
||||
|
||||
if (buildProfile.distribution === 'internal' && buildProfile.gradleCommand?.match(/bundle/)) {
|
||||
@@ -58,21 +58,26 @@ This means that it will most likely produce an AAB and you will not be able to i
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
|
||||
const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext);
|
||||
|
||||
return { applicationId, gradleContext };
|
||||
}
|
||||
|
||||
export async function prepareAndroidBuildAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>
|
||||
): Promise<BuildRequestSender> {
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx,
|
||||
ensureCredentialsAsync: async (ctx: BuildContext<Platform.ANDROID>) => {
|
||||
return await ensureAndroidCredentialsAsync(ctx, gradleContext);
|
||||
return await ensureAndroidCredentialsAsync(ctx);
|
||||
},
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
await validateAndSyncProjectConfigurationAsync({
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
buildProfile,
|
||||
buildProfile: ctx.buildProfile,
|
||||
});
|
||||
},
|
||||
getMetadataContext: () => ({
|
||||
gradleContext,
|
||||
}),
|
||||
prepareJobAsync: async (
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData<AndroidCredentials>
|
||||
@@ -100,8 +105,7 @@ function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean
|
||||
}
|
||||
|
||||
async function ensureAndroidCredentialsAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
gradleContext?: GradleBuildContext
|
||||
ctx: BuildContext<Platform.ANDROID>
|
||||
): Promise<CredentialsResult<AndroidCredentials> | undefined> {
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
return;
|
||||
@@ -109,7 +113,7 @@ async function ensureAndroidCredentialsAsync(
|
||||
const androidApplicationIdentifier = await getApplicationIdAsync(
|
||||
ctx.projectDir,
|
||||
ctx.exp,
|
||||
gradleContext
|
||||
ctx.android.gradleContext
|
||||
);
|
||||
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
|
||||
app: {
|
||||
|
||||
@@ -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';
|
||||
@@ -16,9 +18,7 @@ import { sleepAsync } from '../utils/promise';
|
||||
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 { collectMetadataAsync } from './metadata';
|
||||
import { printDeprecationWarnings } from './utils/printBuildInfo';
|
||||
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
||||
|
||||
@@ -39,7 +39,6 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
|
||||
ctx: BuildContext<TPlatform>
|
||||
): Promise<CredentialsResult<Credentials> | undefined>;
|
||||
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
|
||||
getMetadataContext: () => MetadataContext<TPlatform>;
|
||||
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
|
||||
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
|
||||
}
|
||||
@@ -55,15 +54,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,
|
||||
});
|
||||
}
|
||||
@@ -86,8 +87,7 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
bucketKey: await uploadProjectAsync(ctx),
|
||||
} as const);
|
||||
|
||||
const metadataContext = builder.getMetadataContext();
|
||||
const metadata = await collectMetadataAsync(ctx, metadataContext);
|
||||
const metadata = await collectMetadataAsync(ctx);
|
||||
const job = await builder.prepareJobAsync(ctx, {
|
||||
projectArchive,
|
||||
credentials: credentialsResult?.credentials,
|
||||
@@ -166,8 +166,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 +201,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 } = {}
|
||||
|
||||
@@ -58,7 +58,6 @@ export async function configureAsync(options: {
|
||||
platform: RequestedPlatform;
|
||||
projectDir: string;
|
||||
}): Promise<void> {
|
||||
await getVcsClient().ensureRepoExistsAsync();
|
||||
await maybeBailOnRepoStatusAsync();
|
||||
|
||||
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile, BuildProfile, IosBuildProfile } from '@expo/eas-json';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import { TrackingContext } from '../analytics/common';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { Target } from '../credentials/ios/types';
|
||||
import { RequestedPlatform } from '../platform';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { findAccountByName } from '../user/Account';
|
||||
import { GradleBuildContext } from '../project/android/gradle';
|
||||
import { XcodeBuildContext } from '../project/ios/scheme';
|
||||
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;
|
||||
@@ -27,6 +22,21 @@ export interface ConfigureContext {
|
||||
hasIosNativeProject: boolean;
|
||||
}
|
||||
|
||||
export type CommonContext<T extends Platform> = Omit<BuildContext<T>, 'android' | 'ios'>;
|
||||
|
||||
export interface AndroidBuildContext {
|
||||
applicationId: string;
|
||||
gradleContext?: GradleBuildContext;
|
||||
}
|
||||
|
||||
export interface IosBuildContext {
|
||||
bundleIdentifier: string;
|
||||
applicationTargetBuildSettings: XCBuildConfiguration['buildSettings'];
|
||||
applicationTarget: Target;
|
||||
targets: Target[];
|
||||
xcodeBuildContext: XcodeBuildContext;
|
||||
}
|
||||
|
||||
export interface BuildContext<T extends Platform> {
|
||||
accountName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
@@ -44,112 +54,6 @@ export interface BuildContext<T extends Platform> {
|
||||
trackingCtx: TrackingContext;
|
||||
user: Actor;
|
||||
workflow: Workflow;
|
||||
}
|
||||
|
||||
export async function createBuildContextAsync<T extends Platform>({
|
||||
buildProfileName,
|
||||
buildProfile,
|
||||
clearCache = false,
|
||||
local,
|
||||
nonInteractive = false,
|
||||
platform,
|
||||
projectDir,
|
||||
skipProjectConfiguration = false,
|
||||
}: {
|
||||
buildProfileName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
clearCache: boolean;
|
||||
local: boolean;
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
skipProjectConfiguration: boolean;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
|
||||
|
||||
const user = await ensureLoggedInAsync();
|
||||
const accountName = getProjectAccountName(exp, user);
|
||||
const projectName = exp.slug;
|
||||
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
|
||||
const workflow = await resolveWorkflowAsync(projectDir, platform);
|
||||
const accountId = findAccountByName(user.accounts, accountName)?.id;
|
||||
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
exp,
|
||||
nonInteractive,
|
||||
projectDir,
|
||||
user,
|
||||
});
|
||||
|
||||
const devClientProperties = getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
});
|
||||
const trackingCtx = {
|
||||
tracking_id: uuidv4(),
|
||||
platform,
|
||||
...(accountId && { account_id: accountId }),
|
||||
account_name: accountName,
|
||||
project_id: projectId,
|
||||
project_type: workflow,
|
||||
...devClientProperties,
|
||||
};
|
||||
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
|
||||
|
||||
return {
|
||||
accountName,
|
||||
buildProfile,
|
||||
buildProfileName,
|
||||
clearCache,
|
||||
credentialsCtx,
|
||||
exp,
|
||||
local,
|
||||
nonInteractive,
|
||||
platform,
|
||||
projectDir,
|
||||
projectId,
|
||||
projectName,
|
||||
skipProjectConfiguration,
|
||||
trackingCtx,
|
||||
user,
|
||||
workflow,
|
||||
};
|
||||
}
|
||||
|
||||
function getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
}: {
|
||||
platform: Platform;
|
||||
projectDir: string;
|
||||
buildProfile: AndroidBuildProfile | IosBuildProfile;
|
||||
}): Partial<TrackingContext> {
|
||||
let includesDevClient;
|
||||
const version = tryGetDevClientVersion(projectDir);
|
||||
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
|
||||
} else if (platform === Platform.IOS && 'buildConfiguration' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.buildConfiguration === 'Debug');
|
||||
} else if (buildProfile.developmentClient) {
|
||||
includesDevClient = true;
|
||||
} else {
|
||||
includesDevClient = false;
|
||||
}
|
||||
|
||||
if (version) {
|
||||
return { dev_client: includesDevClient, dev_client_version: version };
|
||||
} else {
|
||||
return { dev_client: includesDevClient };
|
||||
}
|
||||
}
|
||||
|
||||
function tryGetDevClientVersion(projectDir: string): string | null {
|
||||
try {
|
||||
const pkg = JsonFile.read(resolveFrom(projectDir, 'expo-dev-client/package.json'));
|
||||
return pkg.version?.toString() ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
|
||||
ios: T extends Platform.IOS ? IosBuildContext : undefined;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile, BuildProfile, IosBuildProfile } from '@expo/eas-json';
|
||||
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 { getExpoConfig } from '../project/expoConfig';
|
||||
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { findAccountByName } from '../user/Account';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { createAndroidContextAsync } from './android/build';
|
||||
import { BuildContext, CommonContext } from './context';
|
||||
import { createIosContextAsync } from './ios/build';
|
||||
|
||||
export async function createBuildContextAsync<T extends Platform>({
|
||||
buildProfileName,
|
||||
buildProfile,
|
||||
clearCache = false,
|
||||
local,
|
||||
nonInteractive = false,
|
||||
platform,
|
||||
projectDir,
|
||||
skipProjectConfiguration = false,
|
||||
}: {
|
||||
buildProfileName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
clearCache: boolean;
|
||||
local: boolean;
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
skipProjectConfiguration: boolean;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
|
||||
|
||||
const user = await ensureLoggedInAsync();
|
||||
const accountName = getProjectAccountName(exp, user);
|
||||
const projectName = exp.slug;
|
||||
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
|
||||
const workflow = await resolveWorkflowAsync(projectDir, platform);
|
||||
const accountId = findAccountByName(user.accounts, accountName)?.id;
|
||||
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
exp,
|
||||
nonInteractive,
|
||||
projectDir,
|
||||
user,
|
||||
});
|
||||
|
||||
const devClientProperties = getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
});
|
||||
const trackingCtx = {
|
||||
tracking_id: uuidv4(),
|
||||
platform,
|
||||
...(accountId && { account_id: accountId }),
|
||||
account_name: accountName,
|
||||
project_id: projectId,
|
||||
project_type: workflow,
|
||||
...devClientProperties,
|
||||
};
|
||||
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
|
||||
|
||||
const commonContext: CommonContext<T> = {
|
||||
accountName,
|
||||
buildProfile,
|
||||
buildProfileName,
|
||||
clearCache,
|
||||
credentialsCtx,
|
||||
exp,
|
||||
local,
|
||||
nonInteractive,
|
||||
platform,
|
||||
projectDir,
|
||||
projectId,
|
||||
projectName,
|
||||
skipProjectConfiguration,
|
||||
trackingCtx,
|
||||
user,
|
||||
workflow,
|
||||
};
|
||||
if (platform === Platform.ANDROID) {
|
||||
const common = commonContext as CommonContext<Platform.ANDROID>;
|
||||
return {
|
||||
...common,
|
||||
android: await createAndroidContextAsync(common),
|
||||
} as BuildContext<T>;
|
||||
} else {
|
||||
const common = commonContext as CommonContext<Platform.IOS>;
|
||||
return {
|
||||
...common,
|
||||
ios: await createIosContextAsync(common),
|
||||
} as BuildContext<T>;
|
||||
}
|
||||
}
|
||||
|
||||
function getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
}: {
|
||||
platform: Platform;
|
||||
projectDir: string;
|
||||
buildProfile: AndroidBuildProfile | IosBuildProfile;
|
||||
}): Partial<TrackingContext> {
|
||||
let includesDevClient;
|
||||
const version = tryGetDevClientVersion(projectDir);
|
||||
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
|
||||
} else if (platform === Platform.IOS && 'buildConfiguration' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.buildConfiguration === 'Debug');
|
||||
} else if (buildProfile.developmentClient) {
|
||||
includesDevClient = true;
|
||||
} else {
|
||||
includesDevClient = false;
|
||||
}
|
||||
|
||||
if (version) {
|
||||
return { dev_client: includesDevClient, dev_client_version: version };
|
||||
} else {
|
||||
return { dev_client: includesDevClient };
|
||||
}
|
||||
}
|
||||
|
||||
function tryGetDevClientVersion(projectDir: string): string | null {
|
||||
try {
|
||||
const pkg = JsonFile.read(resolveFrom(projectDir, 'expo-dev-client/package.json'));
|
||||
return pkg.version?.toString() ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -8,24 +8,17 @@ import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../pro
|
||||
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
|
||||
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { BuildContext, CommonContext, IosBuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { IosMetadataContext } from '../metadata';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { ensureIosCredentialsAsync } from './credentials';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
|
||||
interface BuildConfiguration {
|
||||
targetName?: string;
|
||||
buildConfiguration?: string;
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}
|
||||
|
||||
export async function prepareIosBuildAsync(
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
): Promise<BuildRequestSender> {
|
||||
export async function createIosContextAsync(
|
||||
ctx: CommonContext<Platform.IOS>
|
||||
): Promise<IosBuildContext> {
|
||||
const { buildProfile } = ctx;
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
@@ -50,29 +43,42 @@ export async function prepareIosBuildAsync(
|
||||
},
|
||||
xcodeBuildContext
|
||||
);
|
||||
const buildConfiguration = resolveBuildConfiguration(ctx, targets);
|
||||
const applicationTarget = findApplicationTarget(targets);
|
||||
const applicationTargetBuildSettings = resolveBuildSettings(ctx, applicationTarget);
|
||||
|
||||
return {
|
||||
bundleIdentifier: applicationTarget.bundleIdentifier,
|
||||
applicationTarget,
|
||||
applicationTargetBuildSettings,
|
||||
targets,
|
||||
xcodeBuildContext,
|
||||
};
|
||||
}
|
||||
|
||||
export async function prepareIosBuildAsync(
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
): Promise<BuildRequestSender> {
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx,
|
||||
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
|
||||
return ensureIosCredentialsAsync(ctx, targets);
|
||||
return ensureIosCredentialsAsync(ctx, ctx.ios.targets);
|
||||
},
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
await validateAndSyncProjectConfigurationAsync({
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
buildProfile,
|
||||
buildSettings: buildConfiguration.buildSettings,
|
||||
buildProfile: ctx.buildProfile,
|
||||
buildSettings: ctx.ios.applicationTargetBuildSettings,
|
||||
});
|
||||
},
|
||||
getMetadataContext: (): IosMetadataContext => {
|
||||
return buildConfiguration;
|
||||
},
|
||||
prepareJobAsync: async (
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData<IosCredentials>
|
||||
): Promise<Job> => {
|
||||
return await prepareJobAsync(ctx, { ...jobData, buildScheme: xcodeBuildContext.buildScheme });
|
||||
return await prepareJobAsync(ctx, {
|
||||
...jobData,
|
||||
buildScheme: ctx.ios.xcodeBuildContext.buildScheme,
|
||||
});
|
||||
},
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
@@ -90,23 +96,17 @@ export async function prepareIosBuildAsync(
|
||||
});
|
||||
}
|
||||
|
||||
function resolveBuildConfiguration(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
targets: Target[]
|
||||
): BuildConfiguration {
|
||||
function resolveBuildSettings(
|
||||
ctx: CommonContext<Platform.IOS>,
|
||||
applicationTarget: Target
|
||||
): XCBuildConfiguration['buildSettings'] {
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
return { buildSettings: {} };
|
||||
return {};
|
||||
}
|
||||
const applicationTarget = findApplicationTarget(targets);
|
||||
const project = IOSConfig.XcodeUtils.getPbxproj(ctx.projectDir);
|
||||
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
|
||||
targetName: applicationTarget.targetName,
|
||||
buildConfiguration: applicationTarget.buildConfiguration,
|
||||
});
|
||||
const buildSettings = xcBuildConfiguration?.buildSettings ?? {};
|
||||
return {
|
||||
buildSettings,
|
||||
targetName: applicationTarget.targetName,
|
||||
buildConfiguration: applicationTarget.buildConfiguration,
|
||||
};
|
||||
return xcBuildConfiguration?.buildSettings ?? {};
|
||||
}
|
||||
@@ -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,39 @@
|
||||
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';
|
||||
import semver from 'semver';
|
||||
|
||||
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 } = await getCommandAndArgsAsync(job);
|
||||
await spawnAsync(command, args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
|
||||
async function getCommandAndArgsAsync(job: Job): Promise<{ 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 {
|
||||
const args = [`${PLUGIN_PACKAGE_NAME}@${PLUGIN_PACKAGE_VERSION}`, jobBase64];
|
||||
if (await isAtLeastNpm7Async()) {
|
||||
// npx shipped with npm >= 7.0.0 requires the "-y" flag to run commands without
|
||||
// prompting the user to install a package that is used for the first time
|
||||
args.unshift('-y');
|
||||
}
|
||||
return {
|
||||
command: 'npx',
|
||||
args,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
async function isAtLeastNpm7Async(): Promise<boolean> {
|
||||
const version = (await spawnAsync('npm', ['--version'])).stdout.trim();
|
||||
return semver.gte(version, '7.0.0');
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
|
||||
import { IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import { getApplicationIdAsync } from '../project/android/applicationId';
|
||||
import { GradleBuildContext } from '../project/android/gradle';
|
||||
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
|
||||
import Log from '../log';
|
||||
import { getUsername } from '../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
@@ -22,22 +20,20 @@ import {
|
||||
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
|
||||
import { isExpoUpdatesInstalled } from './utils/updates';
|
||||
|
||||
export type MetadataContext<T extends Platform> = T extends Platform.ANDROID
|
||||
? AndroidMetadataContext
|
||||
: IosMetadataContext;
|
||||
|
||||
export interface AndroidMetadataContext {
|
||||
gradleContext?: GradleBuildContext;
|
||||
}
|
||||
export interface IosMetadataContext {
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
targetName?: string;
|
||||
buildConfiguration?: string;
|
||||
// TODO(JJ): Replace this with the getRuntimeVersionNullable function in @expo/config-plugins
|
||||
function getRuntimeVersionNullable(
|
||||
...[config, platform]: Parameters<typeof Updates.getRuntimeVersion>
|
||||
): string | null {
|
||||
try {
|
||||
return Updates.getRuntimeVersion(config, platform);
|
||||
} catch (e) {
|
||||
Log.debug(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function collectMetadataAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>,
|
||||
platformContext: MetadataContext<T>
|
||||
ctx: BuildContext<T>
|
||||
): Promise<Metadata> {
|
||||
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
|
||||
const distribution =
|
||||
@@ -46,16 +42,16 @@ export async function collectMetadataAsync<T extends Platform>(
|
||||
: ctx.buildProfile.distribution) ?? 'store';
|
||||
const metadata = {
|
||||
trackingContext: ctx.trackingCtx,
|
||||
...(await maybeResolveVersionsAsync(ctx, platformContext)),
|
||||
...(await maybeResolveVersionsAsync(ctx)),
|
||||
cliVersion: easCliVersion,
|
||||
workflow: ctx.workflow,
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
sdkVersion: ctx.exp.sdkVersion,
|
||||
runtimeVersion: ctx.exp.runtimeVersion,
|
||||
runtimeVersion: getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
|
||||
...channelOrReleaseChannel,
|
||||
distribution,
|
||||
appName: ctx.exp.name,
|
||||
appIdentifier: await resolveAppIdentifierAsync(ctx, platformContext),
|
||||
appIdentifier: resolveAppIdentifier(ctx),
|
||||
buildProfile: ctx.buildProfileName,
|
||||
gitCommitHash: await getVcsClient().getCommitHashAsync(),
|
||||
isGitWorkingTreeDirty: await getVcsClient().hasUncommittedChangesAsync(),
|
||||
@@ -70,15 +66,14 @@ export async function collectMetadataAsync<T extends Platform>(
|
||||
}
|
||||
|
||||
async function maybeResolveVersionsAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>,
|
||||
platformContext: MetadataContext<T>
|
||||
ctx: BuildContext<T>
|
||||
): Promise<{ appBuildVersion?: string; appVersion?: string }> {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
const iosContext = platformContext as IosMetadataContext;
|
||||
const iosContext = ctx as BuildContext<Platform.IOS>;
|
||||
return await maybeResolveIosVersionsAsync(
|
||||
ctx.projectDir,
|
||||
ctx.exp,
|
||||
iosContext?.buildSettings ?? {}
|
||||
iosContext.ios.applicationTargetBuildSettings
|
||||
);
|
||||
} else if (ctx.platform === Platform.ANDROID) {
|
||||
const androidCtx = ctx as BuildContext<Platform.ANDROID>;
|
||||
@@ -88,19 +83,11 @@ async function maybeResolveVersionsAsync<T extends Platform>(
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveAppIdentifierAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>,
|
||||
platformContext: MetadataContext<T>
|
||||
): Promise<string> {
|
||||
function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
const iosContext = platformContext as IosMetadataContext;
|
||||
return await getBundleIdentifierAsync(ctx.projectDir, ctx.exp, {
|
||||
targetName: iosContext.targetName,
|
||||
buildConfiguration: iosContext.buildConfiguration,
|
||||
});
|
||||
return (ctx as BuildContext<Platform.IOS>).ios.bundleIdentifier;
|
||||
} else {
|
||||
const androidContext = platformContext as AndroidMetadataContext;
|
||||
return await getApplicationIdAsync(ctx.projectDir, ctx.exp, androidContext.gradleContext);
|
||||
return (ctx as BuildContext<Platform.ANDROID>).android.applicationId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -21,7 +20,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
}: {
|
||||
projectDir: string;
|
||||
nonInteractive?: boolean;
|
||||
buildProfiles?: ProfileData<BuildProfile>[];
|
||||
buildProfiles?: ProfileData<'build'>[];
|
||||
}): Promise<void> {
|
||||
if (await isExpoDevClientInstalledAsync(projectDir)) {
|
||||
return;
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import path from 'path';
|
||||
|
||||
import Log, { learnMore } from '../log';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { BuildContext } from './context';
|
||||
import { CommonContext } from './context';
|
||||
|
||||
export function checkNodeEnvVariable(ctx: BuildContext<Platform>): void {
|
||||
export function checkNodeEnvVariable(ctx: CommonContext<Platform>): void {
|
||||
if (ctx.buildProfile.env?.NODE_ENV === 'production') {
|
||||
Log.warn(
|
||||
'You set NODE_ENV=production in the build profile. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
|
||||
@@ -16,7 +16,7 @@ export function checkNodeEnvVariable(ctx: BuildContext<Platform>): void {
|
||||
}
|
||||
|
||||
export async function checkGoogleServicesFileAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
ctx: CommonContext<T>
|
||||
): Promise<void> {
|
||||
if (ctx.workflow === Workflow.GENERIC || ctx.buildProfile?.env?.GOOGLE_SERVICES_FILE) {
|
||||
return;
|
||||
|
||||
@@ -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: [] };
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import Log, { learnMore } from '../../log';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
|
||||
export default class BuildConfigure extends EasCommand {
|
||||
static description = 'Configure the project to support EAS Build.';
|
||||
@@ -21,11 +22,14 @@ export default class BuildConfigure extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = this.parse(BuildConfigure);
|
||||
|
||||
Log.log(
|
||||
'💡 The following process will configure your iOS and/or Android project to be compatible with EAS Build. These changes only apply to your local project files and you can safely revert them at any time.'
|
||||
);
|
||||
Log.newLine();
|
||||
|
||||
await getVcsClient().ensureRepoExistsAsync();
|
||||
|
||||
const platform =
|
||||
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -10,13 +10,13 @@ import nullthrows from 'nullthrows';
|
||||
import { prepareAndroidBuildAsync } from '../../build/android/build';
|
||||
import { BuildRequestSender, waitForBuildEndAsync } from '../../build/build';
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import { BuildContext, createBuildContextAsync } from '../../build/context';
|
||||
import { BuildContext } from '../../build/context';
|
||||
import { createBuildContextAsync } from '../../build/createContext';
|
||||
import { prepareIosBuildAsync } from '../../build/ios/build';
|
||||
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from '../../build/utils/devClient';
|
||||
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
|
||||
import { ensureRepoIsCleanAsync, reviewAndCommitChangesAsync } from '../../build/utils/repository';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
@@ -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> } = {};
|
||||
|
||||
@@ -199,9 +193,7 @@ export default class Build extends EasCommand {
|
||||
for (const startedBuild of startedBuilds) {
|
||||
const submission = await this.prepareAndStartSubmissionAsync({
|
||||
build: startedBuild.build,
|
||||
credentialsCtx: nullthrows(
|
||||
buildCtxByPlatform[startedBuild.build.platform]?.credentialsCtx
|
||||
),
|
||||
buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]),
|
||||
flags,
|
||||
moreBuilds: startedBuilds.length > 1,
|
||||
projectDir,
|
||||
@@ -274,7 +266,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 +280,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 +308,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,
|
||||
@@ -339,18 +327,18 @@ export default class Build extends EasCommand {
|
||||
|
||||
private async prepareAndStartSubmissionAsync({
|
||||
build,
|
||||
credentialsCtx,
|
||||
buildCtx,
|
||||
flags,
|
||||
moreBuilds,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
}: {
|
||||
build: BuildFragment;
|
||||
credentialsCtx: CredentialsContext;
|
||||
buildCtx: BuildContext<Platform>;
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
projectDir: string;
|
||||
buildProfile: ProfileData<BuildProfile>;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
}): Promise<SubmissionFragment> {
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const platform = toPlatform(build.platform);
|
||||
@@ -363,7 +351,8 @@ export default class Build extends EasCommand {
|
||||
archiveFlags: { id: build.id },
|
||||
nonInteractive: flags.nonInteractive,
|
||||
env: buildProfile.profile.env,
|
||||
credentialsCtx,
|
||||
credentialsCtx: buildCtx.credentialsCtx,
|
||||
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
|
||||
@@ -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,
|
||||
@@ -37,12 +33,6 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secret: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[] = [];
|
||||
|
||||
@@ -14,8 +14,7 @@ export default class WebhookCreate extends EasCommand {
|
||||
static flags = {
|
||||
event: flags.enum({
|
||||
description: 'Event type that triggers the webhook',
|
||||
options: [WebhookType.Build],
|
||||
default: WebhookType.Build,
|
||||
options: [WebhookType.Build, WebhookType.Submit],
|
||||
}),
|
||||
url: flags.string({
|
||||
description: 'Webhook URL',
|
||||
|
||||
@@ -20,7 +20,7 @@ export default class WebhookList extends EasCommand {
|
||||
static flags = {
|
||||
event: flags.enum({
|
||||
description: 'Event type that triggers the webhook',
|
||||
options: [WebhookType.Build],
|
||||
options: [WebhookType.Build, WebhookType.Submit],
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ export default class WebhookUpdate extends EasCommand {
|
||||
}),
|
||||
event: flags.enum({
|
||||
description: 'Event type that triggers the webhook',
|
||||
options: [WebhookType.Build],
|
||||
default: WebhookType.Build,
|
||||
options: [WebhookType.Build, WebhookType.Submit],
|
||||
}),
|
||||
url: flags.string({
|
||||
description: 'Webhook URL',
|
||||
|
||||
@@ -9,7 +9,7 @@ export class RemoveFcm {
|
||||
async runAsync(ctx: CredentialsContext): Promise<void> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(
|
||||
"Deleting an FCM Api Key is a destructive operation. Start the CLI without the '--non-interactive' flag to delete the credentials."
|
||||
"Deleting an FCM API Key is a destructive operation. Start the CLI without the '--non-interactive' flag to delete the credentials."
|
||||
);
|
||||
}
|
||||
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(
|
||||
@@ -18,7 +18,7 @@ export class RemoveFcm {
|
||||
const fcm = appCredentials?.androidFcm;
|
||||
if (!fcm) {
|
||||
Log.warn(
|
||||
`There is no valid FCM Api Key defined for ${formatProjectFullName(this.app)}, ${
|
||||
`There is no valid FCM API Key defined for ${formatProjectFullName(this.app)}, ${
|
||||
this.app.androidApplicationIdentifier
|
||||
}`
|
||||
);
|
||||
@@ -26,7 +26,7 @@ export class RemoveFcm {
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: 'Permanently delete the FCM Api Key from Expo servers?',
|
||||
message: 'Permanently delete the FCM API Key from Expo servers?',
|
||||
initial: false,
|
||||
});
|
||||
if (!confirm) {
|
||||
@@ -34,6 +34,6 @@ export class RemoveFcm {
|
||||
}
|
||||
|
||||
await ctx.android.deleteFcmAsync(fcm);
|
||||
Log.succeed('FCM Api Key removed');
|
||||
Log.succeed('FCM API Key removed');
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
|
||||
describe(RemoveFcm, () => {
|
||||
it('removes an FCM Api Key', async () => {
|
||||
it('removes an FCM API Key', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
android: {
|
||||
|
||||
@@ -1,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;
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ export async function provideOrGenerateAscApiKeyAsync(
|
||||
purpose: AppStoreApiKeyPurpose
|
||||
): Promise<MinimalAscApiKey> {
|
||||
if (ctx.nonInteractive) {
|
||||
return await generateAscApiKeyAsync(ctx, purpose);
|
||||
throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
|
||||
}
|
||||
|
||||
const userProvided = await promptForAscApiKeyAsync(ctx);
|
||||
@@ -73,7 +73,7 @@ export async function provideOrGenerateAscApiKeyAsync(
|
||||
}
|
||||
|
||||
if (!ctx.appStore.authCtx) {
|
||||
Log.warn('Unable to validate App Store Connect Api Key, you are not authenticated with Apple.');
|
||||
Log.warn('Unable to validate App Store Connect API Key, you are not authenticated with Apple.');
|
||||
return userProvided;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ export async function provideOrGenerateAscApiKeyAsync(
|
||||
return userProvided;
|
||||
}
|
||||
const useUserProvided = await confirmAsync({
|
||||
message: `App Store Connect Api Key with ID ${userProvided.keyId} is not valid on Apple's servers. Proceed anyway?`,
|
||||
message: `App Store Connect API Key with ID ${userProvided.keyId} is not valid on Apple's servers. Proceed anyway?`,
|
||||
});
|
||||
if (useUserProvided) {
|
||||
return userProvided;
|
||||
@@ -94,6 +94,7 @@ 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),
|
||||
@@ -129,7 +130,7 @@ async function promptForKeyP8AndIdAsync(): Promise<Pick<AscApiKeyPath, 'keyP8Pat
|
||||
const { keyP8Path } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'keyP8Path',
|
||||
message: 'Path to App Store Connect Api Key:',
|
||||
message: 'Path to App Store Connect API Key:',
|
||||
initial: 'AuthKey_ABCD.p8',
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
validate: async (filePath: string) => {
|
||||
@@ -189,12 +190,12 @@ export async function selectAscApiKeysFromAccountAsync(
|
||||
if (filterDifferentAppleTeam) {
|
||||
const maybeAppleTeamId = ctx.appStore.authCtx?.team.id;
|
||||
Log.warn(
|
||||
`There are no App Store Connect Api Keys in your EAS account${
|
||||
`There are no App Store Connect API Keys in your EAS account${
|
||||
maybeAppleTeamId ? ` matching Apple Team ID: ${maybeAppleTeamId}.` : '.'
|
||||
}`
|
||||
);
|
||||
} else {
|
||||
Log.warn(`There are no App Store Connect Api Keys available in your EAS account.`);
|
||||
Log.warn(`There are no App Store Connect API Keys available in your EAS account.`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -208,7 +209,7 @@ async function selectAscApiKeysAsync(
|
||||
const { chosenAscApiKey } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'chosenAscApiKey',
|
||||
message: 'Select an Api Key from the list:',
|
||||
message: 'Select an API Key from the list:',
|
||||
choices: sortedAscApiKeys.map(ascApiKey => ({
|
||||
title: formatAscApiKey(ascApiKey),
|
||||
value: ascApiKey,
|
||||
|
||||
@@ -31,7 +31,7 @@ export class AssignAscApiKey {
|
||||
throw new Error(`${purpose} is not yet supported.`);
|
||||
}
|
||||
Log.succeed(
|
||||
`App Store Connect Api Key assigned to ${this.app.projectName}: ${this.app.bundleIdentifier} for ${purpose}.`
|
||||
`App Store Connect API Key assigned to ${this.app.projectName}: ${this.app.bundleIdentifier} for ${purpose}.`
|
||||
);
|
||||
return updatedAppCredentials;
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@ export class CreateAscApiKey {
|
||||
purpose: AppStoreApiKeyPurpose
|
||||
): Promise<AppStoreConnectApiKeyFragment> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(`A new App Store Connect Api Key cannot be created in non-interactive mode.`);
|
||||
throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
|
||||
}
|
||||
|
||||
const ascApiKey = await provideOrGenerateAscApiKeyAsync(ctx, purpose);
|
||||
const result = await ctx.ios.createAscApiKeyAsync(this.account, ascApiKey);
|
||||
Log.succeed('Created App Store Connect Api Key');
|
||||
Log.succeed('Created App Store Connect API Key');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export class SelectAndRemoveAscApiKey {
|
||||
const selected = await selectAscApiKeysFromAccountAsync(ctx, this.account);
|
||||
if (selected) {
|
||||
await new RemoveAscApiKey(this.account, selected).runAsync(ctx);
|
||||
Log.succeed('Removed App Store Connect Api Key');
|
||||
Log.succeed('Removed App Store Connect API Key');
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
@@ -23,28 +23,28 @@ export class RemoveAscApiKey {
|
||||
|
||||
public async runAsync(ctx: CredentialsContext): Promise<void> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(`Cannot remove App Store Connect Api Keys in non-interactive mode.`);
|
||||
throw new Error(`Cannot remove App Store Connect API Keys in non-interactive mode.`);
|
||||
}
|
||||
|
||||
// TODO(quin): add an extra edge on AppStoreConnectApiKey to find the apps using it
|
||||
const confirm = await confirmAsync({
|
||||
message: `Deleting this Api Key may affect your projects that rely on it. Do you want to continue?`,
|
||||
message: `Deleting this API Key may affect your projects that rely on it. Do you want to continue?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
Log.log('Aborting');
|
||||
return;
|
||||
}
|
||||
|
||||
Log.log('Removing Api Key');
|
||||
Log.log('Removing API Key');
|
||||
await ctx.ios.deleteAscApiKeyAsync(this.ascApiKey.id);
|
||||
|
||||
let shouldRevoke = false;
|
||||
if (!ctx.nonInteractive) {
|
||||
shouldRevoke = await confirmAsync({
|
||||
message: `Do you also want to revoke this Api Key on the Apple Developer Portal?`,
|
||||
message: `Do you also want to revoke this API Key on the Apple Developer Portal?`,
|
||||
});
|
||||
} else if (ctx.nonInteractive) {
|
||||
Log.log('Skipping Api Key revocation on the Apple Developer Portal.');
|
||||
Log.log('Skipping API Key revocation on the Apple Developer Portal.');
|
||||
}
|
||||
if (shouldRevoke) {
|
||||
await ctx.appStore.revokeAscApiKeyAsync(this.ascApiKey.keyIdentifier);
|
||||
|
||||
@@ -31,10 +31,10 @@ export enum SetupAscApiKeyChoice {
|
||||
export class SetUpAscApiKey {
|
||||
public choices: { title: string; value: string }[] = [
|
||||
{
|
||||
title: '[Choose an existing key]',
|
||||
title: '[Choose an existing ASC API Key]',
|
||||
value: SetupAscApiKeyChoice.USE_EXISTING,
|
||||
},
|
||||
{ title: '[Add a new key]', value: SetupAscApiKeyChoice.GENERATE },
|
||||
{ title: '[Add a new ASC API Key]', value: SetupAscApiKeyChoice.GENERATE },
|
||||
];
|
||||
|
||||
constructor(private app: AppLookupParams, private purpose: AppStoreApiKeyPurpose) {}
|
||||
@@ -42,15 +42,15 @@ export class SetUpAscApiKey {
|
||||
public async runAsync(ctx: CredentialsContext): Promise<CommonIosAppCredentialsFragment> {
|
||||
const isKeySetup = await this.isAscApiKeySetupAsync(ctx, this.purpose);
|
||||
if (isKeySetup) {
|
||||
Log.succeed('App Store Connect Api Key already set up.');
|
||||
Log.succeed('App Store Connect API Key already set up.');
|
||||
return nullthrows(
|
||||
await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(this.app),
|
||||
'iosAppCredentials cannot be null if App Store Connect Api Key is already set up'
|
||||
'iosAppCredentials cannot be null if App Store Connect API Key is already set up'
|
||||
);
|
||||
}
|
||||
if (ctx.nonInteractive) {
|
||||
throw new MissingCredentialsNonInteractiveError(
|
||||
'App Store Connect Api Keys cannot be set up in --non-interactive mode.'
|
||||
'App Store Connect API Keys cannot be set up in --non-interactive mode.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,11 +88,11 @@ export class SetUpAscApiKey {
|
||||
}
|
||||
const [autoselectedKey] = sortAscApiKeysByUpdatedAtDesc(validKeys);
|
||||
const useAutoselected = await confirmAsync({
|
||||
message: `Reuse this App Store Connect Api Key?\n${formatAscApiKey(autoselectedKey)}`,
|
||||
message: `Reuse this App Store Connect API Key?\n${formatAscApiKey(autoselectedKey)}`,
|
||||
});
|
||||
|
||||
if (useAutoselected) {
|
||||
Log.log(`Using App Store Connect Api Key with ID ${autoselectedKey.keyIdentifier}`);
|
||||
Log.log(`Using App Store Connect API Key with ID ${autoselectedKey.keyIdentifier}`);
|
||||
return autoselectedKey;
|
||||
} else {
|
||||
return null;
|
||||
@@ -105,7 +105,7 @@ export class SetUpAscApiKey {
|
||||
): Promise<boolean> {
|
||||
const appCredentials = await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(this.app);
|
||||
if (purpose !== AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
|
||||
throw new Error(`App Store Connect Api Key setup is not yet supported for ${purpose}.`);
|
||||
throw new Error(`App Store Connect API Key setup is not yet supported for ${purpose}.`);
|
||||
}
|
||||
return !!appCredentials?.appStoreConnectApiKeyForSubmissions;
|
||||
}
|
||||
@@ -123,7 +123,7 @@ export class SetUpAscApiKey {
|
||||
const result = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'choice',
|
||||
message: 'Select the App Store Connect Api Key to use for your project:',
|
||||
message: 'Select what to use for your project:',
|
||||
choices,
|
||||
});
|
||||
choice = result.choice;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { AssignAscApiKey } from '../AssignAscApiKey';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
|
||||
describe(AssignAscApiKey, () => {
|
||||
it('assigns an App Store Connect Api Key in Interactive Mode for EAS Submit', async () => {
|
||||
it('assigns an App Store Connect API Key in Interactive Mode for EAS Submit', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
|
||||
describe(CreateAscApiKey, () => {
|
||||
it('creates a App Store Api Key in Interactive Mode', async () => {
|
||||
it('creates a App Store API Key in Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
|
||||
@@ -10,7 +10,7 @@ jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
|
||||
describe(RemoveAscApiKey, () => {
|
||||
it('removes an Asc Api Key', async () => {
|
||||
it('removes an Asc API Key', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: false });
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const removeAscApiKeyAction = new RemoveAscApiKey(
|
||||
|
||||
@@ -18,7 +18,7 @@ jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
|
||||
describe(SetUpAscApiKey, () => {
|
||||
it('skips setting up a ASC Api Key if it is already configured', async () => {
|
||||
it('skips setting up a ASC API Key if it is already configured', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
ios: {
|
||||
@@ -35,12 +35,12 @@ describe(SetUpAscApiKey, () => {
|
||||
);
|
||||
await setupAscApiKeyAction.runAsync(ctx);
|
||||
|
||||
// expect not to create a new ASC Api Key on expo servers
|
||||
// expect not to create a new ASC API Key on expo servers
|
||||
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
|
||||
// expect configuration not to be updated with a new ASC Api Key
|
||||
// expect configuration not to be updated with a new ASC API Key
|
||||
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
|
||||
});
|
||||
it('sets up a ASC Api Key, creating a new one if there are no existing ASC Api Keys', async () => {
|
||||
it('sets up a ASC API Key, creating a new one if there are no existing ASC API Keys', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
@@ -60,12 +60,12 @@ describe(SetUpAscApiKey, () => {
|
||||
);
|
||||
await setupAscApiKeyAction.runAsync(ctx);
|
||||
|
||||
// expect to create a new ASC Api Key on expo servers
|
||||
// expect to create a new ASC API Key on expo servers
|
||||
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
|
||||
// expect configuration to be updated with a new ASC Api Key
|
||||
// expect configuration to be updated with a new ASC API Key
|
||||
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
|
||||
});
|
||||
it('sets up a ASC Api Key, use autoselected ASC Api Key when there are existing keys', async () => {
|
||||
it('sets up a ASC API Key, use autoselected ASC API Key when there are existing keys', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
@@ -86,12 +86,12 @@ describe(SetUpAscApiKey, () => {
|
||||
);
|
||||
await setupAscApiKeyAction.runAsync(ctx);
|
||||
|
||||
// expect not to create a new ASC Api Key on expo servers, we are using the existing one
|
||||
// expect not to create a new ASC API Key on expo servers, we are using the existing one
|
||||
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
|
||||
// expect configuration to be updated with a new ASC Api Key
|
||||
// expect configuration to be updated with a new ASC API Key
|
||||
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
|
||||
});
|
||||
it('sets up a ASC Api Key, allowing user to choose existing keys', async () => {
|
||||
it('sets up a ASC API Key, allowing user to choose existing keys', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
ios: {
|
||||
@@ -110,9 +110,9 @@ describe(SetUpAscApiKey, () => {
|
||||
);
|
||||
await setupAscApiKeyAction.runAsync(ctx);
|
||||
|
||||
// expect not to create a new ASC Api Key on expo servers, we are using the existing one
|
||||
// expect not to create a new ASC API Key on expo servers, we are using the existing one
|
||||
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
|
||||
// expect configuration to be updated with a new ASC Api Key
|
||||
// expect configuration to be updated with a new ASC API Key
|
||||
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
|
||||
});
|
||||
it('works in Non Interactive Mode if ASC Key is configured', async () => {
|
||||
|
||||
+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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -150,6 +150,30 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
expect(results.disabled).toStrictEqual([]);
|
||||
});
|
||||
|
||||
// It's unclear what is happening in this issue, so for now we can provide a more
|
||||
// helpful error message until we can get a reproducible example.
|
||||
// https://github.com/expo/eas-cli/issues/740
|
||||
it('throw useful error message on unexpected error', async () => {
|
||||
const bundleId = {
|
||||
getBundleIdCapabilitiesAsync: jest.fn(() => []),
|
||||
updateBundleIdCapabilityAsync: jest.fn(() => {
|
||||
throw new Error(`bundle 'XXX333' cannot be deleted. Delete all the Apps`);
|
||||
}),
|
||||
attributes: {
|
||||
identifier: 'dev.expo.app',
|
||||
},
|
||||
id: 'XXX333',
|
||||
} as any;
|
||||
|
||||
await expect(
|
||||
syncCapabilitiesForEntitlementsAsync(bundleId, {
|
||||
'com.apple.developer.healthkit': true,
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
`https://developer.apple.com/account/resources/identifiers/bundleId/edit/XXX333`
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot skip complex duplicates', async () => {
|
||||
const bundleId = {
|
||||
getBundleIdCapabilitiesAsync: jest.fn(() => [
|
||||
|
||||
@@ -6,14 +6,14 @@ import { AscApiKey, AscApiKeyInfo } from './Credentials.types';
|
||||
import { AuthCtx, getRequestContext } from './authenticate';
|
||||
|
||||
export async function listAscApiKeysAsync(authCtx: AuthCtx): Promise<AscApiKeyInfo[]> {
|
||||
const spinner = ora(`Fetching App Store Connect Api Keys.`).start();
|
||||
const spinner = ora(`Fetching App Store Connect API Keys.`).start();
|
||||
try {
|
||||
const context = getRequestContext(authCtx);
|
||||
const keys = await ApiKey.getAsync(context);
|
||||
spinner.succeed(`Fetched App Store Connect Api Keys.`);
|
||||
spinner.succeed(`Fetched App Store Connect API Keys.`);
|
||||
return keys.map(key => getAscApiKeyInfo(key, authCtx));
|
||||
} catch (error) {
|
||||
spinner.fail(`Failed to fetch App Store Connect Api Keys.`);
|
||||
spinner.fail(`Failed to fetch App Store Connect API Keys.`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -22,11 +22,11 @@ export async function getAscApiKeyAsync(
|
||||
authCtx: AuthCtx,
|
||||
keyId: string
|
||||
): Promise<AscApiKeyInfo | null> {
|
||||
const spinner = ora(`Fetching App Store Connect Api Key.`).start();
|
||||
const spinner = ora(`Fetching App Store Connect API Key.`).start();
|
||||
try {
|
||||
const context = getRequestContext(authCtx);
|
||||
const apiKey = await ApiKey.infoAsync(context, { id: keyId });
|
||||
spinner.succeed(`Fetched App Store Connect Api Key (ID: ${keyId}).`);
|
||||
spinner.succeed(`Fetched App Store Connect API Key (ID: ${keyId}).`);
|
||||
return getAscApiKeyInfo(apiKey, authCtx);
|
||||
} catch (error: any) {
|
||||
const message = error?.message ?? '';
|
||||
@@ -35,7 +35,7 @@ export async function getAscApiKeyAsync(
|
||||
return null;
|
||||
}
|
||||
Log.error(error);
|
||||
spinner.fail(`Failed to fetch App Store Connect Api Key.`);
|
||||
spinner.fail(`Failed to fetch App Store Connect API Key.`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export async function createAscApiKeyAsync(
|
||||
keyType,
|
||||
}: Partial<Pick<ApiKeyProps, 'nickname' | 'roles' | 'allAppsVisible' | 'keyType'>>
|
||||
): Promise<AscApiKey> {
|
||||
const spinner = ora(`Creating App Store Connect Api Key.`).start();
|
||||
const spinner = ora(`Creating App Store Connect API Key.`).start();
|
||||
try {
|
||||
const context = getRequestContext(authCtx);
|
||||
const key = await ApiKey.createAsync(context, {
|
||||
@@ -74,13 +74,13 @@ export async function createAscApiKeyAsync(
|
||||
}
|
||||
// this object has more optional parameters populated
|
||||
const fullKey = await ApiKey.infoAsync(context, { id: key.id });
|
||||
spinner.succeed(`Created App Store Connect Api Key.`);
|
||||
spinner.succeed(`Created App Store Connect API Key.`);
|
||||
return {
|
||||
...getAscApiKeyInfo(fullKey, authCtx),
|
||||
keyP8,
|
||||
};
|
||||
} catch (err: any) {
|
||||
spinner.fail('Failed to create App Store Connect Api Key.');
|
||||
spinner.fail('Failed to create App Store Connect API Key.');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -89,16 +89,16 @@ export async function revokeAscApiKeyAsync(
|
||||
authCtx: AuthCtx,
|
||||
keyId: string
|
||||
): Promise<AscApiKeyInfo> {
|
||||
const spinner = ora(`Revoking App Store Connect Api Key.`).start();
|
||||
const spinner = ora(`Revoking App Store Connect API Key.`).start();
|
||||
try {
|
||||
const context = getRequestContext(authCtx);
|
||||
const apiKey = await ApiKey.infoAsync(context, { id: keyId });
|
||||
const revokedKey = await apiKey.revokeAsync();
|
||||
spinner.succeed(`Revoked App Store Connect Api Key.`);
|
||||
spinner.succeed(`Revoked App Store Connect API Key.`);
|
||||
return getAscApiKeyInfo(revokedKey, authCtx);
|
||||
} catch (error) {
|
||||
Log.error(error);
|
||||
spinner.fail(`Failed to revoke App Store Connect Api Key.`);
|
||||
spinner.fail(`Failed to revoke App Store Connect API Key.`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@expo/apple-utils';
|
||||
import { JSONObject, JSONValue } from '@expo/json-file';
|
||||
import getenv from 'getenv';
|
||||
import { inspect } from 'util';
|
||||
|
||||
import Log from '../../../log';
|
||||
|
||||
@@ -100,7 +101,20 @@ export async function syncCapabilitiesForEntitlementsAsync(
|
||||
);
|
||||
|
||||
if (modifiedRequest.length) {
|
||||
await bundleId.updateBundleIdCapabilityAsync(modifiedRequest);
|
||||
Log.debug(`Patch Request:`, inspect(modifiedRequest, { depth: null, colors: true }));
|
||||
try {
|
||||
await bundleId.updateBundleIdCapabilityAsync(modifiedRequest);
|
||||
} catch (error: any) {
|
||||
if (error.message.match(/bundle '[\w\d]+' cannot be deleted. Delete all the Apps/)) {
|
||||
Log.error(
|
||||
'Failed to patch capabilities:',
|
||||
inspect(modifiedRequest, { depth: null, colors: true })
|
||||
);
|
||||
throw new Error(
|
||||
`Unexpected error occurred while attempting to update capabilities for app "${bundleId.attributes.identifier}".\nCapabilities can be modified manually in the Apple developer console at https://developer.apple.com/account/resources/identifiers/bundleId/edit/${bundleId.id}.\nAuto capability syncing can be disabled with the environment variable \`EXPO_NO_CAPABILITY_SYNC=1\`.\n${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { enabled: enabledCapabilityNames, disabled: disabledCapabilityNames };
|
||||
|
||||
@@ -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';
|
||||
import { getPrebuildConfigAsync } from '@expo/prebuild-config';
|
||||
|
||||
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 } = await getPrebuildConfigAsync(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;
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ export interface AscApiKeyPath {
|
||||
}
|
||||
|
||||
export const ascApiKeyIdSchema: CredentialSchema<Pick<MinimalAscApiKey, 'keyId'>> = {
|
||||
name: 'App Store Connect Api Key',
|
||||
name: 'App Store Connect API Key',
|
||||
questions: [
|
||||
{
|
||||
field: 'keyId',
|
||||
@@ -130,7 +130,7 @@ export const ascApiKeyIdSchema: CredentialSchema<Pick<MinimalAscApiKey, 'keyId'>
|
||||
};
|
||||
|
||||
export const ascApiKeyIssuerIdSchema: CredentialSchema<Pick<MinimalAscApiKey, 'issuerId'>> = {
|
||||
name: 'App Store Connect Api Key',
|
||||
name: 'App Store Connect API Key',
|
||||
questions: [
|
||||
{
|
||||
field: 'issuerId',
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ Developer Portal ID 9839M6AY8W
|
||||
Apple Team 77KQ969CHE
|
||||
Updated 6 months ago
|
||||
|
||||
App Store Connect Api Key
|
||||
App Store Connect API Key
|
||||
Developer Portal ID NYN468XNN3
|
||||
Name Expo Submissions pl94k8_lnS
|
||||
Issuer ID 69a6de93-e4e4-47e3-e053-5b8c7c11a4d1
|
||||
|
||||
@@ -239,7 +239,7 @@ function displayAscApiKey(
|
||||
maybeAscApiKey: AppStoreConnectApiKeyFragment | null,
|
||||
fields: { label: string; value: string }[]
|
||||
): void {
|
||||
fields.push({ label: 'App Store Connect Api Key', value: '' });
|
||||
fields.push({ label: 'App Store Connect API Key', value: '' });
|
||||
if (maybeAscApiKey) {
|
||||
const { keyIdentifier, issuerIdentifier, appleTeam, name, roles, updatedAt } = maybeAscApiKey;
|
||||
fields.push({ label: 'Developer Portal ID', value: keyIdentifier });
|
||||
|
||||
@@ -8,7 +8,7 @@ export const highLevelActions: ActionInfo[] = [
|
||||
},
|
||||
{
|
||||
value: AndroidActionType.ManageFcm,
|
||||
title: 'Push Notifications: Manage your FCM Api Key',
|
||||
title: 'Push Notifications: Manage your FCM API Key',
|
||||
scope: Scope.Manager,
|
||||
},
|
||||
{
|
||||
@@ -72,12 +72,12 @@ export const buildCredentialsActions: ActionInfo[] = [
|
||||
export const fcmActions: ActionInfo[] = [
|
||||
{
|
||||
value: AndroidActionType.CreateFcm,
|
||||
title: 'Upload an FCM Api Key',
|
||||
title: 'Upload an FCM API Key',
|
||||
scope: Scope.Project,
|
||||
},
|
||||
{
|
||||
value: AndroidActionType.RemoveFcm,
|
||||
title: 'Delete your FCM Api Key',
|
||||
title: 'Delete your FCM API Key',
|
||||
scope: Scope.Project,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ export const highLevelActions: ActionInfo[] = [
|
||||
},
|
||||
{
|
||||
value: IosActionType.ManageAscApiKey,
|
||||
title: 'App Store Connect: Manage your Api Key',
|
||||
title: 'App Store Connect: Manage your API Key',
|
||||
scope: Scope.Manager,
|
||||
},
|
||||
{
|
||||
@@ -81,22 +81,22 @@ export function getAscApiKeyActions(ctx: CredentialsContext): ActionInfo[] {
|
||||
return [
|
||||
{
|
||||
value: IosActionType.SetUpAscApiKeyForSubmissions,
|
||||
title: 'Set up your project to use an Api Key for EAS Submit',
|
||||
title: 'Set up your project to use an API Key for EAS Submit',
|
||||
scope: Scope.Project,
|
||||
},
|
||||
{
|
||||
value: IosActionType.UseExistingAscApiKeyForSubmissions,
|
||||
title: 'Use an existing Api Key for EAS Submit',
|
||||
title: 'Use an existing API Key for EAS Submit',
|
||||
scope: Scope.Project,
|
||||
},
|
||||
{
|
||||
value: IosActionType.CreateAscApiKeyForSubmissions,
|
||||
title: 'Add a new Api Key For EAS Submit',
|
||||
title: 'Add a new API Key For EAS Submit',
|
||||
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account,
|
||||
},
|
||||
{
|
||||
value: IosActionType.RemoveAscApiKey,
|
||||
title: 'Delete an Api Key',
|
||||
title: 'Delete an API Key',
|
||||
scope: Scope.Account,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -229,7 +229,10 @@ export class ManageIos {
|
||||
distribution: buildProfile.distribution,
|
||||
enterpriseProvisioning: buildProfile.enterpriseProvisioning,
|
||||
iosCapabilitiesOptions: {
|
||||
entitlements: await getManagedEntitlementsJsonAsync(ctx.projectDir),
|
||||
entitlements: await getManagedEntitlementsJsonAsync(
|
||||
ctx.projectDir,
|
||||
buildProfile.env ?? {}
|
||||
),
|
||||
},
|
||||
}).runAsync(ctx);
|
||||
return;
|
||||
@@ -360,7 +363,7 @@ export class ManageIos {
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
);
|
||||
const confirm = await confirmAsync({
|
||||
message: `Do you want ${appLookupParams.projectName} to use the new Api Key?`,
|
||||
message: `Do you want ${appLookupParams.projectName} to use the new API Key?`,
|
||||
});
|
||||
if (confirm) {
|
||||
await new AssignAscApiKey(appLookupParams).runAsync(
|
||||
|
||||
@@ -30,7 +30,7 @@ type SessionHeaders = {
|
||||
};
|
||||
|
||||
export const graphqlClient = createUrqlClient({
|
||||
url: getExpoApiBaseUrl() + '/--/graphql',
|
||||
url: getExpoApiBaseUrl() + '/graphql',
|
||||
exchanges: [
|
||||
dedupExchange,
|
||||
cacheExchange,
|
||||
|
||||
+160
-56
@@ -41,7 +41,7 @@ export type RootQuery = {
|
||||
appleDeviceRegistrationRequest: AppleDeviceRegistrationRequestQuery;
|
||||
/** Top-level query object for querying Apple Teams. */
|
||||
appleTeam: AppleTeamQuery;
|
||||
app?: Maybe<AppQuery>;
|
||||
app: AppQuery;
|
||||
/**
|
||||
* Look up app by app id
|
||||
* @deprecated Use 'byId' field under 'app'.
|
||||
@@ -60,6 +60,8 @@ export type RootQuery = {
|
||||
clientBuilds: ClientBuildQuery;
|
||||
/** Top-level query object for querying Experimentation configuration. */
|
||||
experimentation: ExperimentationQuery;
|
||||
/** Top-level query object for querying Stripe Invoices. */
|
||||
invoice: InvoiceQuery;
|
||||
project: ProjectQuery;
|
||||
snack: SnackQuery;
|
||||
submissions: SubmissionQuery;
|
||||
@@ -428,19 +430,31 @@ export type App = Project & {
|
||||
fullName: Scalars['String'];
|
||||
description: Scalars['String'];
|
||||
slug: Scalars['String'];
|
||||
updated: Scalars['DateTime'];
|
||||
published: Scalars['Boolean'];
|
||||
ownerAccount: Account;
|
||||
githubUrl?: Maybe<Scalars['String']>;
|
||||
playStoreUrl?: Maybe<Scalars['String']>;
|
||||
appStoreUrl?: Maybe<Scalars['String']>;
|
||||
icon?: Maybe<AppIcon>;
|
||||
sdkVersion: Scalars['String'];
|
||||
isDeprecated: Scalars['Boolean'];
|
||||
privacySetting: AppPrivacy;
|
||||
latestReleaseId: Scalars['ID'];
|
||||
pushSecurityEnabled: Scalars['Boolean'];
|
||||
/** Whether there have been any classic update publishes */
|
||||
published: Scalars['Boolean'];
|
||||
/** Time of last classic update publish */
|
||||
updated: Scalars['DateTime'];
|
||||
/** ID of latest classic update release */
|
||||
latestReleaseId: Scalars['ID'];
|
||||
/** Whether the latest classic update publish is using a deprecated SDK version */
|
||||
isDeprecated: Scalars['Boolean'];
|
||||
/** SDK version of the latest classic update publish, 0.0.0 otherwise */
|
||||
sdkVersion: Scalars['String'];
|
||||
/** Classic update release channel names (to be removed) */
|
||||
releaseChannels: Array<Scalars['String']>;
|
||||
/** Classic update release channel names that have at least one build */
|
||||
buildsReleaseChannels: Array<Scalars['String']>;
|
||||
/** githubUrl field from most recent classic update manifest */
|
||||
githubUrl?: Maybe<Scalars['String']>;
|
||||
/** android.playStoreUrl field from most recent classic update manifest */
|
||||
playStoreUrl?: Maybe<Scalars['String']>;
|
||||
/** ios.appStoreUrl field from most recent classic update manifest */
|
||||
appStoreUrl?: Maybe<Scalars['String']>;
|
||||
/** Info about the icon specified in the most recent classic update manifest */
|
||||
icon?: Maybe<AppIcon>;
|
||||
/** (EAS Build) Builds associated with this app */
|
||||
builds: Array<Build>;
|
||||
buildJobs: Array<BuildJob>;
|
||||
@@ -634,6 +648,12 @@ export type AppLatestReleaseForReleaseChannelArgs = {
|
||||
releaseChannel: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum AppPrivacy {
|
||||
Public = 'PUBLIC',
|
||||
Unlisted = 'UNLISTED',
|
||||
Hidden = 'HIDDEN'
|
||||
}
|
||||
|
||||
export type AppIcon = {
|
||||
__typename?: 'AppIcon';
|
||||
url: Scalars['String'];
|
||||
@@ -644,12 +664,6 @@ export type AppIcon = {
|
||||
};
|
||||
|
||||
|
||||
export enum AppPrivacy {
|
||||
Public = 'PUBLIC',
|
||||
Unlisted = 'UNLISTED',
|
||||
Hidden = 'HIDDEN'
|
||||
}
|
||||
|
||||
export type BuildFilter = {
|
||||
platform?: Maybe<AppPlatform>;
|
||||
status?: Maybe<BuildStatus>;
|
||||
@@ -945,7 +959,8 @@ export enum SubmissionStatus {
|
||||
|
||||
export type AndroidSubmissionConfig = {
|
||||
__typename?: 'AndroidSubmissionConfig';
|
||||
applicationIdentifier: Scalars['String'];
|
||||
/** @deprecated applicationIdentifier is deprecated and will be auto-detected on submit */
|
||||
applicationIdentifier?: Maybe<Scalars['String']>;
|
||||
/** @deprecated archiveType is deprecated and will be null */
|
||||
archiveType?: Maybe<SubmissionAndroidArchiveType>;
|
||||
track: SubmissionAndroidTrack;
|
||||
@@ -974,7 +989,7 @@ export enum SubmissionAndroidReleaseStatus {
|
||||
export type IosSubmissionConfig = {
|
||||
__typename?: 'IosSubmissionConfig';
|
||||
ascAppIdentifier: Scalars['String'];
|
||||
appleIdUsername: Scalars['String'];
|
||||
appleIdUsername?: Maybe<Scalars['String']>;
|
||||
ascApiKeyId?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
@@ -1396,7 +1411,8 @@ export type WebhookFilter = {
|
||||
};
|
||||
|
||||
export enum WebhookType {
|
||||
Build = 'BUILD'
|
||||
Build = 'BUILD',
|
||||
Submit = 'SUBMIT'
|
||||
}
|
||||
|
||||
export type Webhook = {
|
||||
@@ -1465,6 +1481,7 @@ export type SubscriptionDetails = {
|
||||
endedAt?: Maybe<Scalars['DateTime']>;
|
||||
trialEnd?: Maybe<Scalars['DateTime']>;
|
||||
status?: Maybe<Scalars['String']>;
|
||||
isDowngrading?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type AddonDetails = {
|
||||
@@ -1745,6 +1762,51 @@ export type ExperimentationQuery = {
|
||||
deviceExperimentationUnit: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type InvoiceQuery = {
|
||||
__typename?: 'InvoiceQuery';
|
||||
/** Preview an upgrade subscription invoice, with proration */
|
||||
previewInvoiceForSubscriptionUpdate: Invoice;
|
||||
};
|
||||
|
||||
|
||||
export type InvoiceQueryPreviewInvoiceForSubscriptionUpdateArgs = {
|
||||
accountId: Scalars['String'];
|
||||
newPlanIdentifier: Scalars['String'];
|
||||
};
|
||||
|
||||
export type Invoice = {
|
||||
__typename?: 'Invoice';
|
||||
id: Scalars['ID'];
|
||||
/** The total amount due for the invoice, in cents */
|
||||
amountDue: Scalars['Int'];
|
||||
/** The total amount that has been paid, considering any discounts or account credit. Value is in cents. */
|
||||
amountPaid: Scalars['Int'];
|
||||
/** The total amount that needs to be paid, considering any discounts or account credit. Value is in cents. */
|
||||
amountRemaining: Scalars['Int'];
|
||||
lineItems: Array<InvoiceLineItem>;
|
||||
period: InvoicePeriod;
|
||||
startingBalance: Scalars['Int'];
|
||||
subtotal: Scalars['Int'];
|
||||
total: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type InvoiceLineItem = {
|
||||
__typename?: 'InvoiceLineItem';
|
||||
id: Scalars['ID'];
|
||||
description: Scalars['String'];
|
||||
/** Line-item amount in cents */
|
||||
amount: Scalars['Int'];
|
||||
period: InvoicePeriod;
|
||||
proration: Scalars['Boolean'];
|
||||
quantity: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type InvoicePeriod = {
|
||||
__typename?: 'InvoicePeriod';
|
||||
start: Scalars['DateTime'];
|
||||
end: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export type ProjectQuery = {
|
||||
__typename?: 'ProjectQuery';
|
||||
byAccountNameAndSlug: Project;
|
||||
@@ -1904,6 +1966,7 @@ export type RootMutation = {
|
||||
iosAppBuildCredentials: IosAppBuildCredentialsMutation;
|
||||
/** Mutations that modify the credentials for an iOS app */
|
||||
iosAppCredentials: IosAppCredentialsMutation;
|
||||
keystoreGenerationUrl: KeystoreGenerationUrlMutation;
|
||||
/** Mutations that create, update, and delete Robots */
|
||||
robot: RobotMutation;
|
||||
/** Mutations that modify an EAS Submit submission */
|
||||
@@ -1998,6 +2061,10 @@ export type AccountMutation = {
|
||||
subscribeToProduct?: Maybe<Account>;
|
||||
/** Cancels the active subscription */
|
||||
cancelSubscription?: Maybe<Account>;
|
||||
/** Upgrades or downgrades the active subscription to the newPlanIdentifier, which must be one of the EAS plans (i.e., Production or Enterprise). */
|
||||
changePlan: Account;
|
||||
/** Cancel scheduled subscription change */
|
||||
cancelScheduledSubscriptionChange: Account;
|
||||
/** Requests a refund for the specified charge. Returns true if auto-refund was possible, otherwise requests a manual refund from support and returns false. */
|
||||
requestRefund?: Maybe<Scalars['Boolean']>;
|
||||
/**
|
||||
@@ -2045,6 +2112,17 @@ export type AccountMutationCancelSubscriptionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type AccountMutationChangePlanArgs = {
|
||||
accountID: Scalars['ID'];
|
||||
newPlanIdentifier: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type AccountMutationCancelScheduledSubscriptionChangeArgs = {
|
||||
accountID: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type AccountMutationRequestRefundArgs = {
|
||||
accountID: Scalars['ID'];
|
||||
chargeIdentifier: Scalars['ID'];
|
||||
@@ -2823,6 +2901,18 @@ export type IosAppCredentialsInput = {
|
||||
appStoreConnectApiKeyForSubmissionsId?: Maybe<Scalars['ID']>;
|
||||
};
|
||||
|
||||
export type KeystoreGenerationUrlMutation = {
|
||||
__typename?: 'KeystoreGenerationUrlMutation';
|
||||
/** Create a Keystore Generation URL */
|
||||
createKeystoreGenerationUrl: KeystoreGenerationUrl;
|
||||
};
|
||||
|
||||
export type KeystoreGenerationUrl = {
|
||||
__typename?: 'KeystoreGenerationUrl';
|
||||
id: Scalars['ID'];
|
||||
url: Scalars['String'];
|
||||
};
|
||||
|
||||
export type RobotMutation = {
|
||||
__typename?: 'RobotMutation';
|
||||
/** Create a Robot and grant it Permissions on an Account */
|
||||
@@ -2957,7 +3047,7 @@ export type AndroidSubmissionConfigInput = {
|
||||
googleServiceAccountKeyId?: Maybe<Scalars['String']>;
|
||||
googleServiceAccountKeyJson?: Maybe<Scalars['String']>;
|
||||
archiveUrl?: Maybe<Scalars['String']>;
|
||||
applicationIdentifier: Scalars['String'];
|
||||
applicationIdentifier?: Maybe<Scalars['String']>;
|
||||
track: SubmissionAndroidTrack;
|
||||
releaseStatus?: Maybe<SubmissionAndroidReleaseStatus>;
|
||||
changesNotSentForReview?: Maybe<Scalars['Boolean']>;
|
||||
@@ -3540,7 +3630,7 @@ export type GetBranchInfoQueryVariables = Exact<{
|
||||
|
||||
export type GetBranchInfoQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
@@ -3550,7 +3640,7 @@ export type GetBranchInfoQuery = (
|
||||
& Pick<UpdateBranch, 'id' | 'name'>
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type DeleteUpdateBranchMutationVariables = Exact<{
|
||||
@@ -3577,7 +3667,7 @@ export type BranchesByAppQueryVariables = Exact<{
|
||||
|
||||
export type BranchesByAppQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
@@ -3588,7 +3678,7 @@ export type BranchesByAppQuery = (
|
||||
& UpdateBranchFragment
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type GetUpdateGroupAsyncQueryVariables = Exact<{
|
||||
@@ -3629,7 +3719,7 @@ export type ViewBranchQueryVariables = Exact<{
|
||||
|
||||
export type ViewBranchQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
@@ -3650,7 +3740,7 @@ export type ViewBranchQuery = (
|
||||
)> }
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type CancelBuildMutationVariables = Exact<{
|
||||
@@ -3695,7 +3785,7 @@ export type GetChannelByNameToEditQueryVariables = Exact<{
|
||||
|
||||
export type GetChannelByNameToEditQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
@@ -3709,7 +3799,7 @@ export type GetChannelByNameToEditQuery = (
|
||||
)> }
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type UpdateChannelBranchMappingMutationVariables = Exact<{
|
||||
@@ -3738,7 +3828,7 @@ export type GetAllChannelsForAppQueryVariables = Exact<{
|
||||
|
||||
export type GetAllChannelsForAppQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
@@ -3763,7 +3853,7 @@ export type GetAllChannelsForAppQuery = (
|
||||
)> }
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type GetChannelByNameForAppQueryVariables = Exact<{
|
||||
@@ -3774,7 +3864,7 @@ export type GetChannelByNameForAppQueryVariables = Exact<{
|
||||
|
||||
export type GetChannelByNameForAppQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
@@ -3799,7 +3889,7 @@ export type GetChannelByNameForAppQuery = (
|
||||
)> }
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type AppInfoQueryVariables = Exact<{
|
||||
@@ -3809,13 +3899,13 @@ export type AppInfoQueryVariables = Exact<{
|
||||
|
||||
export type AppInfoQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
& Pick<App, 'id' | 'fullName'>
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type DeleteUpdateGroupMutationVariables = Exact<{
|
||||
@@ -4056,7 +4146,7 @@ export type CommonAndroidAppCredentialsWithBuildCredentialsByApplicationIdentifi
|
||||
|
||||
export type CommonAndroidAppCredentialsWithBuildCredentialsByApplicationIdentifierQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byFullName: (
|
||||
{ __typename?: 'App' }
|
||||
@@ -4067,7 +4157,7 @@ export type CommonAndroidAppCredentialsWithBuildCredentialsByApplicationIdentifi
|
||||
& CommonAndroidAppCredentialsFragment
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type GoogleServiceAccountKeyByAccountQueryVariables = Exact<{
|
||||
@@ -4453,14 +4543,14 @@ export type AppByFullNameQueryVariables = Exact<{
|
||||
|
||||
export type AppByFullNameQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byFullName: (
|
||||
{ __typename?: 'App' }
|
||||
& Pick<App, 'id'>
|
||||
& AppFragment
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type AppStoreConnectApiKeyByAccountQueryVariables = Exact<{
|
||||
@@ -4593,7 +4683,7 @@ export type AppleDistributionCertificateByAppQueryVariables = Exact<{
|
||||
|
||||
export type AppleDistributionCertificateByAppQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byFullName: (
|
||||
{ __typename?: 'App' }
|
||||
@@ -4617,7 +4707,7 @@ export type AppleDistributionCertificateByAppQuery = (
|
||||
)> }
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type AppleDistributionCertificateByAccountQueryVariables = Exact<{
|
||||
@@ -4650,7 +4740,7 @@ export type AppleProvisioningProfilesByAppQueryVariables = Exact<{
|
||||
|
||||
export type AppleProvisioningProfilesByAppQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byFullName: (
|
||||
{ __typename?: 'App' }
|
||||
@@ -4682,7 +4772,7 @@ export type AppleProvisioningProfilesByAppQuery = (
|
||||
)> }
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type ApplePushKeyByAccountQueryVariables = Exact<{
|
||||
@@ -4753,7 +4843,7 @@ export type IosAppBuildCredentialsByAppleAppIdentiferAndDistributionQueryVariabl
|
||||
|
||||
export type IosAppBuildCredentialsByAppleAppIdentiferAndDistributionQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byFullName: (
|
||||
{ __typename?: 'App' }
|
||||
@@ -4768,7 +4858,7 @@ export type IosAppBuildCredentialsByAppleAppIdentiferAndDistributionQuery = (
|
||||
)> }
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type IosAppCredentialsWithBuildCredentialsByAppIdentifierIdQueryVariables = Exact<{
|
||||
@@ -4780,7 +4870,7 @@ export type IosAppCredentialsWithBuildCredentialsByAppIdentifierIdQueryVariables
|
||||
|
||||
export type IosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byFullName: (
|
||||
{ __typename?: 'App' }
|
||||
@@ -4796,7 +4886,7 @@ export type IosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery = (
|
||||
& CommonIosAppCredentialsWithoutBuildCredentialsFragment
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type CommonIosAppCredentialsWithBuildCredentialsByAppIdentifierIdQueryVariables = Exact<{
|
||||
@@ -4807,7 +4897,7 @@ export type CommonIosAppCredentialsWithBuildCredentialsByAppIdentifierIdQueryVar
|
||||
|
||||
export type CommonIosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byFullName: (
|
||||
{ __typename?: 'App' }
|
||||
@@ -4818,7 +4908,7 @@ export type CommonIosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery =
|
||||
& CommonIosAppCredentialsFragment
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type CreateAppMutationVariables = Exact<{
|
||||
@@ -4939,6 +5029,20 @@ export type DeleteEnvironmentSecretMutation = (
|
||||
) }
|
||||
);
|
||||
|
||||
export type CreateKeystoreGenerationUrlMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type CreateKeystoreGenerationUrlMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { keystoreGenerationUrl: (
|
||||
{ __typename?: 'KeystoreGenerationUrlMutation' }
|
||||
& { createKeystoreGenerationUrl: (
|
||||
{ __typename?: 'KeystoreGenerationUrl' }
|
||||
& Pick<KeystoreGenerationUrl, 'id' | 'url'>
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type GetSignedUploadMutationVariables = Exact<{
|
||||
contentTypes: Array<Scalars['String']>;
|
||||
}>;
|
||||
@@ -5107,7 +5211,7 @@ export type GetAllBuildsForAppQueryVariables = Exact<{
|
||||
|
||||
export type GetAllBuildsForAppQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
@@ -5118,7 +5222,7 @@ export type GetAllBuildsForAppQuery = (
|
||||
& BuildFragment
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type EnvironmentSecretsByAccountNameQueryVariables = Exact<{
|
||||
@@ -5149,7 +5253,7 @@ export type EnvironmentSecretsByAppIdQueryVariables = Exact<{
|
||||
|
||||
export type EnvironmentSecretsByAppIdQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
@@ -5160,7 +5264,7 @@ export type EnvironmentSecretsByAppIdQuery = (
|
||||
& EnvironmentSecretFragment
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type ProjectByUsernameAndSlugQueryVariables = Exact<{
|
||||
@@ -5227,7 +5331,7 @@ export type GetAllSubmissionsForAppQueryVariables = Exact<{
|
||||
|
||||
export type GetAllSubmissionsForAppQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
@@ -5238,7 +5342,7 @@ export type GetAllSubmissionsForAppQuery = (
|
||||
& SubmissionFragment
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
@@ -5271,7 +5375,7 @@ export type WebhooksByAppIdQueryVariables = Exact<{
|
||||
|
||||
export type WebhooksByAppIdQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
@@ -5282,7 +5386,7 @@ export type WebhooksByAppIdQuery = (
|
||||
& WebhookFragment
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type WebhookByIdQueryVariables = Exact<{
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import { CreateKeystoreGenerationUrlMutation } from '../generated';
|
||||
|
||||
export const KeystoreGenerationUrlMutation = {
|
||||
async createKeystoreGenerationUrlAsync(): Promise<string> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateKeystoreGenerationUrlMutation>(
|
||||
gql`
|
||||
mutation CreateKeystoreGenerationUrlMutation {
|
||||
keystoreGenerationUrl {
|
||||
createKeystoreGenerationUrl {
|
||||
id
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.keystoreGenerationUrl.createKeystoreGenerationUrl.url;
|
||||
},
|
||||
};
|
||||
@@ -54,7 +54,7 @@ describe(getApplicationIdAsync, () => {
|
||||
'/app'
|
||||
);
|
||||
await expect(getApplicationIdAsync('/app', {} as any, undefined)).rejects.toThrowError(
|
||||
/Could not read application id/
|
||||
/Failed to find 'build.gradle' /
|
||||
);
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ describe(getApplicationIdAsync, () => {
|
||||
|
||||
await expect(
|
||||
getApplicationIdAsync('/app', {} as any, { moduleName: 'app' })
|
||||
).rejects.toThrowError(/Could not read application id/);
|
||||
).rejects.toThrowError(/Could not read applicationId/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,7 +112,7 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
|
||||
);
|
||||
await expect(
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync('/app', {} as any)
|
||||
).rejects.toThrowError(/we can't update this file programatically/);
|
||||
).rejects.toThrowError(/we can't update this file programmatically/);
|
||||
});
|
||||
it('prompts for the Android package if using app.json', async () => {
|
||||
vol.fromJSON(
|
||||
|
||||
@@ -15,7 +15,7 @@ import { resolveWorkflowAsync } from '../workflow';
|
||||
import { GradleBuildContext } from './gradle';
|
||||
import * as gradleUtils from './gradleUtils';
|
||||
|
||||
const INVALID_APPLICATION_ID_MESSAGE = `Invalid format of Android applicationId. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`;
|
||||
export const INVALID_APPLICATION_ID_MESSAGE = `Invalid format of Android applicationId. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`;
|
||||
|
||||
export async function ensureApplicationIdIsDefinedForManagedProjectAsync(
|
||||
projectDir: string,
|
||||
@@ -33,6 +33,51 @@ export async function ensureApplicationIdIsDefinedForManagedProjectAsync(
|
||||
}
|
||||
}
|
||||
|
||||
export class AmbiguousApplicationIdError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message ?? 'Could not resolve applicationId.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getApplicationIdFromBareAsync(
|
||||
projectDir: string,
|
||||
gradleContext?: GradleBuildContext
|
||||
): Promise<string> {
|
||||
const errorMessage = 'Could not read applicationId from Android project.';
|
||||
|
||||
if (gradleContext) {
|
||||
const buildGradle = await gradleUtils.getAppBuildGradleAsync(projectDir);
|
||||
const applicationIdSuffix = gradleUtils.resolveConfigValue(
|
||||
buildGradle,
|
||||
'applicationIdSuffix',
|
||||
gradleContext.flavor
|
||||
);
|
||||
if (applicationIdSuffix) {
|
||||
throw new Error('"applicationIdSuffix" in app/build.gradle is not supported.');
|
||||
}
|
||||
const applicationId = gradleUtils.resolveConfigValue(
|
||||
buildGradle,
|
||||
'applicationId',
|
||||
gradleContext.flavor
|
||||
);
|
||||
return nullthrows(applicationId, errorMessage);
|
||||
} else {
|
||||
// should return value only if productFlavors are not used
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
const buildGradle = await fs.readFile(buildGradlePath, 'utf8');
|
||||
const matchResult = buildGradle.match(/applicationId ['"](.*)['"]/);
|
||||
if (buildGradle.match(/applicationIdSuffix/)) {
|
||||
throw new Error('"applicationIdSuffix" in app/build.gradle is not supported.');
|
||||
}
|
||||
if (buildGradle.match(/productFlavors/)) {
|
||||
throw new AmbiguousApplicationIdError(
|
||||
'Failed to autodetect applicationId in multi-flavor project.'
|
||||
);
|
||||
}
|
||||
return nullthrows(matchResult?.[1], errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getApplicationIdAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
@@ -42,33 +87,7 @@ export async function getApplicationIdAsync(
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(projectDir, exp);
|
||||
|
||||
const errorMessage = 'Could not read application id from Android project.';
|
||||
if (gradleContext) {
|
||||
const buildGradle = await gradleUtils.getAppBuildGradleAsync(projectDir);
|
||||
const applicationId = gradleUtils.resolveConfigValue(
|
||||
buildGradle,
|
||||
'applicationId',
|
||||
gradleContext.flavor
|
||||
);
|
||||
return nullthrows(applicationId, errorMessage);
|
||||
} else {
|
||||
// fallback to best effort approach, this logic can be dropped when we start supporting
|
||||
// modules different than 'app' and 'flavorDimensions'
|
||||
let buildGradlePath = null;
|
||||
try {
|
||||
buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
} catch {}
|
||||
if (!buildGradlePath || !(await fs.pathExists(buildGradlePath))) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
const buildGradle = await fs.readFile(buildGradlePath, 'utf8');
|
||||
const matchResult = buildGradle.match(/applicationId ['"](.*)['"]/);
|
||||
const applicationId = nullthrows(matchResult?.[1], errorMessage);
|
||||
|
||||
Log.warn(`Unable to detect applicationId`);
|
||||
Log.warn(`Falling back to best effort approach, using applicationId ${applicationId}`);
|
||||
return applicationId;
|
||||
}
|
||||
return getApplicationIdFromBareAsync(projectDir, gradleContext);
|
||||
} else {
|
||||
const applicationId = AndroidConfig.Package.getPackage(exp);
|
||||
if (!applicationId || !isApplicationIdValid(applicationId)) {
|
||||
@@ -91,7 +110,7 @@ async function configureApplicationIdAsync(projectDir: string, exp: ExpoConfig):
|
||||
// we can't automatically update app.config.js
|
||||
if (paths.dynamicConfigPath) {
|
||||
throw new Error(
|
||||
`"android.package" is not defined in your app.config.js and we can't update this file programatically. Add the value on your own and run this command again.`
|
||||
`"android.package" is not defined in your app.config.js and we can't update this file programmatically. Add the value on your own and run this command again.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,7 +144,7 @@ async function configureApplicationIdAsync(projectDir: string, exp: ExpoConfig):
|
||||
return packageName;
|
||||
}
|
||||
|
||||
function isApplicationIdValid(applicationId: string): boolean {
|
||||
export function isApplicationIdValid(applicationId: string): boolean {
|
||||
return /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(applicationId);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function resolveGradleBuildContextAsync(
|
||||
parsedGradleCommand.moduleName !== gradleUtils.DEFAULT_MODULE_NAME
|
||||
) {
|
||||
Log.warn(
|
||||
`Building modules different than "${gradleUtils.DEFAULT_MODULE_NAME}" might result in unexpected behavior`
|
||||
`Building modules different than "${gradleUtils.DEFAULT_MODULE_NAME}" might result in unexpected behavior.`
|
||||
);
|
||||
}
|
||||
return {
|
||||
@@ -38,7 +38,8 @@ export async function resolveGradleBuildContextAsync(
|
||||
return { moduleName: gradleUtils.DEFAULT_MODULE_NAME };
|
||||
}
|
||||
} catch (err: any) {
|
||||
Log.warn(`Unable to read project config from app/build.gradle: ${err.message}`);
|
||||
Log.warn(`Unable to read gradle project config: ${err.message}.`);
|
||||
Log.warn('Values from app/build.gradle might be resolved incorrectly.');
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -12,6 +12,7 @@ interface GradleCommand {
|
||||
|
||||
interface Config {
|
||||
applicationId?: string;
|
||||
applicationIdSuffix?: string; // unsupported value
|
||||
versionCode?: string;
|
||||
versionName?: string;
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
|
||||
);
|
||||
await expect(
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync('/app', {} as any)
|
||||
).rejects.toThrowError(/we can't update this file programatically/);
|
||||
).rejects.toThrowError(/we can't update this file programmatically/);
|
||||
});
|
||||
it('prompts for the bundle identifier if using app.json', async () => {
|
||||
vol.fromJSON(
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { getProjectConfigDescription, getUsername } from '../projectUtils';
|
||||
import { resolveWorkflowAsync } from '../workflow';
|
||||
|
||||
const INVALID_BUNDLE_IDENTIFIER_MESSAGE = `Invalid format of iOS bundle identifier. Only alphanumeric characters, '.' and '-' are allowed, and each '.' must be followed by a letter.`;
|
||||
export const INVALID_BUNDLE_IDENTIFIER_MESSAGE = `Invalid format of iOS bundle identifier. Only alphanumeric characters, '.' and '-' are allowed, and each '.' must be followed by a letter.`;
|
||||
|
||||
export async function ensureBundleIdentifierIsDefinedForManagedProjectAsync(
|
||||
projectDir: string,
|
||||
@@ -28,22 +28,39 @@ export async function ensureBundleIdentifierIsDefinedForManagedProjectAsync(
|
||||
}
|
||||
}
|
||||
|
||||
export class AmbiguousBundleIdentifierError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message ?? 'Could not resolve bundle identifier.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBundleIdentifierAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
{ targetName, buildConfiguration }: { targetName?: string; buildConfiguration?: string } = {}
|
||||
xcodeContext?: { targetName?: string; buildConfiguration?: string }
|
||||
): Promise<string> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(projectDir, exp);
|
||||
|
||||
const bundleIdentifier = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(projectDir, {
|
||||
targetName,
|
||||
buildConfiguration,
|
||||
});
|
||||
const xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
|
||||
const isMultiScheme = IOSConfig.BuildScheme.getSchemesFromXcodeproj(projectDir).length > 1;
|
||||
const isMultiTarget =
|
||||
IOSConfig.Target.getNativeTargets(xcodeProject).filter(([, target]) =>
|
||||
IOSConfig.Target.isTargetOfType(target, IOSConfig.Target.TargetType.APPLICATION)
|
||||
).length > 1;
|
||||
if (!xcodeContext && isMultiScheme && isMultiTarget) {
|
||||
throw new AmbiguousBundleIdentifierError(
|
||||
"Multiple schemes and targets found in Xcode project, bundle identifier couldn't be resolved."
|
||||
);
|
||||
}
|
||||
const bundleIdentifier = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
|
||||
projectDir,
|
||||
xcodeContext ?? {}
|
||||
);
|
||||
const buildConfigurationDesc =
|
||||
targetName && buildConfiguration
|
||||
? ` (target = ${targetName}, build configuration = ${buildConfiguration})`
|
||||
xcodeContext?.targetName && xcodeContext?.buildConfiguration
|
||||
? ` (target = ${xcodeContext.targetName}, build configuration = ${xcodeContext.buildConfiguration})`
|
||||
: '';
|
||||
assert(
|
||||
bundleIdentifier,
|
||||
@@ -58,13 +75,17 @@ export async function getBundleIdentifierAsync(
|
||||
} else {
|
||||
// TODO: the following asserts are only temporary until we support app extensions in managed projects
|
||||
assert(
|
||||
!targetName || targetName === IOSConfig.XcodeUtils.sanitizedName(exp.name),
|
||||
'targetName cannot be set to an arbitrary value for managed projects'
|
||||
!xcodeContext?.targetName ||
|
||||
xcodeContext?.targetName === IOSConfig.XcodeUtils.sanitizedName(exp.name),
|
||||
'targetName cannot be set to an arbitrary value for managed projects.'
|
||||
);
|
||||
assert(!buildConfiguration, 'buildConfiguration cannot be passed for managed projects');
|
||||
const bundleIdentifer = IOSConfig.BundleIdentifier.getBundleIdentifier(exp);
|
||||
if (!bundleIdentifer || !isBundleIdentifierValid(bundleIdentifer)) {
|
||||
if (bundleIdentifer) {
|
||||
assert(
|
||||
!xcodeContext?.buildConfiguration,
|
||||
'buildConfiguration cannot be passed for managed projects.'
|
||||
);
|
||||
const bundleIdentifier = IOSConfig.BundleIdentifier.getBundleIdentifier(exp);
|
||||
if (!bundleIdentifier || !isBundleIdentifierValid(bundleIdentifier)) {
|
||||
if (bundleIdentifier) {
|
||||
Log.warn(INVALID_BUNDLE_IDENTIFIER_MESSAGE);
|
||||
}
|
||||
throw new Error(
|
||||
@@ -73,7 +94,7 @@ export async function getBundleIdentifierAsync(
|
||||
)} and run this command again.`
|
||||
);
|
||||
} else {
|
||||
return bundleIdentifer;
|
||||
return bundleIdentifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +107,7 @@ async function configureBundleIdentifierAsync(
|
||||
// we can't automatically update app.config.js
|
||||
if (paths.dynamicConfigPath) {
|
||||
throw new Error(
|
||||
`"ios.bundleIdentifier" is not defined in your app.config.js and we can't update this file programatically. Add the value on your own and run this command again.`
|
||||
`"ios.bundleIdentifier" is not defined in your app.config.js and we can't update this file programmatically. Add the value on your own and run this command again.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,7 +141,7 @@ async function configureBundleIdentifierAsync(
|
||||
return bundleIdentifier;
|
||||
}
|
||||
|
||||
function isBundleIdentifierValid(bundleIdentifier: string): boolean {
|
||||
export function isBundleIdentifierValid(bundleIdentifier: string): boolean {
|
||||
return /^[a-zA-Z0-9-.]+$/.test(bundleIdentifier);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { apiClient } from '../api';
|
||||
import Log from '../log';
|
||||
|
||||
/**
|
||||
* Checks if the project is allowed for using EAS services.
|
||||
* THIS IS A TEMPORARY STEP NEEDED FOR OPEN PREVIEW
|
||||
* @returns boolean
|
||||
*/
|
||||
export async function isEasEnabledForProjectAsync(projectId: string): Promise<boolean> {
|
||||
try {
|
||||
const {
|
||||
data: { enabled },
|
||||
} = await apiClient.get(`projects/${projectId}/eas-enabled`).json();
|
||||
return enabled;
|
||||
} catch (error: any) {
|
||||
if (error.response?.statusCode === 404) {
|
||||
return true;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const EAS_UNAVAILABLE_MESSAGE = `Your account doesn't have access to Expo Application Services (EAS) features. Enroll in EAS to give it a try: ${chalk.underline(
|
||||
'https://expo.dev/eas'
|
||||
)}`;
|
||||
|
||||
export function warnEasUnavailable(): void {
|
||||
Log.warn(EAS_UNAVAILABLE_MESSAGE);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
import { withAnalyticsAsync } from '../analytics/common';
|
||||
import { Event, SubmissionEvent } from '../analytics/events';
|
||||
import {
|
||||
AndroidSubmissionConfigInput,
|
||||
IosSubmissionConfigInput,
|
||||
@@ -18,12 +20,58 @@ export interface SubmissionInput<P extends Platform> {
|
||||
: IosSubmissionConfigInput;
|
||||
buildId?: string;
|
||||
}
|
||||
export default abstract class BaseSubmitter<P extends Platform, SubmissionOptions> {
|
||||
constructor(protected ctx: SubmissionContext<P>, protected options: SubmissionOptions) {}
|
||||
|
||||
public abstract submitAsync(): Promise<SubmissionFragment>;
|
||||
interface AnalyticEvents {
|
||||
attemptEvent: Event;
|
||||
successEvent: Event;
|
||||
failureEvent: Event;
|
||||
}
|
||||
|
||||
protected async createSubmissionAsync(
|
||||
export default abstract class BaseSubmitter<
|
||||
P extends Platform,
|
||||
ResolvedSourceOptions,
|
||||
SubmissionOptions
|
||||
> {
|
||||
constructor(
|
||||
protected ctx: SubmissionContext<P>,
|
||||
protected options: SubmissionOptions,
|
||||
protected sourceOptionResolver: {
|
||||
[K in keyof ResolvedSourceOptions]: () => Promise<ResolvedSourceOptions[K]>;
|
||||
},
|
||||
protected sourceOptionAnalytics: Record<keyof ResolvedSourceOptions, AnalyticEvents>
|
||||
) {}
|
||||
|
||||
private async getSourceOptionsAsync(): Promise<ResolvedSourceOptions> {
|
||||
const resolvedSourceOptions: ResolvedSourceOptions = {} as ResolvedSourceOptions;
|
||||
// Do not perform this in parallel as some of these require user interaction
|
||||
for (const key in this.sourceOptionResolver) {
|
||||
const sourceOptionKey = key as keyof ResolvedSourceOptions;
|
||||
const sourceOptionAnalytics = this.sourceOptionAnalytics[sourceOptionKey];
|
||||
|
||||
const sourceOption = await withAnalyticsAsync<
|
||||
ResolvedSourceOptions[keyof ResolvedSourceOptions]
|
||||
>(async () => await this.sourceOptionResolver[sourceOptionKey](), {
|
||||
attemptEvent: sourceOptionAnalytics.attemptEvent,
|
||||
successEvent: sourceOptionAnalytics.successEvent,
|
||||
failureEvent: sourceOptionAnalytics.failureEvent,
|
||||
trackingCtx: this.ctx.trackingCtx,
|
||||
});
|
||||
resolvedSourceOptions[sourceOptionKey] = sourceOption;
|
||||
}
|
||||
return resolvedSourceOptions;
|
||||
}
|
||||
|
||||
public async submitAsync(): Promise<SubmissionFragment> {
|
||||
const resolvedSourceOptions = await this.getSourceOptionsAsync();
|
||||
const input = await this.createSubmissionInputAsync(resolvedSourceOptions);
|
||||
return await this.createSubmissionWithAnalyticsAsync(input);
|
||||
}
|
||||
|
||||
public abstract createSubmissionInputAsync(
|
||||
resolvedOptions: ResolvedSourceOptions
|
||||
): Promise<SubmissionInput<P>>;
|
||||
|
||||
private async createSubmissionAsync(
|
||||
submissionInput: SubmissionInput<P>
|
||||
): Promise<SubmissionFragment> {
|
||||
Log.addNewLineIfNone();
|
||||
@@ -39,6 +87,20 @@ export default abstract class BaseSubmitter<P extends Platform, SubmissionOption
|
||||
}
|
||||
}
|
||||
|
||||
private async createSubmissionWithAnalyticsAsync(
|
||||
submissionInput: SubmissionInput<P>
|
||||
): Promise<SubmissionFragment> {
|
||||
return await withAnalyticsAsync<SubmissionFragment>(
|
||||
async () => this.createSubmissionAsync(submissionInput),
|
||||
{
|
||||
attemptEvent: SubmissionEvent.SUBMIT_REQUEST_ATTEMPT,
|
||||
successEvent: SubmissionEvent.SUBMIT_REQUEST_SUCCESS,
|
||||
failureEvent: SubmissionEvent.SUBMIT_REQUEST_FAIL,
|
||||
trackingCtx: this.ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected abstract createPlatformSubmissionAsync(
|
||||
input: SubmissionInput<P>
|
||||
): Promise<SubmissionFragment>;
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { promptAsync } from '../../prompts';
|
||||
|
||||
export enum AndroidPackageSourceType {
|
||||
userDefined,
|
||||
prompt,
|
||||
}
|
||||
|
||||
interface AndroidPackageSourceBase {
|
||||
sourceType: AndroidPackageSourceType;
|
||||
}
|
||||
|
||||
interface AndroidPackageUserDefinedSource extends AndroidPackageSourceBase {
|
||||
sourceType: AndroidPackageSourceType.userDefined;
|
||||
androidPackage: string;
|
||||
}
|
||||
|
||||
interface AndroidPackagePromptSource extends AndroidPackageSourceBase {
|
||||
sourceType: AndroidPackageSourceType.prompt;
|
||||
}
|
||||
|
||||
export type AndroidPackageSource = AndroidPackageUserDefinedSource | AndroidPackagePromptSource;
|
||||
|
||||
export async function getAndroidPackageAsync(source: AndroidPackageSource): Promise<string> {
|
||||
if (source.sourceType === AndroidPackageSourceType.userDefined) {
|
||||
return source.androidPackage;
|
||||
} else if (source.sourceType === AndroidPackageSourceType.prompt) {
|
||||
const { androidPackage } = await promptAsync({
|
||||
name: 'androidPackage',
|
||||
message: 'Android package name:',
|
||||
type: 'text',
|
||||
validate: (val: string) => val !== '' || 'Package name cannot be empty!',
|
||||
});
|
||||
return androidPackage;
|
||||
} else {
|
||||
throw new Error('This should never happen');
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,14 @@ import {
|
||||
SubmissionFragment,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { getApplicationIdAsync } from '../../project/android/applicationId';
|
||||
import * as gradleUtils from '../../project/android/gradleUtils';
|
||||
import {
|
||||
AmbiguousApplicationIdError,
|
||||
getApplicationIdAsync,
|
||||
} from '../../project/android/applicationId';
|
||||
import capitalize from '../../utils/expodash/capitalize';
|
||||
import { ArchiveSource } from '../ArchiveSource';
|
||||
import { resolveArchiveSource } from '../commons';
|
||||
import { SubmissionContext } from '../context';
|
||||
import { AndroidPackageSource, AndroidPackageSourceType } from './AndroidPackageSource';
|
||||
import AndroidSubmitter, { AndroidSubmissionOptions } from './AndroidSubmitter';
|
||||
import { ServiceAccountSource, ServiceAccountSourceType } from './ServiceAccountSource';
|
||||
|
||||
@@ -29,19 +30,12 @@ export default class AndroidSubmitCommand {
|
||||
}
|
||||
|
||||
private async getAndroidSubmissionOptionsAsync(): Promise<AndroidSubmissionOptions> {
|
||||
const androidPackageSource = await this.resolveAndroidPackageSourceAsync();
|
||||
const track = this.resolveTrack();
|
||||
const releaseStatus = this.resolveReleaseStatus();
|
||||
const archiveSource = this.resolveArchiveSource();
|
||||
const serviceAccountSource = this.resolveServiceAccountSource();
|
||||
const serviceAccountSource = await this.resolveServiceAccountSourceAsync();
|
||||
|
||||
const errored = [
|
||||
androidPackageSource,
|
||||
track,
|
||||
releaseStatus,
|
||||
archiveSource,
|
||||
serviceAccountSource,
|
||||
].filter(r => !r.ok);
|
||||
const errored = [track, releaseStatus, archiveSource, serviceAccountSource].filter(r => !r.ok);
|
||||
if (errored.length > 0) {
|
||||
const message = errored.map(err => err.reason?.message).join('\n');
|
||||
Log.error(message);
|
||||
@@ -50,7 +44,6 @@ export default class AndroidSubmitCommand {
|
||||
|
||||
return {
|
||||
projectId: this.ctx.projectId,
|
||||
androidPackageSource: androidPackageSource.enforceValue(),
|
||||
track: track.enforceValue(),
|
||||
releaseStatus: releaseStatus.enforceValue(),
|
||||
archiveSource: archiveSource.enforceValue(),
|
||||
@@ -59,29 +52,19 @@ export default class AndroidSubmitCommand {
|
||||
};
|
||||
}
|
||||
|
||||
private async maybeGetAndroidPackageFromCurrentProjectAsync(): Promise<string | undefined> {
|
||||
private async maybeGetAndroidPackageFromCurrentProjectAsync(): Promise<Result<string | null>> {
|
||||
try {
|
||||
return await getApplicationIdAsync(this.ctx.projectDir, this.ctx.exp, {
|
||||
moduleName: gradleUtils.DEFAULT_MODULE_NAME,
|
||||
});
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveAndroidPackageSourceAsync(): Promise<Result<AndroidPackageSource>> {
|
||||
const androidPackage = await this.maybeGetAndroidPackageFromCurrentProjectAsync();
|
||||
if (androidPackage) {
|
||||
return result({
|
||||
sourceType: AndroidPackageSourceType.userDefined,
|
||||
androidPackage,
|
||||
});
|
||||
} else if (this.ctx.nonInteractive) {
|
||||
return result(new Error("Couldn't resolve the Android package."));
|
||||
} else {
|
||||
return result({
|
||||
sourceType: AndroidPackageSourceType.prompt,
|
||||
});
|
||||
return result(await getApplicationIdAsync(this.ctx.projectDir, this.ctx.exp));
|
||||
} catch (error: any) {
|
||||
if (error instanceof AmbiguousApplicationIdError) {
|
||||
Log.warn(
|
||||
'"applicationId" is ambiguous, specify it via "applicationId" field in the submit profile in the eas.json.'
|
||||
);
|
||||
return result(null);
|
||||
}
|
||||
return result(
|
||||
new Error(`Failed to resolve applicationId in Android project: ${error.message}.`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +120,7 @@ export default class AndroidSubmitCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private resolveServiceAccountSource(): Result<ServiceAccountSource> {
|
||||
private async resolveServiceAccountSourceAsync(): Promise<Result<ServiceAccountSource>> {
|
||||
const { serviceAccountKeyPath } = this.ctx.profile;
|
||||
if (serviceAccountKeyPath) {
|
||||
return result({
|
||||
@@ -145,8 +128,22 @@ export default class AndroidSubmitCommand {
|
||||
path: serviceAccountKeyPath,
|
||||
});
|
||||
}
|
||||
let androidApplicationIdentifier: string | undefined =
|
||||
this.ctx.applicationIdentifierOverride ?? this.ctx.profile.applicationId;
|
||||
if (!androidApplicationIdentifier) {
|
||||
const androidApplicationIdentifierResult =
|
||||
await this.maybeGetAndroidPackageFromCurrentProjectAsync();
|
||||
if (!androidApplicationIdentifierResult.ok) {
|
||||
return result(androidApplicationIdentifierResult.reason);
|
||||
}
|
||||
const androidApplicationIdentifierValue = androidApplicationIdentifierResult.enforceValue();
|
||||
if (androidApplicationIdentifierValue) {
|
||||
androidApplicationIdentifier = androidApplicationIdentifierValue;
|
||||
}
|
||||
}
|
||||
return result({
|
||||
sourceType: ServiceAccountSourceType.credentialsService,
|
||||
androidApplicationIdentifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { SubmissionEvent } from '../../analytics/events';
|
||||
import {
|
||||
AndroidSubmissionConfigInput,
|
||||
SubmissionAndroidReleaseStatus,
|
||||
@@ -11,12 +12,12 @@ import { SubmissionMutation } from '../../graphql/mutations/SubmissionMutation';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { Archive, ArchiveSource, getArchiveAsync } from '../ArchiveSource';
|
||||
import BaseSubmitter, { SubmissionInput } from '../BaseSubmitter';
|
||||
import { SubmissionContext } from '../context';
|
||||
import {
|
||||
ArchiveSourceSummaryFields,
|
||||
formatArchiveSourceSummary,
|
||||
printSummary,
|
||||
} from '../utils/summary';
|
||||
import { AndroidPackageSource, getAndroidPackageAsync } from './AndroidPackageSource';
|
||||
import {
|
||||
ServiceAccountKeyResult,
|
||||
ServiceAccountSource,
|
||||
@@ -29,38 +30,58 @@ export interface AndroidSubmissionOptions
|
||||
'track' | 'releaseStatus' | 'changesNotSentForReview'
|
||||
> {
|
||||
projectId: string;
|
||||
androidPackageSource: AndroidPackageSource;
|
||||
archiveSource: ArchiveSource;
|
||||
serviceAccountSource: ServiceAccountSource;
|
||||
}
|
||||
|
||||
interface ResolvedSourceOptions {
|
||||
androidPackage: string;
|
||||
archive: Archive;
|
||||
serviceAccountKeyResult: ServiceAccountKeyResult;
|
||||
}
|
||||
|
||||
export default class AndroidSubmitter extends BaseSubmitter<
|
||||
Platform.ANDROID,
|
||||
ResolvedSourceOptions,
|
||||
AndroidSubmissionOptions
|
||||
> {
|
||||
async submitAsync(): Promise<SubmissionFragment> {
|
||||
const resolvedSourceOptions = await this.resolveSourceOptionsAsync();
|
||||
const submissionConfig = await this.formatSubmissionConfigAsync(
|
||||
this.options,
|
||||
resolvedSourceOptions
|
||||
);
|
||||
constructor(ctx: SubmissionContext<Platform.ANDROID>, options: AndroidSubmissionOptions) {
|
||||
const sourceOptionsResolver = {
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
archive: async () => await getArchiveAsync(this.options.archiveSource),
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
serviceAccountKeyResult: async () => {
|
||||
return await getServiceAccountKeyResultAsync(this.ctx, this.options.serviceAccountSource);
|
||||
},
|
||||
};
|
||||
const sourceOptionsAnalytics = {
|
||||
archive: {
|
||||
attemptEvent: SubmissionEvent.GATHER_ARCHIVE_ATTEMPT,
|
||||
successEvent: SubmissionEvent.GATHER_ARCHIVE_SUCCESS,
|
||||
failureEvent: SubmissionEvent.GATHER_ARCHIVE_FAIL,
|
||||
},
|
||||
serviceAccountKeyResult: {
|
||||
attemptEvent: SubmissionEvent.GATHER_CREDENTIALS_ATTEMPT,
|
||||
successEvent: SubmissionEvent.GATHER_CREDENTIALS_SUCCESS,
|
||||
failureEvent: SubmissionEvent.GATHER_CREDENTIALS_FAIL,
|
||||
},
|
||||
};
|
||||
super(ctx, options, sourceOptionsResolver, sourceOptionsAnalytics);
|
||||
}
|
||||
|
||||
public async createSubmissionInputAsync(
|
||||
resolvedSourceOptions: ResolvedSourceOptions
|
||||
): Promise<SubmissionInput<Platform.ANDROID>> {
|
||||
const submissionConfig = await this.formatSubmissionConfig(this.options, resolvedSourceOptions);
|
||||
|
||||
printSummary(
|
||||
this.prepareSummaryData(this.options, resolvedSourceOptions),
|
||||
SummaryHumanReadableKeys
|
||||
);
|
||||
|
||||
return await this.createSubmissionAsync({
|
||||
return {
|
||||
projectId: this.options.projectId,
|
||||
submissionConfig,
|
||||
buildId: resolvedSourceOptions.archive.build?.id,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
protected async createPlatformSubmissionAsync({
|
||||
@@ -75,28 +96,12 @@ export default class AndroidSubmitter extends BaseSubmitter<
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveSourceOptionsAsync(): Promise<ResolvedSourceOptions> {
|
||||
const androidPackage = await getAndroidPackageAsync(this.options.androidPackageSource);
|
||||
const archive = await getArchiveAsync(this.options.archiveSource);
|
||||
const serviceAccountKeyResult = await getServiceAccountKeyResultAsync(
|
||||
this.ctx,
|
||||
this.options.serviceAccountSource,
|
||||
androidPackage
|
||||
);
|
||||
return {
|
||||
androidPackage,
|
||||
archive,
|
||||
serviceAccountKeyResult,
|
||||
};
|
||||
}
|
||||
|
||||
private async formatSubmissionConfigAsync(
|
||||
private formatSubmissionConfig(
|
||||
options: AndroidSubmissionOptions,
|
||||
{ archive, androidPackage, serviceAccountKeyResult }: ResolvedSourceOptions
|
||||
): Promise<AndroidSubmissionConfigInput> {
|
||||
{ archive, serviceAccountKeyResult }: ResolvedSourceOptions
|
||||
): AndroidSubmissionConfigInput {
|
||||
const { track, releaseStatus, changesNotSentForReview } = options;
|
||||
return {
|
||||
applicationIdentifier: androidPackage,
|
||||
archiveUrl: archive.url,
|
||||
track,
|
||||
changesNotSentForReview,
|
||||
@@ -107,14 +112,13 @@ export default class AndroidSubmitter extends BaseSubmitter<
|
||||
|
||||
private prepareSummaryData(
|
||||
options: AndroidSubmissionOptions,
|
||||
{ archive, androidPackage, serviceAccountKeyResult }: ResolvedSourceOptions
|
||||
{ archive, serviceAccountKeyResult }: ResolvedSourceOptions
|
||||
): SummaryData {
|
||||
const { projectId, track, releaseStatus, changesNotSentForReview } = options;
|
||||
|
||||
// structuring order affects table rows order
|
||||
return {
|
||||
projectId,
|
||||
androidPackage,
|
||||
track,
|
||||
changesNotSentForReview: changesNotSentForReview ?? undefined,
|
||||
releaseStatus: releaseStatus ?? undefined,
|
||||
@@ -125,7 +129,6 @@ export default class AndroidSubmitter extends BaseSubmitter<
|
||||
}
|
||||
|
||||
type SummaryData = {
|
||||
androidPackage: string;
|
||||
changesNotSentForReview?: boolean;
|
||||
formattedServiceAccount: string;
|
||||
projectId: string;
|
||||
@@ -134,7 +137,6 @@ type SummaryData = {
|
||||
} & ArchiveSourceSummaryFields;
|
||||
|
||||
const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
|
||||
androidPackage: 'Android package',
|
||||
archivePath: 'Archive path',
|
||||
archiveUrl: 'Download URL',
|
||||
changesNotSentForReview: 'Changes not sent for a review',
|
||||
|
||||
@@ -6,6 +6,10 @@ import nullthrows from 'nullthrows';
|
||||
import { SetUpGoogleServiceAccountKey } from '../../credentials/android/actions/SetUpGoogleServiceAccountKey';
|
||||
import { readAndValidateServiceAccountKey } from '../../credentials/android/utils/googleServiceAccountKey';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import {
|
||||
INVALID_APPLICATION_ID_MESSAGE,
|
||||
isApplicationIdValid,
|
||||
} from '../../project/android/applicationId';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { SubmissionContext } from '../context';
|
||||
@@ -32,6 +36,7 @@ interface ServiceAccountPromptSource extends ServiceAccountSourceBase {
|
||||
|
||||
export interface ServiceAccountCredentialsServiceSource extends ServiceAccountSourceBase {
|
||||
sourceType: ServiceAccountSourceType.credentialsService;
|
||||
androidApplicationIdentifier?: string;
|
||||
}
|
||||
|
||||
export type ServiceAccountKeyResult = {
|
||||
@@ -60,11 +65,10 @@ export type ServiceAccountSource =
|
||||
|
||||
export async function getServiceAccountKeyResultAsync(
|
||||
ctx: SubmissionContext<Platform.ANDROID>,
|
||||
source: ServiceAccountSource,
|
||||
androidApplicationIdentifier: string
|
||||
source: ServiceAccountSource
|
||||
): Promise<ServiceAccountKeyResult> {
|
||||
if (source.sourceType === ServiceAccountSourceType.credentialsService) {
|
||||
return await getServiceAccountFromCredentialsServiceAsync(ctx, androidApplicationIdentifier);
|
||||
return await getServiceAccountFromCredentialsServiceAsync(ctx, source);
|
||||
} else {
|
||||
return await getServiceAccountLocallyAsync(source);
|
||||
}
|
||||
@@ -97,9 +101,19 @@ export async function getServiceAccountKeyPathAsync(source: ServiceAccountSource
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForApplicationIdAsync(): Promise<string> {
|
||||
const { androidPackage } = await promptAsync({
|
||||
name: 'androidPackage',
|
||||
message: 'Android package name:',
|
||||
type: 'text',
|
||||
validate: value => (isApplicationIdValid(value) ? true : INVALID_APPLICATION_ID_MESSAGE),
|
||||
});
|
||||
return androidPackage;
|
||||
}
|
||||
|
||||
export async function getServiceAccountFromCredentialsServiceAsync(
|
||||
ctx: SubmissionContext<Platform.ANDROID>,
|
||||
androidApplicationIdentifier: string
|
||||
source: ServiceAccountCredentialsServiceSource
|
||||
): Promise<ServiceAccountKeyResult> {
|
||||
const appLookupParams = {
|
||||
account: nullthrows(
|
||||
@@ -107,8 +121,12 @@ export async function getServiceAccountFromCredentialsServiceAsync(
|
||||
`You do not have access to account: ${ctx.accountName}`
|
||||
),
|
||||
projectName: ctx.projectName,
|
||||
androidApplicationIdentifier,
|
||||
androidApplicationIdentifier:
|
||||
source.androidApplicationIdentifier ?? (await promptForApplicationIdAsync()),
|
||||
};
|
||||
Log.log(
|
||||
`Looking up credentials configuration for ${appLookupParams.androidApplicationIdentifier}...`
|
||||
);
|
||||
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
|
||||
const androidAppCredentials = await setupGoogleServiceAccountKeyAction.runAsync(
|
||||
ctx.credentialsCtx
|
||||
|
||||
@@ -126,7 +126,6 @@ describe(AndroidSubmitCommand, () => {
|
||||
appId: projectId,
|
||||
config: {
|
||||
archiveUrl: 'http://expo.dev/fake.apk',
|
||||
applicationIdentifier: testProject.appJSON.expo.android?.package,
|
||||
googleServiceAccountKeyJson: fakeFiles['/google-service-account.json'],
|
||||
releaseStatus: SubmissionAndroidReleaseStatus.Draft,
|
||||
track: SubmissionAndroidTrack.Internal,
|
||||
@@ -160,7 +159,6 @@ describe(AndroidSubmitCommand, () => {
|
||||
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith({
|
||||
appId: projectId,
|
||||
config: {
|
||||
applicationIdentifier: testProject.appJSON.expo.android?.package,
|
||||
googleServiceAccountKeyJson: fakeFiles['/google-service-account.json'],
|
||||
releaseStatus: SubmissionAndroidReleaseStatus.Draft,
|
||||
track: SubmissionAndroidTrack.Internal,
|
||||
|
||||
@@ -152,7 +152,7 @@ describe(getServiceAccountKeyResultAsync, () => {
|
||||
sourceType: ServiceAccountSourceType.path,
|
||||
path: '/project_dir/subdir/service-account.json',
|
||||
};
|
||||
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, source, 'test.sdf');
|
||||
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, source);
|
||||
expect(serviceAccountResult).toMatchObject({
|
||||
result: {
|
||||
googleServiceAccountKeyJson: mockDetectableServiceAccountJson,
|
||||
@@ -186,7 +186,7 @@ describe(getServiceAccountKeyResultAsync, () => {
|
||||
const source: ServiceAccountSource = {
|
||||
sourceType: ServiceAccountSourceType.prompt,
|
||||
};
|
||||
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, source, 'test.sdf');
|
||||
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, source);
|
||||
expect(serviceAccountResult).toMatchObject({
|
||||
result: {
|
||||
googleServiceAccountKeyJson: mockDetectableServiceAccountJson,
|
||||
@@ -214,13 +214,10 @@ describe(getServiceAccountKeyResultAsync, () => {
|
||||
},
|
||||
nonInteractive: true,
|
||||
});
|
||||
const serviceAccountResult = await getServiceAccountKeyResultAsync(
|
||||
ctx,
|
||||
{
|
||||
sourceType: ServiceAccountSourceType.credentialsService,
|
||||
},
|
||||
'test.application.identiifer'
|
||||
);
|
||||
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, {
|
||||
sourceType: ServiceAccountSourceType.credentialsService,
|
||||
androidApplicationIdentifier: 'test.application.identiifer',
|
||||
});
|
||||
expect(serviceAccountResult).toMatchObject({
|
||||
result: { googleServiceAccountKeyId: 'test-id' },
|
||||
summary: { source: 'EAS servers', email: '[email protected]' },
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { SubmitProfile } from '@expo/eas-json';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { TrackingContext } from '../analytics/common';
|
||||
import { Analytics, SubmissionEvent } from '../analytics/events';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getProjectAccountName } from '../project/projectUtils';
|
||||
import { findAccountByName } from '../user/Account';
|
||||
import { Actor } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
|
||||
@@ -12,6 +16,7 @@ export interface SubmissionContext<T extends Platform> {
|
||||
accountName: string;
|
||||
archiveFlags: SubmitArchiveFlags;
|
||||
credentialsCtx: CredentialsContext;
|
||||
trackingCtx: TrackingContext;
|
||||
exp: ExpoConfig;
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
@@ -20,6 +25,7 @@ export interface SubmissionContext<T extends Platform> {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
user: Actor;
|
||||
applicationIdentifierOverride?: string;
|
||||
}
|
||||
|
||||
export interface SubmitArchiveFlags {
|
||||
@@ -38,17 +44,30 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
|
||||
profile: SubmitProfile<T>;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
applicationIdentifier?: string;
|
||||
}): Promise<SubmissionContext<T>> {
|
||||
const { projectDir, nonInteractive } = params;
|
||||
const { applicationIdentifier, projectDir, nonInteractive } = params;
|
||||
const exp = getExpoConfig(projectDir, { env: params.env });
|
||||
const { env, ...rest } = params;
|
||||
const user = await ensureLoggedInAsync();
|
||||
const projectName = exp.slug;
|
||||
const accountName = getProjectAccountName(exp, user);
|
||||
const accountId = findAccountByName(user.accounts, accountName)?.id;
|
||||
let credentialsCtx: CredentialsContext | undefined = params.credentialsCtx;
|
||||
if (!credentialsCtx) {
|
||||
credentialsCtx = new CredentialsContext({ projectDir, user, exp, nonInteractive });
|
||||
}
|
||||
|
||||
const trackingCtx = {
|
||||
tracking_id: uuidv4(),
|
||||
platform: params.platform,
|
||||
...(accountId && { account_id: accountId }),
|
||||
account_name: accountName,
|
||||
project_id: params.projectId,
|
||||
};
|
||||
|
||||
Analytics.logEvent(SubmissionEvent.SUBMIT_COMMAND, trackingCtx);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
accountName,
|
||||
@@ -56,5 +75,7 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
|
||||
exp,
|
||||
projectName,
|
||||
user,
|
||||
trackingCtx,
|
||||
applicationIdentifierOverride: applicationIdentifier,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import getenv from 'getenv';
|
||||
|
||||
import { promptAsync } from '../../prompts';
|
||||
import UserSettings from '../../user/UserSettings';
|
||||
import { SubmissionContext } from '../context';
|
||||
export enum AppSpecificPasswordSourceType {
|
||||
userDefined,
|
||||
}
|
||||
@@ -11,15 +17,62 @@ interface AppSpecificPasswordUserDefinedSource extends AppSpecificPasswordSource
|
||||
appSpecificPassword: string;
|
||||
}
|
||||
|
||||
export interface AppSpecificPasswordCredentials {
|
||||
password: string;
|
||||
appleIdUsername: string;
|
||||
}
|
||||
|
||||
export type AppSpecificPasswordSource = AppSpecificPasswordUserDefinedSource;
|
||||
|
||||
export async function getAppSpecificPasswordAsync(
|
||||
export async function getAppSpecificPasswordLocallyAsync(
|
||||
ctx: SubmissionContext<Platform.IOS>,
|
||||
source: AppSpecificPasswordSource
|
||||
): Promise<string> {
|
||||
): Promise<AppSpecificPasswordCredentials> {
|
||||
if (source.sourceType === AppSpecificPasswordSourceType.userDefined) {
|
||||
return source.appSpecificPassword;
|
||||
return {
|
||||
password: source.appSpecificPassword,
|
||||
appleIdUsername: await getAppleIdUsernameAsync(ctx),
|
||||
};
|
||||
} else {
|
||||
// exhaustive -- should never happen
|
||||
throw new Error(`Unknown app specific password source type "${(source as any)?.sourceType}"`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAppleIdUsernameAsync(
|
||||
ctx: SubmissionContext<Platform.IOS>
|
||||
): Promise<string> {
|
||||
if (ctx.profile.appleId) {
|
||||
return ctx.profile.appleId;
|
||||
}
|
||||
|
||||
const envAppleId = getenv.string('EXPO_APPLE_ID', '');
|
||||
if (envAppleId) {
|
||||
return envAppleId;
|
||||
}
|
||||
|
||||
if (ctx.credentialsCtx.appStore.authCtx?.appleId) {
|
||||
return ctx.credentialsCtx.appStore.authCtx.appleId;
|
||||
}
|
||||
|
||||
// Get the email address that was last used and set it as
|
||||
// the default value for quicker authentication.
|
||||
const lastAppleId = await UserSettings.getAsync('appleId', null);
|
||||
|
||||
if (ctx.nonInteractive) {
|
||||
if (lastAppleId) {
|
||||
return lastAppleId;
|
||||
} else {
|
||||
throw new Error('Set appleId in the submit profile (eas.json).');
|
||||
}
|
||||
}
|
||||
|
||||
const { appleId } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'appleId',
|
||||
message: `Enter your Apple ID:`,
|
||||
validate: (val: string) => !!val,
|
||||
initial: lastAppleId ?? undefined,
|
||||
});
|
||||
return appleId;
|
||||
}
|
||||
@@ -1,33 +1,21 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import {
|
||||
AppStoreApiKeyPurpose,
|
||||
promptForAscApiKeyPathAsync,
|
||||
} from '../../credentials/ios/actions/AscApiKeyUtils';
|
||||
import { SetUpAscApiKey } from '../../credentials/ios/actions/SetUpAscApiKey';
|
||||
import { promptForAscApiKeyPathAsync } from '../../credentials/ios/actions/AscApiKeyUtils';
|
||||
import { AscApiKeyPath, MinimalAscApiKey } from '../../credentials/ios/credentials';
|
||||
import Log from '../../log';
|
||||
import { getBundleIdentifierAsync } from '../../project/ios/bundleIdentifier';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { SubmissionContext } from '../context';
|
||||
import { isExistingFileAsync } from '../utils/files';
|
||||
|
||||
export enum AscApiKeySourceType {
|
||||
path,
|
||||
prompt,
|
||||
credentialsService,
|
||||
}
|
||||
|
||||
interface AscApiKeySourceBase {
|
||||
sourceType: AscApiKeySourceType;
|
||||
}
|
||||
|
||||
interface AscApiKeyCredentialsServiceSource extends AscApiKeySourceBase {
|
||||
sourceType: AscApiKeySourceType.credentialsService;
|
||||
}
|
||||
|
||||
interface AscApiKeyPromptSource extends AscApiKeySourceBase {
|
||||
sourceType: AscApiKeySourceType.prompt;
|
||||
}
|
||||
@@ -37,10 +25,7 @@ interface AscApiKeyEnvVarSource extends AscApiKeySourceBase {
|
||||
path: AscApiKeyPath;
|
||||
}
|
||||
|
||||
export type AscApiKeySource =
|
||||
| AscApiKeyEnvVarSource
|
||||
| AscApiKeyPromptSource
|
||||
| AscApiKeyCredentialsServiceSource;
|
||||
export type AscApiKeySource = AscApiKeyEnvVarSource | AscApiKeyPromptSource;
|
||||
|
||||
type AscApiKeySummary = {
|
||||
source: 'local' | 'EAS servers';
|
||||
@@ -58,55 +43,7 @@ export type AscApiKeyResult = {
|
||||
summary: AscApiKeySummary;
|
||||
};
|
||||
|
||||
export async function getAscApiKeyResultAsync(
|
||||
ctx: SubmissionContext<Platform.IOS>,
|
||||
source: AscApiKeySource
|
||||
): Promise<AscApiKeyResult> {
|
||||
if (source.sourceType === AscApiKeySourceType.credentialsService) {
|
||||
return await getAscApiKeyFromCredentialsServiceAsync(ctx);
|
||||
} else {
|
||||
return await getAscApiKeyLocallyAsync(ctx, source);
|
||||
}
|
||||
}
|
||||
|
||||
async function getAscApiKeyFromCredentialsServiceAsync(
|
||||
ctx: SubmissionContext<Platform.IOS>
|
||||
): Promise<AscApiKeyResult> {
|
||||
const bundleIdentifier = await getBundleIdentifierAsync(ctx.projectDir, ctx.exp);
|
||||
Log.log(`Looking up credentials configuration for ${bundleIdentifier}...`);
|
||||
|
||||
const appLookupParams = {
|
||||
account: nullthrows(
|
||||
findAccountByName(ctx.user.accounts, ctx.accountName),
|
||||
`You do not have access to account: ${ctx.accountName}`
|
||||
),
|
||||
projectName: ctx.projectName,
|
||||
bundleIdentifier,
|
||||
};
|
||||
const setupAscApiKeyAction = new SetUpAscApiKey(
|
||||
appLookupParams,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
);
|
||||
const iosAppCredentials = await setupAscApiKeyAction.runAsync(ctx.credentialsCtx);
|
||||
const ascKeyForSubmissions = nullthrows(
|
||||
iosAppCredentials.appStoreConnectApiKeyForSubmissions,
|
||||
`An EAS Submit ASC Api Key could not be found for ${iosAppCredentials.appleAppIdentifier.bundleIdentifier}`
|
||||
);
|
||||
const { id, keyIdentifier, name } = ascKeyForSubmissions;
|
||||
Log.log(`Using Api Key ID: ${keyIdentifier}${name ? ` (${name})` : ''}`);
|
||||
return {
|
||||
result: {
|
||||
ascApiKeyId: id,
|
||||
},
|
||||
summary: {
|
||||
source: 'EAS servers',
|
||||
keyId: keyIdentifier,
|
||||
name: name ?? undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getAscApiKeyLocallyAsync(
|
||||
export async function getAscApiKeyLocallyAsync(
|
||||
ctx: SubmissionContext<Platform.IOS>,
|
||||
source: AscApiKeySource
|
||||
): Promise<AscApiKeyResult> {
|
||||
@@ -133,8 +70,6 @@ export async function getAscApiKeyPathAsync(
|
||||
return await handlePathSourceAsync(ctx, source);
|
||||
case AscApiKeySourceType.prompt:
|
||||
return await handlePromptSourceAsync(ctx, source);
|
||||
case AscApiKeySourceType.credentialsService:
|
||||
throw new Error(`AscApiKeySourceType ${source} does not return a path.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { SetUpSubmissionCredentials } from '../../credentials/ios/actions/SetUpSubmissionCredentials';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
INVALID_BUNDLE_IDENTIFIER_MESSAGE,
|
||||
isBundleIdentifierValid,
|
||||
} from '../../project/ios/bundleIdentifier';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { SubmissionContext } from '../context';
|
||||
import {
|
||||
AppSpecificPasswordCredentials,
|
||||
getAppleIdUsernameAsync,
|
||||
} from './AppSpecificPasswordSource';
|
||||
import { AscApiKeyResult } from './AscApiKeySource';
|
||||
|
||||
/**
|
||||
* The Credentials Service will either return an ASC API Key or an App Specific Password
|
||||
* When we no longer support the App Specific Password user prompt, refactor this into the AscApiKeySource
|
||||
*/
|
||||
export const CREDENTIALS_SERVICE_SOURCE = 'CREDENTIALS_SERVICE_SOURCE' as const;
|
||||
export interface CredentialsServiceSource {
|
||||
sourceType: typeof CREDENTIALS_SERVICE_SOURCE;
|
||||
bundleIdentifier?: string;
|
||||
}
|
||||
|
||||
async function promptForBundleIdentifierAsync(): Promise<string> {
|
||||
const { bundleIdentifier } = await promptAsync({
|
||||
name: 'bundleIdentifier',
|
||||
message: 'Bundle identifier:',
|
||||
type: 'text',
|
||||
validate: value => (isBundleIdentifierValid(value) ? true : INVALID_BUNDLE_IDENTIFIER_MESSAGE),
|
||||
});
|
||||
return bundleIdentifier;
|
||||
}
|
||||
|
||||
export async function getFromCredentialsServiceAsync(
|
||||
ctx: SubmissionContext<Platform.IOS>,
|
||||
source: CredentialsServiceSource
|
||||
): Promise<
|
||||
{ appSpecificPassword: AppSpecificPasswordCredentials } | { ascApiKeyResult: AscApiKeyResult }
|
||||
> {
|
||||
const bundleIdentifier = source.bundleIdentifier ?? (await promptForBundleIdentifierAsync());
|
||||
Log.log(`Looking up credentials configuration for ${bundleIdentifier}...`);
|
||||
|
||||
const appLookupParams = {
|
||||
account: nullthrows(
|
||||
findAccountByName(ctx.user.accounts, ctx.accountName),
|
||||
`You do not have access to account: ${ctx.accountName}`
|
||||
),
|
||||
projectName: ctx.projectName,
|
||||
bundleIdentifier,
|
||||
};
|
||||
const setupSubmissionCredentialsAction = new SetUpSubmissionCredentials(appLookupParams);
|
||||
const ascOrAsp = await setupSubmissionCredentialsAction.runAsync(ctx.credentialsCtx);
|
||||
const isAppSpecificPassword = typeof ascOrAsp === 'string';
|
||||
if (isAppSpecificPassword) {
|
||||
return {
|
||||
appSpecificPassword: {
|
||||
password: ascOrAsp,
|
||||
appleIdUsername: await getAppleIdUsernameAsync(ctx),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const ascKeyForSubmissions = nullthrows(
|
||||
ascOrAsp.appStoreConnectApiKeyForSubmissions,
|
||||
`An EAS Submit ASC API Key could not be found for ${ascOrAsp.appleAppIdentifier.bundleIdentifier}`
|
||||
);
|
||||
const { id, keyIdentifier, name } = ascKeyForSubmissions;
|
||||
return {
|
||||
ascApiKeyResult: {
|
||||
result: {
|
||||
ascApiKeyId: id,
|
||||
},
|
||||
summary: {
|
||||
source: 'EAS servers',
|
||||
keyId: keyIdentifier,
|
||||
name: name ?? undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,10 @@ import wrapAnsi from 'wrap-ansi';
|
||||
import { MissingCredentialsError } from '../../credentials/errors';
|
||||
import { SubmissionFragment } from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import UserSettings from '../../user/UserSettings';
|
||||
import {
|
||||
AmbiguousBundleIdentifierError,
|
||||
getBundleIdentifierAsync,
|
||||
} from '../../project/ios/bundleIdentifier';
|
||||
import { ArchiveSource } from '../ArchiveSource';
|
||||
import { resolveArchiveSource } from '../commons';
|
||||
import { SubmissionContext } from '../context';
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
AppSpecificPasswordSourceType,
|
||||
} from './AppSpecificPasswordSource';
|
||||
import { AscApiKeySource, AscApiKeySourceType } from './AscApiKeySource';
|
||||
import { CREDENTIALS_SERVICE_SOURCE, CredentialsServiceSource } from './CredentialsServiceSource';
|
||||
import IosSubmitter, { IosSubmissionOptions } from './IosSubmitter';
|
||||
|
||||
export default class IosSubmitCommand {
|
||||
@@ -30,23 +33,6 @@ export default class IosSubmitCommand {
|
||||
return await submitter.submitAsync();
|
||||
}
|
||||
|
||||
private async resolveCredentialSubmissionOptionsAsync(): Promise<
|
||||
| { appSpecificPasswordSource: Result<AppSpecificPasswordSource> }
|
||||
| { ascApiKeySource: Result<AscApiKeySource> }
|
||||
> {
|
||||
// if an App Specific Password env var is not specified, use ASC Api Key
|
||||
const appSpecificPasswordSource = this.resolveAppSpecificPasswordSource();
|
||||
const shouldSkipAppSpecificPasswordSource =
|
||||
!appSpecificPasswordSource.ok &&
|
||||
appSpecificPasswordSource.enforceError() instanceof MissingCredentialsError;
|
||||
if (!shouldSkipAppSpecificPasswordSource) {
|
||||
return { appSpecificPasswordSource: this.resolveAppSpecificPasswordSource() };
|
||||
}
|
||||
|
||||
const ascApiKeySource = this.resolveAscApiKeySource();
|
||||
return { ascApiKeySource };
|
||||
}
|
||||
|
||||
private async resolveSubmissionOptionsAsync(): Promise<IosSubmissionOptions> {
|
||||
const archiveSource = this.resolveArchiveSource();
|
||||
const credentialsSource = await this.resolveCredentialSubmissionOptionsAsync();
|
||||
@@ -56,15 +42,18 @@ export default class IosSubmitCommand {
|
||||
: null;
|
||||
const maybeAscApiKeySource =
|
||||
'ascApiKeySource' in credentialsSource ? credentialsSource.ascApiKeySource : null;
|
||||
const maybeCredentialsServiceSource =
|
||||
'credentialsServiceSource' in credentialsSource
|
||||
? credentialsSource.credentialsServiceSource
|
||||
: null;
|
||||
const ascAppIdentifier = await this.resolveAscAppIdentifierAsync();
|
||||
const appleIdUsername = await this.resolveAppleIdUsernameAsync();
|
||||
|
||||
const errored = [
|
||||
archiveSource,
|
||||
...(maybeAppSpecificPasswordSource ? [maybeAppSpecificPasswordSource] : []),
|
||||
...(maybeAscApiKeySource ? [maybeAscApiKeySource] : []),
|
||||
...(maybeCredentialsServiceSource ? [maybeCredentialsServiceSource] : []),
|
||||
ascAppIdentifier,
|
||||
appleIdUsername,
|
||||
].filter(r => !r.ok);
|
||||
if (errored.length > 0) {
|
||||
const message = errored.map(err => err.reason?.message).join('\n');
|
||||
@@ -74,7 +63,6 @@ export default class IosSubmitCommand {
|
||||
|
||||
return {
|
||||
projectId: this.ctx.projectId,
|
||||
appleIdUsername: appleIdUsername.enforceValue(),
|
||||
ascAppIdentifier: ascAppIdentifier.enforceValue(),
|
||||
archiveSource: archiveSource.enforceValue(),
|
||||
...(maybeAppSpecificPasswordSource
|
||||
@@ -87,6 +75,69 @@ export default class IosSubmitCommand {
|
||||
ascApiKeySource: maybeAscApiKeySource.enforceValue(),
|
||||
}
|
||||
: null),
|
||||
...(maybeCredentialsServiceSource
|
||||
? {
|
||||
credentialsServiceSource: maybeCredentialsServiceSource.enforceValue(),
|
||||
}
|
||||
: null),
|
||||
};
|
||||
}
|
||||
|
||||
private async maybeGetIosBundleIdentifierAsync(): Promise<Result<string | null>> {
|
||||
try {
|
||||
return result(await getBundleIdentifierAsync(this.ctx.projectDir, this.ctx.exp));
|
||||
} catch (error: any) {
|
||||
if (error instanceof AmbiguousBundleIdentifierError) {
|
||||
Log.warn(
|
||||
'bundleIdentifier in the Xcode project is ambiguous, specify it via "bundleIdentifier" field in the submit profile in the eas.json.'
|
||||
);
|
||||
return result(null);
|
||||
}
|
||||
return result(
|
||||
new Error(`Failed to resolve bundleIdentifier in the Xcode project: ${error.message}.`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveCredentialSubmissionOptionsAsync(): Promise<
|
||||
| { appSpecificPasswordSource: Result<AppSpecificPasswordSource> }
|
||||
| { ascApiKeySource: Result<AscApiKeySource> }
|
||||
| { credentialsServiceSource: Result<CredentialsServiceSource> }
|
||||
> {
|
||||
const ascApiKeySource = this.resolveAscApiKeySource();
|
||||
const shouldSkipAscApiKeySource =
|
||||
!ascApiKeySource.ok && ascApiKeySource.enforceError() instanceof MissingCredentialsError;
|
||||
if (!shouldSkipAscApiKeySource) {
|
||||
return { ascApiKeySource };
|
||||
}
|
||||
|
||||
const appSpecificPasswordSource = this.resolveAppSpecificPasswordSource();
|
||||
const shouldSkipAppSpecificPasswordSource =
|
||||
!appSpecificPasswordSource.ok &&
|
||||
appSpecificPasswordSource.enforceError() instanceof MissingCredentialsError;
|
||||
if (!shouldSkipAppSpecificPasswordSource) {
|
||||
return { appSpecificPasswordSource: this.resolveAppSpecificPasswordSource() };
|
||||
}
|
||||
|
||||
let bundleIdentifier =
|
||||
this.ctx.applicationIdentifierOverride ?? this.ctx.profile.bundleIdentifier;
|
||||
if (!bundleIdentifier) {
|
||||
const bundleIdentifierResult = await this.maybeGetIosBundleIdentifierAsync();
|
||||
if (!bundleIdentifierResult.ok) {
|
||||
return {
|
||||
credentialsServiceSource: result(bundleIdentifierResult.reason),
|
||||
};
|
||||
}
|
||||
const bundleIdentifierValue = bundleIdentifierResult.enforceValue();
|
||||
if (bundleIdentifierValue) {
|
||||
bundleIdentifier = bundleIdentifierValue;
|
||||
}
|
||||
}
|
||||
return {
|
||||
credentialsServiceSource: result({
|
||||
sourceType: CREDENTIALS_SERVICE_SOURCE,
|
||||
bundleIdentifier,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -129,7 +180,11 @@ export default class IosSubmitCommand {
|
||||
});
|
||||
}
|
||||
|
||||
return result({ sourceType: AscApiKeySourceType.credentialsService });
|
||||
return result(
|
||||
new MissingCredentialsError(
|
||||
'Set the ascApiKeyPath, ascApiKeyIssuerId and ascApiKeyId fields in eas.json.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private resolveArchiveSource(): Result<ArchiveSource> {
|
||||
@@ -164,40 +219,4 @@ export default class IosSubmitCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveAppleIdUsernameAsync(): Promise<Result<string>> {
|
||||
if (this.ctx.profile.appleId) {
|
||||
return result(this.ctx.profile.appleId);
|
||||
}
|
||||
|
||||
const envAppleId = getenv.string('EXPO_APPLE_ID', '');
|
||||
if (envAppleId) {
|
||||
return result(envAppleId);
|
||||
}
|
||||
|
||||
if (this.ctx.credentialsCtx.appStore.authCtx?.appleId) {
|
||||
return result(this.ctx.credentialsCtx.appStore.authCtx.appleId);
|
||||
}
|
||||
|
||||
// Get the email address that was last used and set it as
|
||||
// the default value for quicker authentication.
|
||||
const lastAppleId = await UserSettings.getAsync('appleId', null);
|
||||
|
||||
if (this.ctx.nonInteractive) {
|
||||
if (lastAppleId) {
|
||||
return result(lastAppleId);
|
||||
} else {
|
||||
return result(new Error('Set appleId in the submit profile (eas.json).'));
|
||||
}
|
||||
}
|
||||
|
||||
const { appleId } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'appleId',
|
||||
message: `Enter your Apple ID:`,
|
||||
validate: (val: string) => !!val,
|
||||
initial: lastAppleId ?? undefined,
|
||||
});
|
||||
return result(appleId);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,34 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { SubmissionEvent } from '../../analytics/events';
|
||||
import { MinimalAscApiKey } from '../../credentials/ios/credentials';
|
||||
import { IosSubmissionConfigInput, SubmissionFragment } from '../../graphql/generated';
|
||||
import { SubmissionMutation } from '../../graphql/mutations/SubmissionMutation';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { Archive, ArchiveSource, getArchiveAsync } from '../ArchiveSource';
|
||||
import BaseSubmitter, { SubmissionInput } from '../BaseSubmitter';
|
||||
import { SubmissionContext } from '../context';
|
||||
import {
|
||||
ArchiveSourceSummaryFields,
|
||||
formatArchiveSourceSummary,
|
||||
printSummary,
|
||||
} from '../utils/summary';
|
||||
import {
|
||||
AppSpecificPasswordCredentials,
|
||||
AppSpecificPasswordSource,
|
||||
getAppSpecificPasswordAsync,
|
||||
getAppSpecificPasswordLocallyAsync,
|
||||
} from './AppSpecificPasswordSource';
|
||||
import {
|
||||
AscApiKeyFromExpoServers,
|
||||
AscApiKeyResult,
|
||||
AscApiKeySource,
|
||||
getAscApiKeyResultAsync,
|
||||
getAscApiKeyLocallyAsync,
|
||||
} from './AscApiKeySource';
|
||||
import {
|
||||
CredentialsServiceSource,
|
||||
getFromCredentialsServiceAsync,
|
||||
} from './CredentialsServiceSource';
|
||||
|
||||
export interface IosSubmissionOptions
|
||||
extends Pick<IosSubmissionConfigInput, 'appleIdUsername' | 'ascAppIdentifier'> {
|
||||
@@ -29,32 +36,77 @@ export interface IosSubmissionOptions
|
||||
archiveSource: ArchiveSource;
|
||||
appSpecificPasswordSource?: AppSpecificPasswordSource;
|
||||
ascApiKeySource?: AscApiKeySource;
|
||||
credentialsServiceSource?: CredentialsServiceSource;
|
||||
}
|
||||
|
||||
interface ResolvedSourceOptions {
|
||||
archive: Archive;
|
||||
appSpecificPassword?: string;
|
||||
ascApiKeyResult?: AscApiKeyResult;
|
||||
credentials: {
|
||||
appSpecificPassword?: AppSpecificPasswordCredentials;
|
||||
ascApiKeyResult?: AscApiKeyResult;
|
||||
};
|
||||
}
|
||||
|
||||
export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmissionOptions> {
|
||||
async submitAsync(): Promise<SubmissionFragment> {
|
||||
const resolvedSourceOptions = await this.resolveSourceOptionsAsync();
|
||||
const submissionConfig = await this.formatSubmissionConfigAsync(
|
||||
this.options,
|
||||
resolvedSourceOptions
|
||||
);
|
||||
export default class IosSubmitter extends BaseSubmitter<
|
||||
Platform.IOS,
|
||||
ResolvedSourceOptions,
|
||||
IosSubmissionOptions
|
||||
> {
|
||||
constructor(ctx: SubmissionContext<Platform.IOS>, options: IosSubmissionOptions) {
|
||||
const sourceOptionsResolver = {
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
archive: async () => await getArchiveAsync(this.options.archiveSource),
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
credentials: async () => {
|
||||
const maybeAppSpecificPassword = this.options.appSpecificPasswordSource
|
||||
? await getAppSpecificPasswordLocallyAsync(
|
||||
this.ctx,
|
||||
this.options.appSpecificPasswordSource
|
||||
)
|
||||
: null;
|
||||
const maybeAppStoreConnectApiKey = this.options.ascApiKeySource
|
||||
? await getAscApiKeyLocallyAsync(this.ctx, this.options.ascApiKeySource)
|
||||
: null;
|
||||
const maybeAscOrAspFromCredentialsService = this.options.credentialsServiceSource
|
||||
? await getFromCredentialsServiceAsync(this.ctx, this.options.credentialsServiceSource)
|
||||
: null;
|
||||
return {
|
||||
...(maybeAppSpecificPassword ? { appSpecificPassword: maybeAppSpecificPassword } : null),
|
||||
...(maybeAppStoreConnectApiKey ? { ascApiKeyResult: maybeAppStoreConnectApiKey } : null),
|
||||
...(maybeAscOrAspFromCredentialsService ? maybeAscOrAspFromCredentialsService : null),
|
||||
};
|
||||
},
|
||||
};
|
||||
const sourceOptionsAnalytics = {
|
||||
archive: {
|
||||
attemptEvent: SubmissionEvent.GATHER_ARCHIVE_ATTEMPT,
|
||||
successEvent: SubmissionEvent.GATHER_ARCHIVE_SUCCESS,
|
||||
failureEvent: SubmissionEvent.GATHER_ARCHIVE_FAIL,
|
||||
},
|
||||
credentials: {
|
||||
attemptEvent: SubmissionEvent.GATHER_CREDENTIALS_ATTEMPT,
|
||||
successEvent: SubmissionEvent.GATHER_CREDENTIALS_SUCCESS,
|
||||
failureEvent: SubmissionEvent.GATHER_CREDENTIALS_FAIL,
|
||||
},
|
||||
};
|
||||
super(ctx, options, sourceOptionsResolver, sourceOptionsAnalytics);
|
||||
}
|
||||
|
||||
public async createSubmissionInputAsync(
|
||||
resolvedSourceOptions: ResolvedSourceOptions
|
||||
): Promise<SubmissionInput<Platform.IOS>> {
|
||||
const submissionConfig = this.formatSubmissionConfig(this.options, resolvedSourceOptions);
|
||||
|
||||
printSummary(
|
||||
this.prepareSummaryData(this.options, resolvedSourceOptions),
|
||||
SummaryHumanReadableKeys
|
||||
);
|
||||
|
||||
return await this.createSubmissionAsync({
|
||||
return {
|
||||
projectId: this.options.projectId,
|
||||
submissionConfig,
|
||||
buildId: resolvedSourceOptions.archive.build?.id,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
protected async createPlatformSubmissionAsync({
|
||||
@@ -69,35 +121,30 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveSourceOptionsAsync(): Promise<ResolvedSourceOptions> {
|
||||
const archive = await getArchiveAsync(this.options.archiveSource);
|
||||
const maybeAppSpecificPassword = this.options.appSpecificPasswordSource
|
||||
? await getAppSpecificPasswordAsync(this.options.appSpecificPasswordSource)
|
||||
: null;
|
||||
const maybeAppStoreConnectApiKey = this.options.ascApiKeySource
|
||||
? await getAscApiKeyResultAsync(this.ctx, this.options.ascApiKeySource)
|
||||
: null;
|
||||
return {
|
||||
archive,
|
||||
...(maybeAppSpecificPassword ? { appSpecificPassword: maybeAppSpecificPassword } : null),
|
||||
...(maybeAppStoreConnectApiKey ? { ascApiKeyResult: maybeAppStoreConnectApiKey } : null),
|
||||
};
|
||||
}
|
||||
|
||||
private async formatSubmissionConfigAsync(
|
||||
private formatSubmissionConfig(
|
||||
options: IosSubmissionOptions,
|
||||
{ archive, appSpecificPassword, ascApiKeyResult }: ResolvedSourceOptions
|
||||
): Promise<IosSubmissionConfigInput> {
|
||||
{ archive, credentials }: ResolvedSourceOptions
|
||||
): IosSubmissionConfigInput {
|
||||
const { appSpecificPassword, ascApiKeyResult } = credentials;
|
||||
const { appleIdUsername, ascAppIdentifier } = options;
|
||||
return {
|
||||
ascAppIdentifier,
|
||||
appleIdUsername,
|
||||
archiveUrl: archive.url,
|
||||
appleAppSpecificPassword: appSpecificPassword,
|
||||
...(appSpecificPassword ? this.formatAppSpecificPassword(appSpecificPassword) : null),
|
||||
...(ascApiKeyResult?.result ? this.formatAscApiKeyResult(ascApiKeyResult.result) : null),
|
||||
};
|
||||
}
|
||||
|
||||
private formatAppSpecificPassword(
|
||||
appSpecificPassword: AppSpecificPasswordCredentials
|
||||
): Pick<IosSubmissionConfigInput, 'appleAppSpecificPassword' | 'appleIdUsername'> {
|
||||
return {
|
||||
appleAppSpecificPassword: appSpecificPassword.password,
|
||||
appleIdUsername: appSpecificPassword.appleIdUsername,
|
||||
};
|
||||
}
|
||||
|
||||
private formatAscApiKeyResult(
|
||||
result: MinimalAscApiKey | AscApiKeyFromExpoServers
|
||||
): Pick<IosSubmissionConfigInput, 'ascApiKey'> | Pick<IosSubmissionConfigInput, 'ascApiKeyId'> {
|
||||
@@ -114,15 +161,16 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
|
||||
|
||||
private prepareSummaryData(
|
||||
options: IosSubmissionOptions,
|
||||
{ archive, ascApiKeyResult }: ResolvedSourceOptions
|
||||
{ archive, credentials }: ResolvedSourceOptions
|
||||
): SummaryData {
|
||||
const { appleIdUsername, ascAppIdentifier, projectId } = options;
|
||||
const { ascApiKeyResult, appSpecificPassword } = credentials;
|
||||
const { ascAppIdentifier, projectId } = options;
|
||||
|
||||
// structuring order affects table rows order
|
||||
return {
|
||||
ascAppIdentifier,
|
||||
appleIdUsername: appleIdUsername ?? undefined,
|
||||
projectId,
|
||||
...(appSpecificPassword ? { appleIdUsername: appSpecificPassword.appleIdUsername } : null),
|
||||
...(ascApiKeyResult ? { formattedAscApiKey: formatAscApiKeySummary(ascApiKeyResult) } : null),
|
||||
...formatArchiveSourceSummary(archive),
|
||||
};
|
||||
@@ -143,7 +191,7 @@ const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
|
||||
archiveUrl: 'Archive URL',
|
||||
archivePath: 'Archive Path',
|
||||
formattedBuild: 'Build',
|
||||
formattedAscApiKey: 'App Store Connect Api Key',
|
||||
formattedAscApiKey: 'App Store Connect API Key',
|
||||
};
|
||||
|
||||
function formatAscApiKeySummary({ summary }: AscApiKeyResult): string {
|
||||
|
||||
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user