Compare commits

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

No files matched your search

+1
View File
@@ -11,6 +11,7 @@ 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.

After

Width:  |  Height:  |  Size: 61 KiB

-7
View File
@@ -21,13 +21,6 @@ body:
label: Summary
description: Describe the issue in 1 or 2 sentences
placeholder: Clearly describe what the expected behavior is vs. what is actually happening. This should be as short as possible, while still communicating all the necessary information. If your summary is just 'X library/method isn't working', then you need to continue debugging yourself and provide more information.
- type: textarea
validations:
required: true
attributes:
label: Managed or bare?
description: See the [Workflows](https://docs.expo.dev/introduction/managed-vs-bare/) page for more information.
placeholder: Is your project managed or bare? Bare - Android and/or iOS native projects exist in your repo; Managed - you don't have native code (custom or 3rd party) in your project.
- type: textarea
validations:
required: true
+1
View File
@@ -3,6 +3,7 @@
# Checklist
- [ ] I've added an entry to [CHANGELOG.md](https://github.com/expo/eas-cli/blob/main/CHANGELOG.md#main) if necessary. You can comment this pull request with `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]` and CHANGELOG.md will be updated automatically.
- [ ] I've tagged the changelog entry with `[EAS BUILD API]` if it's a part of a breaking change to EAS Build API (only possible when updating `@expo/eas-build-job` package).
# Why
+1 -1
View File
@@ -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@v2
- uses: actions/setup-node@v1
with:
node-version: '16'
- name: Checkout Pull Request
+5 -5
View File
@@ -25,7 +25,7 @@ jobs:
needs: release
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/setup-node@v1
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@v2
- uses: actions/setup-node@v1
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@v2
- uses: actions/setup-node@v1
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@v2
- uses: actions/setup-node@v1
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@v2
- uses: actions/setup-node@v1
with:
node-version: '16'
- name: Install dependencies
+1 -1
View File
@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/setup-node@v1
with:
node-version: '16'
- uses: preactjs/compressed-size-action@v2
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- run: yarn install --frozen-lockfile --check-files
+10 -14
View File
@@ -1,18 +1,14 @@
# Build directories
packages/*/build
packages/eas-cli/dist
# Code editors
.idea
.history
# macOS
.DS_Store
# Node
node_modules
*-debug.log
*-error.log
# Tests
**/build/**
!packages/eas-cli/src/commands/build/**
!packages/eas-cli/src/build/**
dist
/package-lock.json
tmp
node_modules
coverage
.history
.DS_Store
+1 -1
View File
@@ -2,7 +2,7 @@
"printWidth": 100,
"tabWidth": 2,
"singleQuote": true,
"bracketSameLine": true,
"jsxBracketSameLine": true,
"trailingComma": "es5",
"arrowParens": "avoid"
}
+22 -157
View File
@@ -8,159 +8,24 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Ask user to select profile if `release` does not exist. ([#829](https://github.com/expo/eas-cli/pull/829) by [@dsokal](https://github.com/dsokal))
- Add `build:inspect` command. ([#834](https://github.com/expo/eas-cli/pull/834) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Fix validating tool versions in eas.json. ([#832](https://github.com/expo/eas-cli/pull/832) by [@dsokal](https://github.com/dsokal))
- Fix submission URL. ([#845](https://github.com/expo/eas-cli/pull/845) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Warn when building managed project with SDK < 41. ([#833](https://github.com/expo/eas-cli/pull/833) by [@dsokal](https://github.com/dsokal))
## [0.40.0](https://github.com/expo/eas-cli/releases/tag/v0.40.0) - 2021-12-08
### 🛠 Breaking changes
- [eas-cli] change eas update:publish to eas update. ([#830](https://github.com/expo/eas-cli/pull/830) by [@jkhales](https://github.com/jkhales))
### 🐛 Bug fixes
- Pass build profile environment to credentials command. ([#828](https://github.com/expo/eas-cli/pull/828) by [@quinlanj](https://github.com/quinlanj))
## [0.39.0](https://github.com/expo/eas-cli/releases/tag/v0.39.0) - 2021-12-06
### 🎉 New features
- Set runtime version in `eas update:configure`. ([#811](https://github.com/expo/eas-cli/pull/811) by [@jkhales](https://github.com/jkhales))
- Print project workflow in `eas diagnostics`. ([#822](https://github.com/expo/eas-cli/pull/822) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix submit for multi-target projects where ascAppId is not specified. ([#809](https://github.com/expo/eas-cli/pull/809) by [@wkozyra95](https://github.com/wkozyra95))
- Fix building iOS projects with Apple Watch companion app. ([#812](https://github.com/expo/eas-cli/pull/812), [#817](https://github.com/expo/eas-cli/pull/817), [#821](https://github.com/expo/eas-cli/pull/821) by [@jkhales](https://github.com/jkhales) + [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Improve error message for missing ascAppId. ([#813](https://github.com/expo/eas-cli/pull/813) by [@wkozyra95](https://github.com/wkozyra95))
- Add `react-native` version to metadata. ([#823](https://github.com/expo/eas-cli/pull/823) by [@wkozyra95](https://github.com/wkozyra95))
## [0.38.3](https://github.com/expo/eas-cli/releases/tag/v0.38.3) - 2021-11-29
### 🐛 Bug fixes
- Print warning about outdated version to stderr. ([#805](https://github.com/expo/eas-cli/pull/805) by [@wkozyra95](https://github.com/wkozyra95))
- Fix Apple sign in capabilities. ([#806](https://github.com/expo/eas-cli/pull/806) by [@brentvatne](https://github.com/brentvatne))
## [0.38.2](https://github.com/expo/eas-cli/releases/tag/v0.38.2) - 2021-11-26
### 🎉 New features
- Add support for extending submit profiles. ([#794](https://github.com/expo/eas-cli/pull/794) by [@dsokal](https://github.com/dsokal))
- Add command: `eas update:configure`. ([#800](https://github.com/expo/eas-cli/pull/800) by [@jkhales](https://github.com/jkhales))
### 🐛 Bug fixes
- Fix reading app version and app build version of iOS projects. ([#798](https://github.com/expo/eas-cli/pull/798) by [@dsokal](https://github.com/dsokal))
- Fix push notifications entitlements. ([#801](https://github.com/expo/eas-cli/pull/801) by [@brentvatne](https://github.com/brentvatne))
## [0.38.1](https://github.com/expo/eas-cli/releases/tag/v0.38.1) - 2021-11-25
### 🐛 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))
## [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))
- Add ASC Api Key generation workflow. ([#718](https://github.com/expo/eas-cli/pull/718) by [@quinlanj](https://github.com/quinlanj))
- Add support for removal of ASC Api Keys. ([#721](https://github.com/expo/eas-cli/pull/721) by [@quinlanj](https://github.com/quinlanj))
- Allow users to assign an ASC Api Key to their project. ([#719](https://github.com/expo/eas-cli/pull/719) by [@quinlanj](https://github.com/quinlanj))
- Add setup support for ASC Api Keys. ([#733](https://github.com/expo/eas-cli/pull/733) by [@quinlanj](https://github.com/quinlanj))
- Show initiating user display name when selecting a build to submit. ([#730](https://github.com/expo/eas-cli/pull/730) by [@barthap](https://github.com/barthap))
- Handle Apple servers maintenance error in `eas submit`. ([#738](https://github.com/expo/eas-cli/pull/738) by [@barthap](https://github.com/barthap))
- Integrate ASC API Key with submissions workflow. ([#737](https://github.com/expo/eas-cli/pull/737) by [@quinlanj](https://github.com/quinlanj))
- Change EAS API server domain. ([#744](https://github.com/expo/eas-cli/pull/744) by [@ide](https://github.com/ide))
- Integrate ASC Api Key with submissions workflow. ([#737](https://github.com/expo/eas-cli/pull/737) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
### 🧹 Chores
- Clean up credentials prompt method. ([#728](https://github.com/expo/eas-cli/pull/728) by [@quinlanj](https://github.com/quinlanj))
- Bump `@expo/apple-utils` to 0.0.0-alpha.26. ([#723](https://github.com/expo/eas-cli/pull/723) by [@brentvatne](https://github.com/brentvatne))
- Bump @expo/apple-utils to 0.0.0-alpha.26. ([#723](https://github.com/expo/eas-cli/pull/723] by [@brentvatne](https://github.com/brentvatne))
- Grammar: replace setup with set up. ([#735](https://github.com/expo/eas-cli/pull/735) by [@quinlanj](https://github.com/quinlanj))
- Improve VCS workflow migration experience. ([#732](https://github.com/expo/eas-cli/pull/732) by [@wkozyra95](https://github.com/wkozyra95))
## [0.34.1](https://github.com/expo/eas-cli/releases/tag/v0.34.1) - 2021-11-02
## [0.34.1](https://github.com/expo/eas-cli/releases/tag/0.34.1) - 2021-11-02
### 🐛 Bug fixes
@@ -168,16 +33,16 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores
- Add App Store Connect API Key graphql type and print support. ([#717](https://github.com/expo/eas-cli/pull/717) by [@quinlanj](https://github.com/quinlanj))
- Add App Store Connect Api Key graphql type and print support. ([#717](https://github.com/expo/eas-cli/pull/717) by [@quinlanj](https://github.com/quinlanj))
## [0.34.0](https://github.com/expo/eas-cli/releases/tag/v0.34.0) - 2021-11-01
## [0.34.0](https://github.com/expo/eas-cli/releases/tag/0.34.0) - 2021-11-01
### 🛠 Breaking changes
- Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701) and [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal))
- `[EAS BUILD API]` Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701) and [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal))
- Don't print logs to `stderr`. ([#708](https://github.com/expo/eas-cli/pull/708) by [@dsokal](https://github.com/dsokal))
- Remove automatic migration for legacy `eas.json` format introduced in [v0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2). ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
- Implement no-commit build workflow. Add required `cli` field in root of `eas.json`. ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
- Add required `cli` field in root of `eas.json` ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
@@ -192,7 +57,7 @@ This is the log of notable changes to EAS CLI and related packages.
- Remove unneeded parameters. ([#710](https://github.com/expo/eas-cli/pull/710) by [@quinlanj](https://github.com/quinlanj))
- Enforce additionalTypenames for graphql queries. ([#709](https://github.com/expo/eas-cli/pull/709) by [@quinlanj](https://github.com/quinlanj))
## [0.33.1](https://github.com/expo/eas-cli/releases/tag/v0.33.1) - 2021-10-22
## [0.33.1](https://github.com/expo/eas-cli/releases/tag/0.33.1) - 2021-10-22
### 🎉 New features
@@ -202,7 +67,7 @@ This is the log of notable changes to EAS CLI and related packages.
- [eas-cli] Skip validating updates scripts integration, which no longer exist in SDK 43+ ([#706](https://github.com/expo/eas-cli/pull/706) by [@brentvatne](https://github.com/brentvatne))
## [0.33.0](https://github.com/expo/eas-cli/releases/tag/v0.33.0) - 2021-10-20
## [0.33.0](https://github.com/expo/eas-cli/releases/tag/0.33.0) - 2021-10-20
### 🎉 New features
@@ -225,11 +90,11 @@ This is the log of notable changes to EAS CLI and related packages.
- Skip the second prompt for Apple ID if the user is already signed in with Apple. ([#691](https://github.com/expo/eas-cli/pull/691) by [@dsokal](https://github.com/dsokal))
- Unify reading `app.json`. ([#692](https://github.com/expo/eas-cli/pull/692) by [@dsokal](https://github.com/dsokal))
## [0.32.0](https://github.com/expo/eas-cli/releases/tag/v0.32.0) - 2021-10-15
## [0.32.0](https://github.com/expo/eas-cli/releases/tag/0.32.0) - 2021-10-15
### 🎉 New features
- Add App Store Connect API Key support to iOS submissions. ([#678](https://github.com/expo/eas-cli/pull/678) by [@quinlanj](https://github.com/quinlanj))
- Add App Store Connect Api Key support to iOS submissions. ([#678](https://github.com/expo/eas-cli/pull/678) by [@quinlanj](https://github.com/quinlanj))
- Create/list/revoke App Store Connect Api keys via Apple apis. ([#687](https://github.com/expo/eas-cli/pull/687) by [@quinlanj](https://github.com/quinlanj))
- Add ability to select a build from a list in `eas submit` interactive mode. ([#688](https://github.com/expo/eas-cli/pull/688) by [@barthap](https://github.com/barthap))
@@ -239,16 +104,16 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores
- Add App Store Connect API Key fields to `eas.json`. ([#684](https://github.com/expo/eas-cli/pull/684) by [@quinlanj](https://github.com/quinlanj))
- Add App Store Connect Api Key fields to `eas.json`. ([#684](https://github.com/expo/eas-cli/pull/684) by [@quinlanj](https://github.com/quinlanj))
- Enable no-underscore-dangle eslint rule. ([#686](https://github.com/expo/eas-cli/pull/686) by [@dsokal](https://github.com/dsokal))
## [0.31.1](https://github.com/expo/eas-cli/releases/tag/v0.31.1) - 2021-10-08
## [0.31.1](https://github.com/expo/eas-cli/releases/tag/0.31.1) - 2021-10-08
### 🐛 Bug fixes
- Google Service Account Keys: Fix non-interactive bug in credentials service workflow ([#682](https://github.com/expo/eas-cli/pull/682) by [@quinlanj](https://github.com/quinlanj))
## [0.31.0](https://github.com/expo/eas-cli/releases/tag/v0.31.0) - 2021-10-08
## [0.31.0](https://github.com/expo/eas-cli/releases/tag/0.31.0) - 2021-10-08
### 🛠 Breaking changes
@@ -259,7 +124,7 @@ This is the log of notable changes to EAS CLI and related packages.
- Integrate credentials service with Android submissions. ([#664](https://github.com/expo/eas-cli/pull/664) by [@quinlanj](https://github.com/quinlanj))
- Add option to review ad-hoc devices when reusing provisioning profile. ([#673](https://github.com/expo/eas-cli/pull/673) by [@dsokal](https://github.com/dsokal))
## [0.30.1](https://github.com/expo/eas-cli/releases/tag/v0.30.1) - 2021-10-06
## [0.30.1](https://github.com/expo/eas-cli/releases/tag/0.30.1) - 2021-10-06
### 🐛 Bug fixes
@@ -638,7 +503,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
- Remove "Auto" option for `schemeBuildConfiguration` and make the old "Auto" behaviour the default. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
- `[EAS BUILD API]` Remove "Auto" option for `schemeBuildConfiguration` and make the old "Auto" behaviour the default. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
- Remove `experimental.disableIosBundleIdentifierValidation` flag from eas.json. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
- Deprecate `android.package` and `ios.bundleIdentifier` in app config for generic projects. EAS CLI depends on the values in native code now. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
- Remove application id synchronization (`android.package` and `ios.bundleIdentifier`) between app.json and native code when running builds. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
+7 -1
View File
@@ -1,4 +1,10 @@
## Releasing a new version
1. Run `yarn release` in the repository root folder.
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
2. Wait, the `release` workflow on GitHub Actions is going to take care of the rest (including publishing packages to npm and updating the changelog).
3. Check the changelog. If any of the entries have been tagged with `[EAS BUILD API]`
- Open `package.json` and note the version of `@expo/eas-build-job` used in the eas-cli.
- Find the `Publish` commit in [expo/eas-build](https://github.com/expo/eas-build/commits) for that version. In most cases, it will be the latest `Publish` commit.
- Find the `eas-cli-local-build-plugin` version published in that commit.
![example publish commit](./.gh-assets/eas-build-publish-commit.png)
- Run `npm dist-tags add eas-cli-local-build-plugin@VERSION latest` with the version from the previous step.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.41.0"
"version": "0.34.1"
}
+5 -7
View File
@@ -1,8 +1,6 @@
{
"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",
@@ -10,8 +8,8 @@
"start": "lerna run watch --parallel",
"watch": "yarn start",
"eas": "packages/eas-cli/bin/run",
"lint": "eslint .",
"release": "./scripts/bin/run update-local-plugin && lerna version",
"lint": "eslint . --ext .ts --ext .js",
"release": "lerna version",
"test": "jest",
"clean": "lerna run clean && rm -rf node_modules coverage"
},
@@ -26,9 +24,9 @@
"jest": "27.1.0",
"jest-watch-typeahead": "0.6.4",
"lerna": "4.0.0",
"prettier": "2.4.1",
"prettier": "2.3.2",
"ts-jest": "27.0.5",
"typescript": "4.5.2"
"typescript": "4.4.2"
},
"volta": {
"node": "16.10.0",
+44 -76
View File
@@ -7,9 +7,9 @@ EAS command line tool
[![Downloads/week](https://img.shields.io/npm/dw/eas-cli.svg)](https://npmjs.org/package/eas-cli)
[![License](https://img.shields.io/npm/l/eas-cli.svg)](https://github.com/expo/eas-cli/blob/main/package.json)
- [Installation](#installation)
- [Usage](#usage)
- [Commands](#commands)
* [Installation](#installation)
* [Usage](#usage)
* [Commands](#commands)
# Installation
@@ -40,7 +40,6 @@ eas --help COMMAND
* [`eas build`](#eas-build)
* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
* [`eas build:configure`](#eas-buildconfigure)
* [`eas build:inspect`](#eas-buildinspect)
* [`eas build:list`](#eas-buildlist)
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
* [`eas config`](#eas-config)
@@ -74,7 +73,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/login.js)_
## `eas account:logout`
@@ -88,7 +87,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/logout.js)_
## `eas account:view`
@@ -102,7 +101,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/view.js)_
## `eas analytics [STATUS]`
@@ -113,11 +112,11 @@ USAGE
$ eas analytics [STATUS]
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/analytics.js)_
## `eas build`
Start a build
start a build
```
USAGE
@@ -147,7 +146,7 @@ OPTIONS
--[no-]wait Wait for build(s) to complete
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/index.js)_
## `eas build:cancel [BUILD_ID]`
@@ -158,7 +157,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/cancel.js)_
## `eas build:configure`
@@ -172,39 +171,7 @@ OPTIONS
-p, --platform=(android|ios|all) Platform to configure
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:inspect`
Inspect the state of the project at specific build stages. Useful for troubleshooting.
```
USAGE
$ eas build:inspect
OPTIONS
-p, --platform=(android|ios)
(required)
-s, --stage=(archive|pre-build|post-build)
(required) Stage of the build you want to inspect.
archive - builds the project archive that would be uploaded to EAS when building
pre-build - prepares the project to be built with Gradle/Xcode. Does not run the native build.
post-build - builds the native project and leaves the output directory for inspection
--force
Delete OUTPUT_DIRECTORY if it already exists.
--output=OUTPUT_DIRECTORY
(required) Output directory.
--profile=PROFILE_NAME
Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.
--verbose
```
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/configure.js)_
## `eas build:list`
@@ -237,7 +204,7 @@ OPTIONS
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/list.js)_
## `eas build:view [BUILD_ID]`
@@ -251,7 +218,7 @@ OPTIONS
--json Enable JSON output, non-JSON messages will be printed to stderr
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/view.js)_
## `eas config`
@@ -266,7 +233,7 @@ OPTIONS
--profile=profile
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/config.js)_
## `eas credentials`
@@ -277,7 +244,7 @@ USAGE
$ eas credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/credentials.js)_
## `eas device:create`
@@ -288,7 +255,7 @@ USAGE
$ eas device:create
```
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/create.js)_
## `eas device:list`
@@ -302,7 +269,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/list.js)_
## `eas device:view [UDID]`
@@ -313,7 +280,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/view.js)_
## `eas diagnostics`
@@ -324,7 +291,7 @@ USAGE
$ eas diagnostics
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/diagnostics.js)_
## `eas help [COMMAND]`
@@ -341,7 +308,7 @@ OPTIONS
--all see all commands in CLI
```
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.5/src/commands/help.ts)_
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.3/src/commands/help.ts)_
## `eas project:info`
@@ -352,7 +319,7 @@ USAGE
$ eas project:info
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/project/info.js)_
## `eas project:init`
@@ -366,7 +333,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/project/init.ts)_
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/project/init.js)_
## `eas secret:create`
@@ -383,7 +350,7 @@ OPTIONS
--value=value Value of the secret
```
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/create.js)_
## `eas secret:delete`
@@ -400,7 +367,7 @@ DESCRIPTION
Unsure where to find the secret's ID? Run eas secret:list
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/delete.js)_
## `eas secret:list`
@@ -411,11 +378,11 @@ USAGE
$ eas secret:list
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/list.js)_
## `eas submit`
Submit build archive to App Store Connect
submit build archive to app store
```
USAGE
@@ -438,13 +405,14 @@ OPTIONS
--[no-]wait Wait for submission to complete
DESCRIPTION
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
See how to configure submits with eas.json:
https://docs.expo.dev/submit/eas-json/
ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/submit.js)_
## `eas webhook:create`
@@ -455,15 +423,15 @@ USAGE
$ eas webhook:create
OPTIONS
--event=(BUILD|SUBMIT) Event type that triggers the webhook
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
--secret=secret Secret used to create a hash signature of the request payload, provided in the
'Expo-Signature' header.
--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.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/create.js)_
## `eas webhook:delete [ID]`
@@ -477,7 +445,7 @@ ARGUMENTS
ID ID of the webhook to delete
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/delete.js)_
## `eas webhook:list`
@@ -488,10 +456,10 @@ USAGE
$ eas webhook:list
OPTIONS
--event=(BUILD|SUBMIT) Event type that triggers the webhook
--event=(BUILD) Event type that triggers the webhook
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/list.js)_
## `eas webhook:update`
@@ -502,16 +470,16 @@ USAGE
$ eas webhook:update
OPTIONS
--event=(BUILD|SUBMIT) Event type that triggers the webhook
--id=id (required) Webhook ID
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
--id=id (required) Webhook ID
--secret=secret Secret used to create a hash signature of the request payload, provided in the
'Expo-Signature' header.
--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.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/update.js)_
## `eas webhook:view ID`
@@ -525,5 +493,5 @@ ARGUMENTS
ID ID of the webhook to view
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.41.0/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/view.js)_
<!-- commandsstop -->
+1 -1
View File
@@ -1,5 +1,5 @@
overwrite: true
schema: 'https://staging-api.expo.dev/graphql'
schema: 'https://staging.exp.host/--/graphql'
documents:
- 'src/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
+184 -977
View File
File diff suppressed because it is too large. Load diff
+16 -16
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.41.0",
"version": "0.34.1",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -9,23 +9,24 @@
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.26",
"@expo/config": "6.0.11",
"@expo/config-plugins": "4.0.11",
"@expo/eas-build-job": "0.2.61",
"@expo/eas-json": "^0.41.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.1",
"@expo/prebuild-config": "3.0.11",
"@expo/config": "6.0.3",
"@expo/config-plugins": "4.0.3",
"@expo/eas-build-job": "0.2.57",
"@expo/eas-json": "^0.34.1",
"@expo/json-file": "8.2.33",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.14",
"@expo/plugin-warn-if-update-available": "1.7.0",
"@expo/prebuild-config": "3.0.3",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/rudder-sdk-node": "1.1.0",
"@expo/sdk-runtime-versions": "1.0.0",
"@expo/spawn-async": "1.5.0",
"@expo/timeago.js": "1.0.0",
"@oclif/command": "1.8.3",
"@oclif/config": "1.17.1",
"@oclif/command": "1.8.0",
"@oclif/config": "1.17.0",
"@oclif/errors": "1.3.5",
"@oclif/plugin-help": "3.2.5",
"@oclif/plugin-help": "3.2.3",
"@urql/core": "2.3.1",
"@urql/exchange-retry": "0.3.0",
"chalk": "4.1.2",
@@ -95,8 +96,7 @@
"mockdate": "^3.0.2",
"nock": "^13.0.5",
"tempy": "0.7.0",
"ts-deepmerge": "1.0.8",
"typescript": "4.5.2"
"ts-deepmerge": "1.0.8"
},
"engines": {
"node": ">=12.0.0"
+2 -2
View File
@@ -8,7 +8,7 @@ import { apiClient, getExpoApiBaseUrl } from '../api';
describe('apiClient', () => {
it('converts Expo APIv2 error to ApiV2Error', async () => {
nock(getExpoApiBaseUrl())
.post('/v2/test')
.post('/--/api/v2/test')
.reply(400, {
errors: [
{
@@ -39,7 +39,7 @@ describe('apiClient', () => {
});
it('does not convert non-APIv2 error to ApiV2Error', async () => {
nock(getExpoApiBaseUrl()).post('/v2/test').reply(500, 'Something went wrong');
nock(getExpoApiBaseUrl()).post('/--/api/v2/test').reply(500, 'Something went wrong');
let error: Error | null = null;
try {
@@ -3,8 +3,8 @@ import os from 'os';
import { URL } from 'url';
import { v4 as uuidv4 } from 'uuid';
import UserSettings from '../user/UserSettings';
import { easCliVersion } from '../utils/easCli';
import UserSettings from './user/UserSettings';
import { easCliVersion } from './utils/easCli';
const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
darwin: 'Mac',
-25
View File
@@ -1,25 +0,0 @@
import { Analytics, Event } from './events';
export type TrackingContext = Record<string, string | number | boolean>;
export async function withAnalyticsAsync<Result>(
fn: () => Promise<Result>,
analytics: {
attemptEvent: Event;
successEvent: Event;
failureEvent: Event;
trackingCtx: TrackingContext;
}
): Promise<Result> {
try {
const result = await fn();
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
return result;
} catch (error: any) {
Analytics.logEvent(analytics.failureEvent, {
...analytics.trackingCtx,
reason: error.message,
});
throw error;
}
}
-52
View File
@@ -1,52 +0,0 @@
import { logEvent } from './rudderstackClient';
export type Event = BuildEvent | SubmissionEvent;
export enum SubmissionEvent {
SUBMIT_COMMAND = 'submit cli submit command',
SUBMIT_COMMAND_ATTEMPT = 'submit cli attempt',
SUBMIT_COMMAND_SUCCESS = 'submit cli success',
SUBMIT_COMMAND_FAIL = 'submit cli fail',
GATHER_CREDENTIALS_ATTEMPT = 'submit cli gather credentials attempt',
GATHER_CREDENTIALS_SUCCESS = 'submit cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'submit cli gather credentials fail',
GATHER_ARCHIVE_ATTEMPT = 'submit cli gather archive attempt',
GATHER_ARCHIVE_SUCCESS = 'submit cli gather archive success',
GATHER_ARCHIVE_FAIL = 'submit cli gather archive fail',
SUBMIT_REQUEST_ATTEMPT = 'submit cli request attempt',
SUBMIT_REQUEST_SUCCESS = 'submit cli request success',
SUBMIT_REQUEST_FAIL = 'submit cli request fail',
}
export enum BuildEvent {
BUILD_COMMAND = 'build cli build command',
PROJECT_UPLOAD_ATTEMPT = 'build cli project upload attempt',
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
GATHER_CREDENTIALS_ATTEMPT = 'build cli gather credentials attempt',
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
CONFIGURE_PROJECT_ATTEMPT = 'build cli configure project attempt',
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
BUILD_REQUEST_ATTEMPT = 'build cli build request attempt',
BUILD_REQUEST_SUCCESS = 'build cli build request success',
BUILD_REQUEST_FAIL = 'build cli build request fail',
BUILD_STATUS_COMMAND = 'build cli build status',
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
CREDENTIALS_SYNC_UPDATE_LOCAL_ATTEMPT = 'build cli credentials sync update local attempt',
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
CREDENTIALS_SYNC_UPDATE_REMOTE_ATTEMPT = 'build cli credentials sync update remote attempt',
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
}
export class Analytics {
static logEvent(name: Event, properties: Record<string, any>): void {
logEvent(name, properties);
}
}
+3 -3
View File
@@ -4,7 +4,7 @@ import ApiV2Error from './ApiV2Error';
import { getAccessToken, getSessionSecret } from './user/sessionStorage';
export const apiClient = got.extend({
prefixUrl: getExpoApiBaseUrl() + '/v2/',
prefixUrl: getExpoApiBaseUrl() + '/--/api/v2/',
hooks: {
beforeRequest: [
(options: NormalizedOptions) => {
@@ -40,11 +40,11 @@ export const apiClient = got.extend({
export function getExpoApiBaseUrl(): string {
if (process.env.EXPO_STAGING) {
return `https://staging-api.expo.dev`;
return `https://staging.exp.host`;
} else if (process.env.EXPO_LOCAL) {
return `http://127.0.0.1:3000`;
} else {
return `https://api.expo.dev`;
return `https://exp.host`;
}
}
+13 -17
View File
@@ -11,7 +11,7 @@ import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
getApplicationIdAsync,
} from '../../project/android/applicationId';
import { resolveGradleBuildContextAsync } from '../../project/android/gradle';
import { GradleBuildContext, 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 { AndroidBuildContext, BuildContext, CommonContext } from '../context';
import { BuildContext } 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 createAndroidContextAsync(
ctx: CommonContext<Platform.ANDROID>
): Promise<AndroidBuildContext> {
export async function prepareAndroidBuildAsync(
ctx: BuildContext<Platform.ANDROID>
): Promise<BuildRequestSender> {
const { buildProfile } = ctx;
if (buildProfile.distribution === 'internal' && buildProfile.gradleCommand?.match(/bundle/)) {
@@ -58,26 +58,21 @@ 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);
return await ensureAndroidCredentialsAsync(ctx, gradleContext);
},
ensureProjectConfiguredAsync: async () => {
await validateAndSyncProjectConfigurationAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile: ctx.buildProfile,
buildProfile,
});
},
getMetadataContext: () => ({
gradleContext,
}),
prepareJobAsync: async (
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData<AndroidCredentials>
@@ -105,7 +100,8 @@ function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean
}
async function ensureAndroidCredentialsAsync(
ctx: BuildContext<Platform.ANDROID>
ctx: BuildContext<Platform.ANDROID>,
gradleContext?: GradleBuildContext
): Promise<CredentialsResult<AndroidCredentials> | undefined> {
if (!shouldProvideCredentials(ctx)) {
return;
@@ -113,7 +109,7 @@ async function ensureAndroidCredentialsAsync(
const androidApplicationIdentifier = await getApplicationIdAsync(
ctx.projectDir,
ctx.exp,
ctx.android.gradleContext
gradleContext
);
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
app: {
@@ -1,7 +1,7 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { AndroidVersionAutoIncrement, BuildProfile } from '@expo/eas-json';
import { AndroidBuildProfile, AndroidVersionAutoIncrement } from '@expo/eas-json';
import Log from '../../log';
import {
@@ -41,7 +41,7 @@ export async function validateAndSyncProjectConfigurationAsync({
}: {
projectDir: string;
exp: ExpoConfig;
buildProfile: BuildProfile<Platform.ANDROID>;
buildProfile: AndroidBuildProfile;
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const { autoIncrement } = buildProfile;
@@ -1,5 +1,5 @@
import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { AndroidBuildProfile } from '@expo/eas-json';
import path from 'path';
import slash from 'slash';
@@ -25,7 +25,7 @@ export async function prepareJobAsync(
jobData: JobData
): Promise<Job> {
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
const buildProfile: AndroidBuildProfile = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const { credentials } = jobData;
+38 -19
View File
@@ -3,8 +3,6 @@ import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import { withAnalyticsAsync } from '../analytics/common';
import { BuildEvent } from '../analytics/events';
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
import { BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildQuery } from '../graphql/queries/BuildQuery';
@@ -18,7 +16,9 @@ import { sleepAsync } from '../utils/promise';
import { getVcsClient } from '../vcs';
import { BuildContext } from './context';
import { runLocalBuildAsync } from './local';
import { collectMetadataAsync } from './metadata';
import { MetadataContext, collectMetadataAsync } from './metadata';
import { TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
import { printDeprecationWarnings } from './utils/printBuildInfo';
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
@@ -39,6 +39,7 @@ 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>;
}
@@ -54,17 +55,15 @@ export async function prepareBuildRequestForPlatformAsync<
const credentialsResult = await withAnalyticsAsync(
async () => await builder.ensureCredentialsAsync(ctx),
{
attemptEvent: BuildEvent.GATHER_CREDENTIALS_ATTEMPT,
successEvent: BuildEvent.GATHER_CREDENTIALS_SUCCESS,
failureEvent: BuildEvent.GATHER_CREDENTIALS_FAIL,
successEvent: Event.GATHER_CREDENTIALS_SUCCESS,
failureEvent: Event.GATHER_CREDENTIALS_FAIL,
trackingCtx: ctx.trackingCtx,
}
);
if (!ctx.skipProjectConfiguration) {
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(ctx), {
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
successEvent: Event.CONFIGURE_PROJECT_SUCCESS,
failureEvent: Event.CONFIGURE_PROJECT_FAIL,
trackingCtx: ctx.trackingCtx,
});
}
@@ -77,7 +76,7 @@ export async function prepareBuildRequestForPlatformAsync<
);
}
const projectArchive = ctx.localBuildOptions.enable
const projectArchive = ctx.local
? ({
type: ArchiveSourceType.PATH,
path: (await makeProjectTarballAsync()).path,
@@ -87,15 +86,16 @@ export async function prepareBuildRequestForPlatformAsync<
bucketKey: await uploadProjectAsync(ctx),
} as const);
const metadata = await collectMetadataAsync(ctx);
const metadataContext = builder.getMetadataContext();
const metadata = await collectMetadataAsync(ctx, metadataContext);
const job = await builder.prepareJobAsync(ctx, {
projectArchive,
credentials: credentialsResult?.credentials,
});
return async () => {
if (ctx.localBuildOptions.enable) {
await runLocalBuildAsync(job, ctx.localBuildOptions);
if (ctx.local) {
await runLocalBuildAsync(job);
return undefined;
} else {
try {
@@ -166,9 +166,8 @@ async function uploadProjectAsync<TPlatform extends Platform>(
return bucketKey;
},
{
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
successEvent: BuildEvent.PROJECT_UPLOAD_SUCCESS,
failureEvent: BuildEvent.PROJECT_UPLOAD_FAIL,
successEvent: Event.PROJECT_UPLOAD_SUCCESS,
failureEvent: Event.PROJECT_UPLOAD_FAIL,
trackingCtx: ctx.trackingCtx,
}
);
@@ -201,14 +200,34 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
return build;
},
{
attemptEvent: BuildEvent.BUILD_REQUEST_ATTEMPT,
successEvent: BuildEvent.BUILD_REQUEST_SUCCESS,
failureEvent: BuildEvent.BUILD_REQUEST_FAIL,
successEvent: Event.BUILD_REQUEST_SUCCESS,
failureEvent: Event.BUILD_REQUEST_FAIL,
trackingCtx: ctx.trackingCtx,
}
);
}
async function withAnalyticsAsync<Result>(
fn: () => Promise<Result>,
analytics: {
successEvent: Event;
failureEvent: Event;
trackingCtx: TrackingContext;
}
): Promise<Result> {
try {
const result = await fn();
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
return result;
} catch (error: any) {
Analytics.logEvent(analytics.failureEvent, {
...analytics.trackingCtx,
reason: error.message,
});
throw error;
}
}
export async function waitForBuildEndAsync(
buildIds: string[],
{ timeoutSec = 3600, intervalSec = 30 } = {}
+2 -1
View File
@@ -58,6 +58,7 @@ export async function configureAsync(options: {
platform: RequestedPlatform;
projectDir: string;
}): Promise<void> {
await getVcsClient().ensureRepoExistsAsync();
await maybeBailOnRepoStatusAsync();
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
@@ -143,7 +144,7 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
if (await fs.pathExists(easJsonPath)) {
const reader = new EasJsonReader(ctx.projectDir);
await reader.readAsync();
await reader.readAndValidateAsync();
Log.withTick('Validated eas.json');
return;
+120 -25
View File
@@ -1,16 +1,20 @@
import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
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 { CredentialsContext } from '../credentials/context';
import { Target } from '../credentials/ios/types';
import { RequestedPlatform } from '../platform';
import { GradleBuildContext } from '../project/android/gradle';
import { XcodeBuildContext } from '../project/ios/scheme';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { findAccountByName } from '../user/Account';
import { Actor } from '../user/User';
import { LocalBuildOptions } from './local';
import { ensureLoggedInAsync } from '../user/actions';
import { TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
export interface ConfigureContext {
user: Actor;
@@ -23,21 +27,6 @@ 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>;
@@ -45,7 +34,7 @@ export interface BuildContext<T extends Platform> {
clearCache: boolean;
credentialsCtx: CredentialsContext;
exp: ExpoConfig;
localBuildOptions: LocalBuildOptions;
local: boolean;
nonInteractive: boolean;
platform: T;
projectDir: string;
@@ -55,6 +44,112 @@ export interface BuildContext<T extends Platform> {
trackingCtx: TrackingContext;
user: Actor;
workflow: Workflow;
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
ios: T extends Platform.IOS ? IosBuildContext : undefined;
}
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;
}
}
-139
View File
@@ -1,139 +0,0 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } 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';
import { LocalBuildOptions } from './local';
export async function createBuildContextAsync<T extends Platform>({
buildProfileName,
buildProfile,
clearCache = false,
localBuildOptions,
nonInteractive = false,
platform,
projectDir,
skipProjectConfiguration = false,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
clearCache: boolean;
localBuildOptions: LocalBuildOptions;
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,
env: buildProfile.env,
});
const devClientProperties = getDevClientEventProperties({
platform,
projectDir,
buildProfile,
});
const trackingCtx = {
tracking_id: uuidv4(),
platform,
...(accountId && { account_id: accountId }),
project_id: projectId,
project_type: workflow,
...devClientProperties,
};
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
const commonContext: CommonContext<T> = {
accountName,
buildProfile,
buildProfileName,
clearCache,
credentialsCtx,
exp,
localBuildOptions,
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: BuildProfile;
}): 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 '../../../utils/plist';
import { readPlistAsync } from '../plist';
import {
BumpStrategy,
bumpVersionAsync,
+31 -31
View File
@@ -8,17 +8,24 @@ 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, CommonContext, IosBuildContext } from '../context';
import { BuildContext } 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';
export async function createIosContextAsync(
ctx: CommonContext<Platform.IOS>
): Promise<IosBuildContext> {
interface BuildConfiguration {
targetName?: string;
buildConfiguration?: string;
buildSettings: XCBuildConfiguration['buildSettings'];
}
export async function prepareIosBuildAsync(
ctx: BuildContext<Platform.IOS>
): Promise<BuildRequestSender> {
const { buildProfile } = ctx;
if (ctx.workflow === Workflow.MANAGED) {
@@ -43,42 +50,29 @@ export async function createIosContextAsync(
},
xcodeBuildContext
);
const applicationTarget = findApplicationTarget(targets);
const applicationTargetBuildSettings = resolveBuildSettings(ctx, applicationTarget);
const buildConfiguration = resolveBuildConfiguration(ctx, targets);
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, ctx.ios.targets);
return ensureIosCredentialsAsync(ctx, targets);
},
ensureProjectConfiguredAsync: async () => {
await validateAndSyncProjectConfigurationAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile: ctx.buildProfile,
buildSettings: ctx.ios.applicationTargetBuildSettings,
buildProfile,
buildSettings: buildConfiguration.buildSettings,
});
},
getMetadataContext: (): IosMetadataContext => {
return buildConfiguration;
},
prepareJobAsync: async (
ctx: BuildContext<Platform.IOS>,
jobData: JobData<IosCredentials>
): Promise<Job> => {
return await prepareJobAsync(ctx, {
...jobData,
buildScheme: ctx.ios.xcodeBuildContext.buildScheme,
});
return await prepareJobAsync(ctx, { ...jobData, buildScheme: xcodeBuildContext.buildScheme });
},
sendBuildRequestAsync: async (
appId: string,
@@ -96,17 +90,23 @@ export async function prepareIosBuildAsync(
});
}
function resolveBuildSettings(
ctx: CommonContext<Platform.IOS>,
applicationTarget: Target
): XCBuildConfiguration['buildSettings'] {
function resolveBuildConfiguration(
ctx: BuildContext<Platform.IOS>,
targets: Target[]
): BuildConfiguration {
if (ctx.workflow === Workflow.MANAGED) {
return {};
return { buildSettings: {} };
}
const applicationTarget = findApplicationTarget(targets);
const project = IOSConfig.XcodeUtils.getPbxproj(ctx.projectDir);
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
targetName: applicationTarget.targetName,
buildConfiguration: applicationTarget.buildConfiguration,
});
return xcBuildConfiguration?.buildSettings ?? {};
const buildSettings = xcBuildConfiguration?.buildSettings ?? {};
return {
buildSettings,
targetName: applicationTarget.targetName,
buildConfiguration: applicationTarget.buildConfiguration,
};
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
import { IosBuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import Log from '../../log';
@@ -36,7 +36,7 @@ export async function validateAndSyncProjectConfigurationAsync({
}: {
projectDir: string;
exp: ExpoConfig;
buildProfile: BuildProfile<Platform.IOS>;
buildProfile: IosBuildProfile;
buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
@@ -20,11 +20,7 @@ export async function ensureIosCredentialsAsync(
app: getAppFromContext(buildCtx.credentialsCtx),
targets,
iosCapabilitiesOptions: {
entitlements: await resolveEntitlementsJsonAsync(
buildCtx.projectDir,
buildCtx.workflow,
buildCtx.buildProfile.env ?? {}
),
entitlements: await resolveEntitlementsJsonAsync(buildCtx.projectDir, buildCtx.workflow),
},
distribution: buildCtx.buildProfile.distribution ?? 'store',
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
@@ -3,17 +3,12 @@ import plist from '@expo/plist';
import fs from 'fs-extra';
import path from 'path';
export async function readPlistAsync(plistPath: string): Promise<object | null> {
export async function readPlistAsync(plistPath: string): Promise<object> {
if (await fs.pathExists(plistPath)) {
const expoPlistContent = await fs.readFile(plistPath, 'utf8');
try {
return plist.parse(expoPlistContent);
} catch (err: any) {
err.message = `Failed to parse ${plistPath}. ${err.message}`;
throw err;
}
return plist.parse(expoPlistContent);
} else {
return null;
return {};
}
}
+4 -13
View File
@@ -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,11 +128,7 @@ export async function maybeResolveVersionsAsync(
appBuildVersion: await readBuildNumberAsync(projectDir, exp, buildSettings),
appVersion: await readShortVersionAsync(projectDir, exp, buildSettings),
};
} catch (err: any) {
Log.warn('Failed to read app versions.');
Log.debug(err);
Log.warn(err.message);
Log.warn('Proceeding anyway...');
} catch {
return {};
}
}
@@ -178,7 +174,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({
@@ -201,12 +197,7 @@ function ensureStaticConfigExists(projectDir: string): void {
}
}
export function evaluateTemplateString(
s: string,
buildSettings: XCBuildConfiguration['buildSettings']
): string {
// necessary because XCBuildConfiguration['buildSettings'] is not a plain object
const vars = { ...buildSettings };
export function evaluateTemplateString(s: string, vars: Record<string, any>): string {
return s.replace(/\$\((\w+)\)/g, (match, key) => {
if (vars.hasOwnProperty(key)) {
const value = String(vars[key]);
+32 -65
View File
@@ -1,78 +1,45 @@
import { Job } from '@expo/eas-build-job';
import spawnAsync from '@expo/spawn-async';
import { ChildProcess } from 'child_process';
import semver from 'semver';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { ora } from '../ora';
import Log from '../log';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.56';
export interface LocalBuildOptions {
enable: boolean;
skipCleanup?: boolean;
skipNativeBuild?: boolean;
artifactsDir?: string;
workingdir?: string;
verbose?: boolean;
}
export async function runLocalBuildAsync(job: Job, options: LocalBuildOptions): Promise<void> {
const { command, args } = await getCommandAndArgsAsync(job);
let spinner;
if (!options.verbose) {
spinner = ora().start(options.skipNativeBuild ? 'Preparing project' : 'Building project');
}
let childProcess: ChildProcess | undefined;
const interruptHandler = (): void => {
if (childProcess) {
childProcess.kill();
}
};
process.on('SIGINT', interruptHandler);
export async function runLocalBuildAsync(job: Job): Promise<void> {
const execNameOrPath =
process.env.EAS_LOCAL_BUILD_PLUGIN_PATH ??
(await findWithNodeResolutionAsync()) ??
PLUGIN_PACKAGE_NAME;
try {
const spawnPromise = spawnAsync(command, args, {
stdio: options.verbose ? 'inherit' : 'pipe',
env: {
...process.env,
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
...(options.skipCleanup || options.skipNativeBuild
? { EAS_LOCAL_BUILD_SKIP_CLEANUP: '1' }
: {}),
...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}),
...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}),
},
const arg = Buffer.from(JSON.stringify({ job })).toString('base64');
await spawnAsync(execNameOrPath, [arg], {
stdio: 'inherit',
});
childProcess = spawnPromise.child;
await spawnPromise;
} finally {
process.removeListener('SIGINT', interruptHandler);
spinner?.stop();
}
}
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');
} 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;
}
return {
command: 'npx',
args,
};
}
}
async function isAtLeastNpm7Async(): Promise<boolean> {
const version = (await spawnAsync('npm', ['--version'])).stdout.trim();
return semver.gte(version, '7.0.0');
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;
}
+35 -38
View File
@@ -1,10 +1,10 @@
import { Updates } from '@expo/config-plugins';
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
import { IosEnterpriseProvisioning } from '@expo/eas-json';
import fs from 'fs-extra';
import resolveFrom from 'resolve-from';
import type { XCBuildConfiguration } from 'xcode';
import Log from '../log';
import { getApplicationIdAsync } from '../project/android/applicationId';
import { GradleBuildContext } from '../project/android/gradle';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
import { getUsername } from '../project/projectUtils';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
@@ -22,20 +22,22 @@ import {
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
import { isExpoUpdatesInstalled } from './utils/updates';
// 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 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;
}
export async function collectMetadataAsync<T extends Platform>(
ctx: BuildContext<T>
ctx: BuildContext<T>,
platformContext: MetadataContext<T>
): Promise<Metadata> {
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
const distribution =
@@ -44,17 +46,16 @@ export async function collectMetadataAsync<T extends Platform>(
: ctx.buildProfile.distribution) ?? 'store';
const metadata = {
trackingContext: ctx.trackingCtx,
...(await maybeResolveVersionsAsync(ctx)),
...(await maybeResolveVersionsAsync(ctx, platformContext)),
cliVersion: easCliVersion,
workflow: ctx.workflow,
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
reactNativeVersion: await getReactNativeVersionAsync(ctx),
runtimeVersion: ctx.exp.runtimeVersion,
...channelOrReleaseChannel,
distribution,
appName: ctx.exp.name,
appIdentifier: resolveAppIdentifier(ctx),
appIdentifier: await resolveAppIdentifierAsync(ctx, platformContext),
buildProfile: ctx.buildProfileName,
gitCommitHash: await getVcsClient().getCommitHashAsync(),
isGitWorkingTreeDirty: await getVcsClient().hasUncommittedChangesAsync(),
@@ -69,14 +70,15 @@ export async function collectMetadataAsync<T extends Platform>(
}
async function maybeResolveVersionsAsync<T extends Platform>(
ctx: BuildContext<T>
ctx: BuildContext<T>,
platformContext: MetadataContext<T>
): Promise<{ appBuildVersion?: string; appVersion?: string }> {
if (ctx.platform === Platform.IOS) {
const iosContext = ctx as BuildContext<Platform.IOS>;
const iosContext = platformContext as IosMetadataContext;
return await maybeResolveIosVersionsAsync(
ctx.projectDir,
ctx.exp,
iosContext.ios.applicationTargetBuildSettings
iosContext?.buildSettings ?? {}
);
} else if (ctx.platform === Platform.ANDROID) {
const androidCtx = ctx as BuildContext<Platform.ANDROID>;
@@ -86,11 +88,19 @@ async function maybeResolveVersionsAsync<T extends Platform>(
}
}
function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string {
async function resolveAppIdentifierAsync<T extends Platform>(
ctx: BuildContext<T>,
platformContext: MetadataContext<T>
): Promise<string> {
if (ctx.platform === Platform.IOS) {
return (ctx as BuildContext<Platform.IOS>).ios.bundleIdentifier;
const iosContext = platformContext as IosMetadataContext;
return await getBundleIdentifierAsync(ctx.projectDir, ctx.exp, {
targetName: iosContext.targetName,
buildConfiguration: iosContext.buildConfiguration,
});
} else {
return (ctx as BuildContext<Platform.ANDROID>).android.applicationId;
const androidContext = platformContext as AndroidMetadataContext;
return await getApplicationIdAsync(ctx.projectDir, ctx.exp, androidContext.gradleContext);
}
}
@@ -144,19 +154,6 @@ async function getNativeChannelAsync<T extends Platform>(
return undefined;
}
async function getReactNativeVersionAsync(
ctx: BuildContext<Platform>
): Promise<string | undefined> {
try {
const reactNativePackageJsonPath = resolveFrom(ctx.projectDir, 'react-native/package.json');
return (await fs.readJson(reactNativePackageJsonPath)).version;
} catch (err) {
Log.debug('Failed to resolve react-native version:');
Log.debug(err);
return undefined;
}
}
function resolveIosEnterpriseProvisioning(
ctx: BuildContext<Platform.IOS>
): IosEnterpriseProvisioning | undefined {
@@ -1,257 +0,0 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, SubmitProfile } from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { AppPlatform, BuildFragment, BuildStatus, SubmissionFragment } from '../graphql/generated';
import { toAppPlatform, toPlatform } from '../graphql/types/AppPlatform';
import Log from '../log';
import {
RequestedPlatform,
appPlatformDisplayNames,
appPlatformEmojis,
toPlatforms,
} from '../platform';
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
import { createSubmissionContextAsync } from '../submit/context';
import {
submitAsync,
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { ProfileData, getProfilesAsync } from '../utils/profiles';
import { getVcsClient } from '../vcs';
import { prepareAndroidBuildAsync } from './android/build';
import { BuildRequestSender, waitForBuildEndAsync } from './build';
import { ensureProjectConfiguredAsync } from './configure';
import { BuildContext } from './context';
import { createBuildContextAsync } from './createContext';
import { prepareIosBuildAsync } from './ios/build';
import { LocalBuildOptions } from './local';
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
import { ensureRepoIsCleanAsync } from './utils/repository';
let metroConfigValidated = false;
let sdkVersionChecked = false;
export interface BuildFlags {
requestedPlatform: RequestedPlatform;
skipProjectConfiguration: boolean;
profile?: string;
nonInteractive: boolean;
wait: boolean;
clearCache: boolean;
json: boolean;
autoSubmit: boolean;
submitProfile?: string;
localBuildOptions: LocalBuildOptions;
}
export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFlags): Promise<void> {
await getVcsClient().ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
await ensureProjectConfiguredAsync(projectDir, flags.requestedPlatform);
const platforms = toPlatforms(flags.requestedPlatform);
const buildProfiles = await getProfilesAsync({
type: 'build',
projectDir,
platforms,
profileName: flags.profile ?? undefined,
});
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir,
nonInteractive: flags.nonInteractive,
buildProfiles,
});
const startedBuilds: {
build: BuildFragment;
buildProfile: ProfileData<'build'>;
}[] = [];
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
for (const buildProfile of buildProfiles) {
const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
projectDir,
flags,
moreBuilds: platforms.length > 1,
buildProfile,
});
if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile });
}
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
}
if (flags.localBuildOptions.enable) {
return;
}
Log.newLine();
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
Log.newLine();
const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) {
const submitProfiles = await getProfilesAsync({
projectDir,
platforms,
profileName: flags.submitProfile,
type: 'submit',
});
for (const startedBuild of startedBuilds) {
const submitProfile = nullthrows(
submitProfiles.find(
({ platform }) => toAppPlatform(platform) === startedBuild.build.platform
)
).profile;
const submission = await prepareAndStartSubmissionAsync({
build: startedBuild.build,
buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]),
moreBuilds: startedBuilds.length > 1,
projectDir,
buildProfile: startedBuild.buildProfile.profile,
submitProfile,
nonInteractive: flags.nonInteractive,
});
submissions.push(submission);
}
Log.newLine();
printSubmissionDetailsUrls(submissions);
Log.newLine();
}
if (!flags.wait) {
return;
}
const builds = await waitForBuildEndAsync(startedBuilds.map(({ build }) => build.id));
printBuildResults(builds, flags.json);
const haveAllBuildsFailedOrCanceled = builds.every(
build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status)
);
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
exitWithNonZeroCodeIfSomeBuildsFailed(builds);
} else {
// the following function also exits with non zero code if any of the submissions failed
await waitForSubmissionsToCompleteAsync(submissions);
}
}
async function prepareAndStartBuildAsync({
projectDir,
flags,
moreBuilds,
buildProfile,
}: {
projectDir: string;
flags: BuildFlags;
moreBuilds: boolean;
buildProfile: ProfileData<'build'>;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName,
clearCache: flags.clearCache,
buildProfile: buildProfile.profile,
nonInteractive: flags.nonInteractive,
platform: buildProfile.platform,
projectDir,
skipProjectConfiguration: flags.skipProjectConfiguration,
localBuildOptions: flags.localBuildOptions,
});
if (moreBuilds) {
Log.newLine();
const appPlatform = toAppPlatform(buildProfile.platform);
Log.log(
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
`${appPlatformDisplayNames[appPlatform]} build`
)}`
);
}
if (buildCtx.workflow === Workflow.MANAGED) {
if (!sdkVersionChecked) {
await checkExpoSdkIsSupportedAsync(buildCtx);
sdkVersionChecked = true;
}
if (!metroConfigValidated) {
await validateMetroConfigForManagedWorkflowAsync(buildCtx);
metroConfigValidated = true;
}
}
const build = await startBuildAsync(buildCtx);
return {
build,
buildCtx,
};
}
async function startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
let sendBuildRequestAsync: BuildRequestSender;
if (ctx.platform === Platform.ANDROID) {
sendBuildRequestAsync = await prepareAndroidBuildAsync(ctx as BuildContext<Platform.ANDROID>);
} else {
sendBuildRequestAsync = await prepareIosBuildAsync(ctx as BuildContext<Platform.IOS>);
}
return await sendBuildRequestAsync();
}
async function prepareAndStartSubmissionAsync({
build,
buildCtx,
moreBuilds,
projectDir,
buildProfile,
submitProfile,
nonInteractive,
}: {
build: BuildFragment;
buildCtx: BuildContext<Platform>;
moreBuilds: boolean;
projectDir: string;
buildProfile: BuildProfile;
submitProfile: SubmitProfile;
nonInteractive: boolean;
}): Promise<SubmissionFragment> {
const platform = toPlatform(build.platform);
const submissionCtx = await createSubmissionContextAsync({
platform,
projectDir,
projectId: build.project.id,
profile: submitProfile,
archiveFlags: { id: build.id },
nonInteractive,
env: buildProfile.env,
credentialsCtx: buildCtx.credentialsCtx,
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
});
if (moreBuilds) {
Log.newLine();
Log.log(
`${appPlatformEmojis[build.platform]} ${chalk.bold(
`${appPlatformDisplayNames[build.platform]} submission`
)}`
);
}
return await submitAsync(submissionCtx);
}
function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
i => i.status === BuildStatus.Errored
);
if (failedBuilds.length > 0) {
process.exit(1);
}
}
+2
View File
@@ -12,3 +12,5 @@ export enum BuildDistributionType {
INTERNAL = 'internal',
SIMULATOR = 'simulator',
}
export type TrackingContext = Record<string, string | number | boolean>;
@@ -0,0 +1,29 @@
import { logEvent } from '../../analytics';
export enum Event {
BUILD_COMMAND = 'build cli build command',
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
BUILD_REQUEST_SUCCESS = 'build cli build request success',
BUILD_REQUEST_FAIL = 'build cli build request fail',
BUILD_STATUS_COMMAND = 'build cli build status',
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
}
export default {
logEvent(name: Event, properties: Record<string, any>) {
logEvent(name, properties);
},
};
@@ -1,4 +1,5 @@
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';
@@ -20,7 +21,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
}: {
projectDir: string;
nonInteractive?: boolean;
buildProfiles?: ProfileData<'build'>[];
buildProfiles?: ProfileData<BuildProfile>[];
}): Promise<void> {
if (await isExpoDevClientInstalledAsync(projectDir)) {
return;
@@ -136,7 +136,6 @@ export async function reviewAndCommitChangesAsync(
await getVcsClient().commitAsync({
commitMessage: initialCommitMessage,
commitAllFiles: false,
nonInteractive,
});
Log.withTick('Committed changes.');
return;
+1 -1
View File
@@ -22,7 +22,7 @@ export function getArtifactUrl(artifactId: string): string {
export function getInternalDistributionInstallUrl(build: BuildFragment): string {
if (build.platform === AppPlatform.Ios) {
return `itms-services://?action=download-manifest;url=${getExpoApiBaseUrl()}/v2/projects/${
return `itms-services://?action=download-manifest;url=${getExpoApiBaseUrl()}/--/api/v2/projects/${
build.project.id
}/builds/${build.id}/manifest.plist`;
}
+3 -3
View File
@@ -4,9 +4,9 @@ import path from 'path';
import Log, { learnMore } from '../log';
import { getVcsClient } from '../vcs';
import { CommonContext } from './context';
import { BuildContext } from './context';
export function checkNodeEnvVariable(ctx: CommonContext<Platform>): void {
export function checkNodeEnvVariable(ctx: BuildContext<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: CommonContext<Platform>): void {
}
export async function checkGoogleServicesFileAsync<T extends Platform>(
ctx: CommonContext<T>
ctx: BuildContext<T>
): Promise<void> {
if (ctx.workflow === Workflow.GENERIC || ctx.buildProfile?.env?.GOOGLE_SERVICES_FILE) {
return;
@@ -7,7 +7,8 @@ import {
flushAsync as flushAnalyticsAsync,
initAsync as initAnalyticsAsync,
logEvent,
} from '../analytics/rudderstackClient';
} from '../analytics';
import Log from '../log';
import { findProjectRootAsync } from '../project/projectUtils';
import { getUserAsync } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
@@ -26,6 +27,8 @@ export default abstract class EasCommand extends Command {
// eslint-disable-next-line async-protect/async-suffix
async run(): Promise<any> {
EasJsonReader.setLog(Log);
await initAnalyticsAsync();
await this.applyCliConfigAsync();
@@ -1,5 +1,5 @@
import { asMock } from '../../__tests__/utils';
import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClient';
import { flushAsync, initAsync, logEvent } from '../../analytics';
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
import { getUserAsync } from '../../user/User';
import { ensureLoggedInAsync } from '../../user/actions';
@@ -10,8 +10,8 @@ TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
jest.mock('../../user/User');
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
jest.mock('../../analytics/rudderstackClient', () => {
const { AnalyticsEvent } = jest.requireActual('../../analytics/rudderstackClient');
jest.mock('../../analytics', () => {
const { AnalyticsEvent } = jest.requireActual('../../analytics');
return {
AnalyticsEvent,
logEvent: jest.fn(),
+1 -1
View File
@@ -76,7 +76,7 @@ export default class BranchList extends EasCommand {
Log.log(JSON.stringify(branches, null, 2));
} else {
const table = new CliTable({
head: ['Branch', ...UPDATE_COLUMNS],
head: ['branch', ...UPDATE_COLUMNS],
wordWrap: true,
});
+435 -2
View File
@@ -1,10 +1,443 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { flags } from '@oclif/command';
import assert from 'assert';
import chalk from 'chalk';
import dateFormat from 'dateformat';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
GetUpdateGroupAsyncQuery,
Robot,
RootQueryUpdatesByGroupArgs,
Update,
UpdateInfoGroup,
User,
ViewBranchQuery,
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import Log from '../../log';
import { ora } from '../../ora';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import {
PublishPlatform,
buildBundlesAsync,
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync, selectAsync } from '../../prompts';
import { formatUpdate } from '../../update/utils';
import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
import { getVcsClient } from '../../vcs';
import { createUpdateBranchOnAppAsync } from './create';
import { listBranchesAsync } from './list';
import { viewUpdateBranchAsync } from './view';
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
type PlatformFlag = PublishPlatform | 'all';
async function getUpdateGroupAsync({
group,
}: RootQueryUpdatesByGroupArgs): Promise<GetUpdateGroupAsyncQuery['updatesByGroup']> {
const { updatesByGroup } = await withErrorHandlingAsync(
graphqlClient
.query<GetUpdateGroupAsyncQuery, RootQueryUpdatesByGroupArgs>(
gql`
query getUpdateGroupAsync($group: ID!) {
updatesByGroup(group: $group) {
id
group
runtimeVersion
manifestFragment
platform
message
}
}
`,
{
group,
},
{ additionalTypenames: ['Update'] }
)
.toPromise()
);
return updatesByGroup;
}
async function ensureBranchExistsAsync({
appId,
name: branchName,
}: {
appId: string;
name: string;
}): Promise<{
id: string;
updates: Exclude<
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
null | undefined
>['updates'];
}> {
const { app } = await viewUpdateBranchAsync({
appId,
name: branchName,
});
const updateBranch = app?.byId.updateBranchByName;
if (updateBranch) {
const { id, updates } = updateBranch;
return { id, updates };
}
const newUpdateBranch = await createUpdateBranchOnAppAsync({ appId, name: branchName });
return { id: newUpdateBranch.id, updates: [] };
}
export default class BranchPublish extends EasCommand {
static description = 'deprecated, use "eas update"';
static hidden = true;
static description = 'Publish an update group to a branch.';
static args = [
{
name: 'name',
description: 'Name of the branch to publish on',
},
];
static flags = {
message: flags.string({
description: 'short message describing the updates.',
required: false,
}),
republish: flags.boolean({
description: 'republish an update group',
exclusive: ['input-dir', 'skip-bundler'],
}),
group: flags.string({
description: 'update group to republish',
exclusive: ['input-dir', 'skip-bundler'],
}),
'input-dir': flags.string({
description: 'location of the bundle',
default: 'dist',
required: false,
}),
'skip-bundler': flags.boolean({
description: `skip running Expo CLI to bundle the app before publishing`,
default: false,
}),
platform: flags.enum({
char: 'p',
description: `only publish to a single platform`,
options: [...defaultPublishPlatforms, 'all'],
default: 'all',
required: false,
}),
json: flags.boolean({
description: `return a json with the new update group.`,
default: false,
}),
auto: flags.boolean({
description:
'use the current git branch and commit message for the EAS branch and update message',
default: false,
}),
};
async runAsync(): Promise<void> {
throw new Error(BranchPublish.description);
let {
args: { name: branchName },
flags: {
json: jsonFlag,
auto: autoFlag,
message,
republish,
group,
'input-dir': inputDir,
'skip-bundler': skipBundler,
},
} = this.parse(BranchPublish);
const platformFlag = this.parse(BranchPublish).flags.platform as PlatformFlag;
// If a group was specified, that means we are republishing it.
republish = group ? true : republish;
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
isPublicConfig: true,
});
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
const projectId = await getProjectIdAsync(exp);
if (!branchName && autoFlag) {
branchName =
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substr(2, 4)}`;
}
if (!branchName) {
const validationMessage = 'branch name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
const branches = await listBranchesAsync({ projectId });
branchName = await selectAsync<string>(
'which branch would you like to publish on?',
branches.map(branch => {
return {
title: `${branch.name} ${chalk.grey(
`- current update: ${formatUpdate(branch.updates[0])}`
)}`,
value: branch.name,
};
})
);
assert(branchName, 'branch name must be specified.');
}
const { id: branchId, updates } = await ensureBranchExistsAsync({
appId: projectId,
name: branchName,
});
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
let oldMessage: string, oldRuntimeVersion: string;
if (republish) {
// If we are republishing, we don't need to worry about building the bundle or uploading the assets.
// Instead we get the `updateInfoGroup` from the update we wish to republish.
let updatesToRepublish: Pick<
Update,
'group' | 'message' | 'runtimeVersion' | 'manifestFragment' | 'platform'
>[];
if (group) {
updatesToRepublish = await getUpdateGroupAsync({ group });
} else {
// Drop into interactive mode if the user has not specified an update group to republish.
if (jsonFlag) {
throw new Error('You must specify the update group to republish.');
}
const updateGroups = uniqBy(updates, u => u.group)
.filter(update => {
// Only show groups that have updates on the specified platform(s).
return platformFlag === 'all' || update.platform === platformFlag;
})
.map(update => ({
title: formatUpdateTitle(update),
value: update.group,
}));
if (updateGroups.length === 0) {
throw new Error(
`There are no updates on branch "${branchName}" published on the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
);
}
const selectedUpdateGroup = await selectAsync<string>(
'which update would you like to republish?',
updateGroups
);
updatesToRepublish = updates.filter(update => update.group === selectedUpdateGroup);
}
const updatesToRepublishFilteredByPlatform = updatesToRepublish.filter(
// Only republish to the specified platforms
update => platformFlag === 'all' || update.platform === platformFlag
);
if (updatesToRepublishFilteredByPlatform.length === 0) {
throw new Error(
`There are no updates on branch "${branchName}" published on the platform(s) "${platformFlag}" with group ID "${
group ? group : updatesToRepublish[0].group
}". Did you mean to publish a new update instead?`
);
}
let publicationPlatformMessage: string;
if (platformFlag === 'all') {
if (updatesToRepublishFilteredByPlatform.length !== defaultPublishPlatforms.length) {
Log.warn(`You are republishing an update that wasn't published for all platforms.`);
}
publicationPlatformMessage = `The republished update will appear on the same plaforms it was originally published on: ${updatesToRepublishFilteredByPlatform
.map(update => update.platform)
.join(',')}`;
} else {
publicationPlatformMessage = `The republished update will appear only on: ${platformFlag}`;
}
Log.withTick(publicationPlatformMessage);
for (const update of updatesToRepublishFilteredByPlatform) {
const { manifestFragment } = update;
const platform = update.platform as PublishPlatform;
unsortedUpdateInfoGroups[platform] = JSON.parse(manifestFragment);
}
// These are the same for each member of an update group
group = updatesToRepublishFilteredByPlatform[0].group;
oldMessage = updatesToRepublishFilteredByPlatform[0].message ?? '';
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
} else {
// build bundle and upload assets for a new publish
if (!skipBundler) {
await buildBundlesAsync({ projectDir, inputDir });
}
const assetSpinner = ora('Uploading assets...').start();
try {
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
const assets = await collectAssetsAsync({ inputDir: inputDir!, platforms });
await uploadAssetsAsync(assets);
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
assetSpinner.succeed('Uploaded assets!');
} catch (e) {
assetSpinner.fail('Failed to upload assets');
throw e;
}
}
if (!message && autoFlag) {
message = (await getVcsClient().getLastCommitMessageAsync())?.trim();
}
if (!message) {
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ publishMessage: message } = await promptAsync({
type: 'text',
name: 'publishMessage',
message: `Please enter a publication message.`,
initial: republish
? `Republish "${oldMessage!}" - group: ${group}`
: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
validate: value => (value ? true : validationMessage),
}));
}
const runtimeToPlatformMapping: Record<string, string[]> = {};
for (const runtime of new Set(Object.values(runtimeVersions))) {
runtimeToPlatformMapping[runtime] = Object.entries(runtimeVersions)
.filter(pair => pair[1] === runtime)
.map(pair => pair[0]);
}
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups = Object.entries(runtimeToPlatformMapping).map(([runtime, platforms]) => {
const localUpdateInfoGroup = Object.fromEntries(
platforms.map(platform => [
platform,
unsortedUpdateInfoGroups[platform as keyof UpdateInfoGroup],
])
);
if (republish && !oldRuntimeVersion) {
throw new Error(
'Can not find the runtime version of the update group that is being republished.'
);
}
return {
branchId,
updateInfoGroup: localUpdateInfoGroup,
runtimeVersion: republish ? oldRuntimeVersion : runtime,
message,
};
});
let newUpdates;
const publishSpinner = ora('Publishing...').start();
try {
newUpdates = await PublishMutation.publishUpdateGroupAsync(updateGroups);
publishSpinner.succeed('Published!');
} catch (e) {
publishSpinner.fail('Failed to published updates');
throw e;
}
if (jsonFlag) {
Log.log(JSON.stringify(newUpdates));
} else {
if (new Set(newUpdates.map(update => update.group)).size > 1) {
Log.addNewLineIfNone();
Log.log(
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
);
}
Log.addNewLineIfNone();
for (const runtime of new Set(Object.values(runtimeVersions))) {
const platforms = newUpdates
.filter(update => update.runtimeVersion === runtime)
.map(update => update.platform);
const newUpdate = newUpdates.find(update => update.runtimeVersion === runtime);
if (!newUpdate) {
throw new Error(`Publish response is missing updates with runtime ${runtime}.`);
}
Log.log(
formatFields([
{ label: 'branch', value: branchName },
{ label: 'runtime version', value: runtime },
{ label: 'platform', value: platforms.join(',') },
{ label: 'update group ID', value: newUpdate.group },
{ label: 'message', value: message },
])
);
Log.addNewLineIfNone();
}
}
}
}
async function getRuntimeVersionObjectAsync(
exp: ExpoConfig,
platformFlag: PlatformFlag,
projectDir: string
): Promise<Record<string, string>> {
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
for (const platform of platforms) {
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
if (isPolicy) {
const isManaged = (await resolveWorkflowAsync(projectDir, platform)) === Workflow.MANAGED;
if (!isManaged) {
throw new Error('Runtime version policies are only supported in the managed workflow.');
}
}
}
return Object.fromEntries(
platforms.map(platform => [platform, Updates.getRuntimeVersion(exp, platform)])
);
}
function formatUpdateTitle(
update: Exclude<
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
null | undefined
>['updates'][number]
): string {
const { message, createdAt, actor, runtimeVersion } = update;
let actorName: string;
switch (actor?.__typename) {
case 'User': {
actorName = (actor as Pick<User, 'username' | 'id'>).username;
break;
}
case 'Robot': {
const { firstName, id } = actor as Pick<Robot, 'firstName' | 'id'>;
actorName = firstName ?? `robot: ${id.slice(0, 4)}...`;
break;
}
default:
actorName = 'unknown';
}
return `[${dateFormat(
createdAt,
'mmm dd HH:MM'
)} by ${actorName}, runtimeVersion: ${runtimeVersion}] ${message}`;
}
@@ -7,7 +7,6 @@ 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.';
@@ -22,14 +21,11 @@ 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());
@@ -52,7 +48,7 @@ function logSuccess(platform: RequestedPlatform): void {
storesText = 'the Google Play Store';
} else if (platform === 'ios') {
platformsText = 'iOS project is';
storesText = 'the Apple App Store Connect';
storesText = 'the Apple App Store';
}
Log.log(`🎉 Your ${platformsText} ready to build.
+251 -13
View File
@@ -1,19 +1,53 @@
import { EasJsonReader } from '@expo/eas-json';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, EasJsonReader } from '@expo/eas-json';
import { flags } from '@oclif/command';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import figures from 'figures';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import { prepareAndroidBuildAsync } from '../../build/android/build';
import { BuildRequestSender, waitForBuildEndAsync } from '../../build/build';
import { ensureProjectConfiguredAsync } from '../../build/configure';
import { BuildContext, createBuildContextAsync } from '../../build/context';
import { prepareIosBuildAsync } from '../../build/ios/build';
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from '../../build/utils/devClient';
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
import { ensureRepoIsCleanAsync, reviewAndCommitChangesAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { CredentialsContext } from '../../credentials/context';
import {
AppPlatform,
BuildFragment,
BuildStatus,
SubmissionFragment,
} from '../../graphql/generated';
import { toAppPlatform, toPlatform } from '../../graphql/types/AppPlatform';
import Log, { learnMore, link } from '../../log';
import { RequestedPlatform, selectRequestedPlatformAsync } from '../../platform';
import {
RequestedPlatform,
appPlatformDisplayNames,
appPlatformEmojis,
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';
import { createSubmissionContextAsync } from '../../submit/context';
import {
submitAsync,
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../../submit/submit';
import { printSubmissionDetailsUrls } from '../../submit/utils/urls';
import { easCliVersion } from '../../utils/easCli';
import { enableJsonOutput } from '../../utils/json';
import { ProfileData, getProfilesAsync } from '../../utils/profiles';
import { getVcsClient, setVcsClient } from '../../vcs';
import GitClient from '../../vcs/clients/git';
@@ -31,8 +65,21 @@ interface RawBuildFlags {
'auto-submit-with-profile'?: string;
}
interface BuildFlags {
requestedPlatform: RequestedPlatform;
skipProjectConfiguration: boolean;
profile: string | null;
nonInteractive: boolean;
local: boolean;
wait: boolean;
clearCache: boolean;
json: boolean;
autoSubmit: boolean;
submitProfile?: string;
}
export default class Build extends EasCommand {
static description = 'Start a build';
static description = 'start a build';
static flags = {
platform: flags.enum({
@@ -86,17 +133,104 @@ export default class Build extends EasCommand {
}),
};
private metroConfigValidated = false;
async runAsync(): Promise<void> {
const { flags: rawFlags } = this.parse(Build);
if (rawFlags.json) {
enableJsonOutput();
}
const flags = await this.sanitizeFlagsAsync(rawFlags);
const { requestedPlatform } = flags;
const projectDir = await findProjectRootAsync();
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await runBuildAndSubmitAsync(projectDir, flags);
await getVcsClient().ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
const platforms = toPlatforms(requestedPlatform);
const easJsonReader = new EasJsonReader(projectDir);
const buildProfiles = await getProfilesAsync({
platforms,
profileName: flags.profile,
async readProfileAsync(platform, profileName) {
return await easJsonReader.readBuildProfileAsync(platform, profileName);
},
});
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir,
nonInteractive: flags.nonInteractive,
buildProfiles,
});
const startedBuilds: {
build: BuildFragment;
buildProfile: ProfileData<BuildProfile>;
}[] = [];
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
for (const buildProfile of buildProfiles) {
const { build: maybeBuild, buildCtx } = await this.prepareAndStartBuildAsync({
projectDir,
flags,
moreBuilds: platforms.length > 1,
buildProfile,
});
if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile });
}
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
}
if (flags.local) {
return;
}
Log.newLine();
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
Log.newLine();
const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) {
for (const startedBuild of startedBuilds) {
const submission = await this.prepareAndStartSubmissionAsync({
build: startedBuild.build,
credentialsCtx: nullthrows(
buildCtxByPlatform[startedBuild.build.platform]?.credentialsCtx
),
flags,
moreBuilds: startedBuilds.length > 1,
projectDir,
buildProfile: startedBuild.buildProfile,
});
submissions.push(submission);
}
Log.newLine();
printSubmissionDetailsUrls(submissions);
Log.newLine();
}
if (!flags.wait) {
return;
}
const builds = await waitForBuildEndAsync(startedBuilds.map(({ build }) => build.id));
printBuildResults(builds, flags.json);
const haveAllBuildsFailedOrCanceled = builds.every(
build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status)
);
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
this.exitWithNonZeroCodeIfSomeBuildsFailed(builds);
} else {
// the following function also exits with non zero code if any of the submissions failed
await waitForSubmissionsToCompleteAsync(submissions);
}
}
private async sanitizeFlagsAsync(flags: RawBuildFlags): Promise<BuildFlags> {
@@ -134,20 +268,124 @@ export default class Build extends EasCommand {
return {
requestedPlatform,
skipProjectConfiguration: flags['skip-project-configuration'],
profile,
profile: profile ?? null,
nonInteractive,
localBuildOptions: flags['local']
? { enable: true, verbose: true }
: {
enable: false,
},
local: flags['local'],
wait: flags['wait'],
clearCache: flags['clear-cache'],
json: flags['json'],
autoSubmit: flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined,
autoSubmit: flags['auto-submit'],
submitProfile: flags['auto-submit-with-profile'] ?? profile,
};
}
private async prepareAndStartBuildAsync({
projectDir,
flags,
moreBuilds,
buildProfile,
}: {
projectDir: string;
flags: BuildFlags;
moreBuilds: boolean;
buildProfile: ProfileData<BuildProfile>;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName,
clearCache: flags.clearCache,
buildProfile: buildProfile.profile,
local: flags.local,
nonInteractive: flags.nonInteractive,
platform: buildProfile.platform,
projectDir,
skipProjectConfiguration: flags.skipProjectConfiguration,
});
if (moreBuilds) {
Log.newLine();
const appPlatform = toAppPlatform(buildProfile.platform);
Log.log(
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
`${appPlatformDisplayNames[appPlatform]} build`
)}`
);
}
if (buildCtx.workflow === Workflow.MANAGED && !this.metroConfigValidated) {
await validateMetroConfigForManagedWorkflowAsync(buildCtx);
this.metroConfigValidated = true;
}
if (!buildCtx.local && !(await isEasEnabledForProjectAsync(buildCtx.projectId))) {
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
}
const build = await this.startBuildAsync(buildCtx);
return {
build,
buildCtx,
};
}
private async startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
let sendBuildRequestAsync: BuildRequestSender;
if (ctx.platform === Platform.ANDROID) {
sendBuildRequestAsync = await prepareAndroidBuildAsync(ctx as BuildContext<Platform.ANDROID>);
} else {
sendBuildRequestAsync = await prepareIosBuildAsync(ctx as BuildContext<Platform.IOS>);
}
return await sendBuildRequestAsync();
}
private async prepareAndStartSubmissionAsync({
build,
credentialsCtx,
flags,
moreBuilds,
projectDir,
buildProfile,
}: {
build: BuildFragment;
credentialsCtx: CredentialsContext;
flags: BuildFlags;
moreBuilds: boolean;
projectDir: string;
buildProfile: ProfileData<BuildProfile>;
}): Promise<SubmissionFragment> {
const easJsonReader = new EasJsonReader(projectDir);
const platform = toPlatform(build.platform);
const submitProfile = await easJsonReader.readSubmitProfileAsync(platform, flags.submitProfile);
const submissionCtx = await createSubmissionContextAsync({
platform,
projectDir,
projectId: build.project.id,
profile: submitProfile,
archiveFlags: { id: build.id },
nonInteractive: flags.nonInteractive,
env: buildProfile.profile.env,
credentialsCtx,
});
if (moreBuilds) {
Log.newLine();
Log.log(
`${appPlatformEmojis[build.platform]} ${chalk.bold(
`${appPlatformDisplayNames[build.platform]} submission`
)}`
);
}
return await submitAsync(submissionCtx);
}
private exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
i => i.status === BuildStatus.Errored
);
if (failedBuilds.length > 0) {
process.exit(1);
}
}
}
export async function handleDeprecatedEasJsonAsync(
@@ -159,7 +397,7 @@ export async function handleDeprecatedEasJsonAsync(
return;
}
const easJsonReader = new EasJsonReader(projectDir);
const rawEasJson = await easJsonReader.readAsync();
const rawEasJson = await easJsonReader.readRawAsync();
if (rawEasJson?.cli) {
return;
}
@@ -1,128 +0,0 @@
import { flags } from '@oclif/command';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import EasCommand from '../../commandUtils/EasCommand';
import Log from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import { findProjectRootAsync } from '../../project/projectUtils';
import { getTmpDirectory } from '../../utils/paths';
import { getVcsClient } from '../../vcs';
enum InspectStage {
ARCHIVE = 'archive',
PRE_BUILD = 'pre-build',
POST_BUILD = 'post-build',
}
const STAGE_DESCRIPTION = `Stage of the build you want to inspect.
archive - builds the project archive that would be uploaded to EAS when building
pre-build - prepares the project to be built with Gradle/Xcode. Does not run the native build.
post-build - builds the native project and leaves the output directory for inspection`;
export default class BuildInspect extends EasCommand {
static description =
'Inspect the state of the project at specific build stages. Useful for troubleshooting.';
static flags = {
platform: flags.enum({
char: 'p',
options: [RequestedPlatform.Android, RequestedPlatform.Ios],
required: true,
}),
profile: flags.string({
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
}),
stage: flags.enum({
char: 's',
description: STAGE_DESCRIPTION,
options: [InspectStage.ARCHIVE, InspectStage.PRE_BUILD, InspectStage.POST_BUILD],
required: true,
}),
output: flags.string({
description: 'Output directory.',
required: true,
helpValue: 'OUTPUT_DIRECTORY',
}),
force: flags.boolean({
description: 'Delete OUTPUT_DIRECTORY if it already exists.',
default: false,
}),
verbose: flags.boolean({
default: false,
}),
};
async runAsync(): Promise<void> {
const { flags } = this.parse(BuildInspect);
const outputDirectory = path.resolve(process.cwd(), flags.output);
await this.prepareOutputDirAsync(outputDirectory, flags.force);
if (flags.stage === InspectStage.ARCHIVE) {
const vcs = getVcsClient();
await vcs.ensureRepoExistsAsync();
await vcs.makeShallowCopyAsync(outputDirectory);
Log.withTick(`Project saved to ${outputDirectory}`);
} else {
const projectDir = await findProjectRootAsync();
const tmpWorkingdir = path.join(getTmpDirectory(), uuidv4());
try {
await runBuildAndSubmitAsync(projectDir, {
skipProjectConfiguration: false,
nonInteractive: false,
wait: true,
clearCache: false,
json: false,
autoSubmit: false,
requestedPlatform: flags.platform,
profile: flags.profile,
localBuildOptions: {
enable: true,
...(flags.stage === InspectStage.PRE_BUILD ? { skipNativeBuild: true } : {}),
...(flags.stage === InspectStage.POST_BUILD ? { skipCleanup: true } : {}),
verbose: flags.verbose,
workingdir: tmpWorkingdir,
artifactsDir: path.join(tmpWorkingdir, 'artifacts'),
},
});
if (!flags.verbose) {
Log.log(chalk.green('Build successful'));
}
} catch (err) {
if (!flags.verbose) {
Log.error('Build failed');
Log.error(`Re-run this command with ${chalk.bold('--verbose')} flag to see the logs`);
}
} finally {
const spinner = ora().start(`Copying project build directory to ${outputDirectory}`);
try {
const tmpBuildDirectory = path.join(tmpWorkingdir, 'build');
if (await fs.pathExists(tmpBuildDirectory)) {
await fs.copy(tmpBuildDirectory, outputDirectory);
}
await fs.remove(tmpWorkingdir);
spinner.succeed(`Project build directory saved to ${outputDirectory}`);
} catch (err) {
spinner.fail();
throw err;
}
}
}
}
private async prepareOutputDirAsync(outputDir: string, force: boolean): Promise<void> {
if (await fs.pathExists(outputDir)) {
if (force) {
await fs.remove(outputDir);
} else {
throw new Error(`Directory ${outputDir} already exists`);
}
}
await fs.mkdirp(outputDir);
}
}
@@ -20,7 +20,7 @@ import { promptAsync } from '../../prompts';
import formatFields from '../../utils/formatFields';
import { createUpdateBranchOnAppAsync } from '../branch/create';
export async function createUpdateChannelOnAppAsync({
async function createUpdateChannelOnAppAsync({
appId,
channelName,
branchId,
@@ -89,7 +89,7 @@ export async function updateChannelBranchMappingAsync({
);
const channel = data.updateChannel.editUpdateChannel;
if (!channel) {
throw new Error(`Could not find a channel with id: ${channelId}`);
throw new Error(`Could not fine channel with id ${channelId}`);
}
return data.updateChannel.editUpdateChannel!;
}
@@ -181,7 +181,7 @@ export function logChannelDetails(channel: {
: []),
formatUpdate(update),
update?.runtimeVersion ?? 'N/A',
update?.group ?? 'N/A',
update?.group ?? 'N/A,',
getPlatformsForGroup({
updates: branch.updates,
group: branch.updates[0]?.group,
@@ -239,7 +239,7 @@ export default class ChannelView extends EasCommand {
});
const channel = getUpdateChannelByNameForAppresult.app?.byId.updateChannelByName;
if (!channel) {
throw new Error(`Could not find a channel with name: ${channelName}`);
throw new Error(`Could not fine channel with name ${channelName}`);
}
if (jsonFlag) {
@@ -256,7 +256,9 @@ export default class ChannelView extends EasCommand {
])
);
Log.addNewLineIfNone();
Log.log(chalk`{bold Branches pointed at this channel and their most recent update group:}`);
Log.log(
chalk`{bold Branches, pointed at by this channel, and their most recent update group:}`
);
logChannelDetails(channel);
}
}
+1 -1
View File
@@ -55,7 +55,7 @@ export default class Config extends EasCommand {
},
]));
const profile = await reader.getBuildProfileAsync(platform, profileName);
const profile = await reader.readBuildProfileAsync(platform, profileName);
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
Log.log(getProjectConfigDescription(projectDir));
+7 -1
View File
@@ -1,10 +1,16 @@
import EasCommand from '../commandUtils/EasCommand';
import { CredentialsContext } from '../credentials/context';
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
import { ensureLoggedInAsync } from '../user/actions';
export default class Credentials extends EasCommand {
static description = 'manage your credentials';
async runAsync(): Promise<void> {
await new SelectPlatform().runAsync();
const ctx = new CredentialsContext({
projectDir: process.cwd(),
user: await ensureLoggedInAsync(),
});
await new SelectPlatform().runAsync(ctx);
}
}
+1 -21
View File
@@ -1,10 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import envinfo from 'envinfo';
import EasCommand from '../commandUtils/EasCommand';
import Log from '../log';
import { findProjectRootAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { easCliVersion } from '../utils/easCli';
export default class Diagnostics extends EasCommand {
@@ -35,23 +32,6 @@ export default class Diagnostics extends EasCommand {
title: `EAS CLI ${easCliVersion} environment info`,
}
);
Log.log(info.trimEnd());
await this.printWorkflowAsync();
Log.newLine();
}
private async printWorkflowAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const androidWorkflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const iosWorkflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (androidWorkflow === iosWorkflow) {
Log.log(` Project workflow: ${androidWorkflow}`);
} else {
Log.log(` Project Workflow:`);
Log.log(` Android: ${androidWorkflow}`);
Log.log(` iOS: ${iosWorkflow}`);
}
Log.log(info);
}
}
@@ -9,6 +9,10 @@ import {
EnvironmentSecretsQuery,
} from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log';
import {
isEasEnabledForProjectAsync,
warnEasUnavailable,
} from '../../project/isEasEnabledForProject';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
@@ -53,6 +57,12 @@ 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,6 +10,10 @@ import {
EnvironmentSecretsQuery,
} from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log';
import {
isEasEnabledForProjectAsync,
warnEasUnavailable,
} from '../../project/isEasEnabledForProject';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
@@ -33,6 +37,12 @@ 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,6 +6,10 @@ 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,
@@ -21,6 +25,12 @@ 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");
}
+13 -3
View File
@@ -1,4 +1,5 @@
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';
@@ -14,6 +15,7 @@ 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';
@@ -42,7 +44,7 @@ interface Flags {
}
export default class Submit extends EasCommand {
static description = `Submit build archive to App Store Connect
static description = `submit build archive to app store
See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit/eas-json/')}`;
static aliases = ['build:submit'];
@@ -94,12 +96,20 @@ 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[] = [];
@@ -1,114 +0,0 @@
import { ExpoConfig, getConfig, modifyConfigAsync } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import Log, { learnMore } from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
const EAS_UPDATE_URL = 'https://u.expo.dev';
const DEFAULT_MANAGED_RUNTIME_VERSION = { policy: 'sdkVersion' } as const;
const DEFAULT_BARE_RUNTIME_VERSION = '1.0.0';
export async function getEASUpdateURLAsync(exp: ExpoConfig): Promise<string> {
const projectId = await getProjectIdAsync(exp);
return new URL(projectId, EAS_UPDATE_URL).href;
}
async function configureProjectForEASUpdateAsync(
projectDir: string,
exp: ExpoConfig,
isBare: boolean
): Promise<void> {
const easUpdateURL = await getEASUpdateURLAsync(exp);
const preexistingRuntimeVersion = exp.runtimeVersion;
const defaultRuntimeVersion = isBare
? DEFAULT_BARE_RUNTIME_VERSION
: DEFAULT_MANAGED_RUNTIME_VERSION;
const result = await modifyConfigAsync(projectDir, {
runtimeVersion: preexistingRuntimeVersion ?? defaultRuntimeVersion,
updates: { ...exp.updates, url: easUpdateURL },
});
switch (result.type) {
case 'success':
if (exp.updates?.url) {
if (exp.updates.url !== easUpdateURL) {
Log.withTick(
`Overwrote "${exp.updates?.url}" with "${easUpdateURL}" for the updates.url value in app.json`
);
}
} else {
Log.withTick(`Set updates.url value, to "${easUpdateURL}" in app.json`);
}
if (!preexistingRuntimeVersion) {
Log.withTick(
`Set runtimeVersion to "${JSON.stringify(defaultRuntimeVersion)}" in app.json`
);
}
break;
case 'warn': {
Log.addNewLineIfNone();
Log.warn(
`It looks like you are using a dynamic configuration! ${learnMore(
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs)'
)}`
);
Log.warn(
`In order to finish configuring your project for EAS Update, you are going to need manually add the following to your app.config.js:\n${learnMore(
'https://expo.fyi/eas-update-config.md'
)}\n`
);
Log.log(
chalk.bold(
`{\n updates": {\n "url": "${easUpdateURL}"\n },\n "runtimeVersion": {\n "policy": "sdkVersion"\n }\n}`
)
);
Log.addNewLineIfNone();
throw new Error(result.message);
}
case 'fail':
throw new Error(result.message);
default:
throw new Error('Unexpected result type from modifyConfigAsync');
}
}
export default class UpdateConfigure extends EasCommand {
static hidden = true;
static description = 'Configure the project to support EAS Update.';
async runAsync(): Promise<void> {
Log.log(
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
});
const hasAndroidNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
const hasIosNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.IOS)) === Workflow.GENERIC;
const isBare = hasAndroidNativeProject || hasIosNativeProject;
await configureProjectForEASUpdateAsync(projectDir, exp, isBare);
Log.addNewLineIfNone();
if (isBare) {
Log.log(
`🧐 It seems you are on the bare workflow! Please also update your native files. You can do this by either running ${chalk.bold(
'eas build:configure'
)} or manually editing Expo.plist/AndroidManifest.xml. ${learnMore(
'https://expo.fyi/eas-update-config.md#native-configuration'
)}`
);
} else {
Log.log(`🎉 Your app is configured to run EAS Update!`);
}
}
}
@@ -1,507 +0,0 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { flags } from '@oclif/command';
import assert from 'assert';
import chalk from 'chalk';
import dateFormat from 'dateformat';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
GetUpdateGroupAsyncQuery,
Robot,
RootQueryUpdatesByGroupArgs,
Update,
UpdateInfoGroup,
User,
ViewBranchQuery,
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import Log from '../../log';
import { ora } from '../../ora';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import {
PublishPlatform,
buildBundlesAsync,
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync, selectAsync } from '../../prompts';
import { formatUpdate } from '../../update/utils';
import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
import { getVcsClient } from '../../vcs';
import { createUpdateBranchOnAppAsync } from '../branch/create';
import { listBranchesAsync } from '../branch/list';
import { viewUpdateBranchAsync } from '../branch/view';
import { createUpdateChannelOnAppAsync } from '../channel/create';
import { getEASUpdateURLAsync } from './configure';
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
type PlatformFlag = PublishPlatform | 'all';
async function getUpdateGroupAsync({
group,
}: RootQueryUpdatesByGroupArgs): Promise<GetUpdateGroupAsyncQuery['updatesByGroup']> {
const { updatesByGroup } = await withErrorHandlingAsync(
graphqlClient
.query<GetUpdateGroupAsyncQuery, RootQueryUpdatesByGroupArgs>(
gql`
query getUpdateGroupAsync($group: ID!) {
updatesByGroup(group: $group) {
id
group
runtimeVersion
manifestFragment
platform
message
}
}
`,
{
group,
},
{ additionalTypenames: ['Update'] }
)
.toPromise()
);
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,
}: {
appId: string;
name: string;
}): Promise<{
id: string;
updates: Exclude<
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
null | undefined
>['updates'];
}> {
const { app } = await viewUpdateBranchAsync({
appId,
name: branchName,
});
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: [] };
}
export default class UpdatePublish extends EasCommand {
static hidden = true;
static description = 'Publish an update group.';
static flags = {
branch: flags.string({
description: 'Branch to publish the update group on',
required: false,
}),
message: flags.string({
description: 'A short message describing the update',
required: false,
}),
republish: flags.boolean({
description: 'Republish an update group',
exclusive: ['input-dir', 'skip-bundler'],
}),
group: flags.string({
description: 'Update group to republish',
exclusive: ['input-dir', 'skip-bundler'],
}),
'input-dir': flags.string({
description: 'Location of the bundle',
default: 'dist',
required: false,
}),
'skip-bundler': flags.boolean({
description: `Skip running Expo CLI to bundle the app before publishing`,
default: false,
}),
platform: flags.enum({
char: 'p',
options: [...defaultPublishPlatforms, 'all'],
default: 'all',
required: false,
}),
json: flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
default: false,
}),
auto: flags.boolean({
description:
'Use the current git branch and commit message for the EAS branch and update message',
default: false,
}),
};
async runAsync(): Promise<void> {
let {
flags: {
branch: branchName,
json: jsonFlag,
auto: autoFlag,
message,
republish,
group,
'input-dir': inputDir,
'skip-bundler': skipBundler,
},
} = this.parse(UpdatePublish);
const platformFlag = this.parse(UpdatePublish).flags.platform as PlatformFlag;
// If a group was specified, that means we are republishing it.
republish = group ? true : republish;
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
isPublicConfig: true,
});
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
const projectId = await getProjectIdAsync(exp);
await checkEASUpdateURLIsSetAsync(exp);
if (!branchName && autoFlag) {
branchName =
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substr(2, 4)}`;
}
if (!branchName) {
const validationMessage = 'Branch name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
const branches = await listBranchesAsync({ projectId });
if (branches.length === 0) {
({ name: branchName } = await promptAsync({
type: 'text',
name: 'name',
message: 'No branches found. Creating a new one. Please name the new branch:',
initial:
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substr(2, 4)}`,
validate: value => (value ? true : validationMessage),
}));
} else {
branchName = await selectAsync<string>(
'Which branch would you like to publish on?',
branches.map(branch => {
return {
title: `${branch.name} ${chalk.grey(
`- current update: ${formatUpdate(branch.updates[0])}`
)}`,
value: branch.name,
};
})
);
}
assert(branchName, 'Branch name must be specified.');
}
const { id: branchId, updates } = await ensureBranchExistsAsync({
appId: projectId,
name: branchName,
});
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
let oldMessage: string, oldRuntimeVersion: string;
if (republish) {
// If we are republishing, we don't need to worry about building the bundle or uploading the assets.
// Instead we get the `updateInfoGroup` from the update we wish to republish.
let updatesToRepublish: Pick<
Update,
'group' | 'message' | 'runtimeVersion' | 'manifestFragment' | 'platform'
>[];
if (group) {
updatesToRepublish = await getUpdateGroupAsync({ group });
} else {
// Drop into interactive mode if the user has not specified an update group to republish.
if (jsonFlag) {
throw new Error('You must specify the update group to republish.');
}
const updateGroups = uniqBy(updates, u => u.group)
.filter(update => {
// Only show groups that have updates on the specified platform(s).
return platformFlag === 'all' || update.platform === platformFlag;
})
.map(update => ({
title: formatUpdateTitle(update),
value: update.group,
}));
if (updateGroups.length === 0) {
throw new Error(
`There are no updates on branch "${branchName}" published on the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
);
}
const selectedUpdateGroup = await selectAsync<string>(
'which update would you like to republish?',
updateGroups
);
updatesToRepublish = updates.filter(update => update.group === selectedUpdateGroup);
}
const updatesToRepublishFilteredByPlatform = updatesToRepublish.filter(
// Only republish to the specified platforms
update => platformFlag === 'all' || update.platform === platformFlag
);
if (updatesToRepublishFilteredByPlatform.length === 0) {
throw new Error(
`There are no updates on branch "${branchName}" published on the platform(s) "${platformFlag}" with group ID "${
group ? group : updatesToRepublish[0].group
}". Did you mean to publish a new update instead?`
);
}
let publicationPlatformMessage: string;
if (platformFlag === 'all') {
if (updatesToRepublishFilteredByPlatform.length !== defaultPublishPlatforms.length) {
Log.warn(`You are republishing an update that wasn't published for all platforms.`);
}
publicationPlatformMessage = `The republished update will appear on the same plaforms it was originally published on: ${updatesToRepublishFilteredByPlatform
.map(update => update.platform)
.join(', ')}`;
} else {
publicationPlatformMessage = `The republished update will appear only on: ${platformFlag}`;
}
Log.withTick(publicationPlatformMessage);
for (const update of updatesToRepublishFilteredByPlatform) {
const { manifestFragment } = update;
const platform = update.platform as PublishPlatform;
unsortedUpdateInfoGroups[platform] = JSON.parse(manifestFragment);
}
// These are the same for each member of an update group
group = updatesToRepublishFilteredByPlatform[0].group;
oldMessage = updatesToRepublishFilteredByPlatform[0].message ?? '';
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
if (!message) {
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ publishMessage: message } = await promptAsync({
type: 'text',
name: 'publishMessage',
message: `Please enter an update message.`,
initial: `Republish "${oldMessage!}" - group: ${group}`,
validate: (value: any) => (value ? true : validationMessage),
}));
}
} else {
if (!message && autoFlag) {
message = (await getVcsClient().getLastCommitMessageAsync())?.trim();
}
if (!message) {
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ publishMessage: message } = await promptAsync({
type: 'text',
name: 'publishMessage',
message: `Please enter an update message.`,
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
validate: (value: any) => (value ? true : validationMessage),
}));
}
// build bundle and upload assets for a new publish
if (!skipBundler) {
await buildBundlesAsync({ projectDir, inputDir });
}
const assetSpinner = ora('Uploading assets...').start();
try {
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
const assets = await collectAssetsAsync({ inputDir: inputDir!, platforms });
await uploadAssetsAsync(assets);
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
assetSpinner.succeed('Uploaded assets!');
} catch (e) {
assetSpinner.fail('Failed to upload assets');
throw e;
}
}
const runtimeToPlatformMapping: Record<string, string[]> = {};
for (const runtime of new Set(Object.values(runtimeVersions))) {
runtimeToPlatformMapping[runtime] = Object.entries(runtimeVersions)
.filter(pair => pair[1] === runtime)
.map(pair => pair[0]);
}
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups = Object.entries(runtimeToPlatformMapping).map(([runtime, platforms]) => {
const localUpdateInfoGroup = Object.fromEntries(
platforms.map(platform => [
platform,
unsortedUpdateInfoGroups[platform as keyof UpdateInfoGroup],
])
);
if (republish && !oldRuntimeVersion) {
throw new Error(
'Can not find the runtime version of the update group that is being republished.'
);
}
return {
branchId,
updateInfoGroup: localUpdateInfoGroup,
runtimeVersion: republish ? oldRuntimeVersion : runtime,
message,
};
});
let newUpdates;
const publishSpinner = ora('Publishing...').start();
try {
newUpdates = await PublishMutation.publishUpdateGroupAsync(updateGroups);
publishSpinner.succeed('Published!');
} catch (e) {
publishSpinner.fail('Failed to published updates');
throw e;
}
if (jsonFlag) {
Log.log(JSON.stringify(newUpdates));
} else {
if (new Set(newUpdates.map(update => update.group)).size > 1) {
Log.addNewLineIfNone();
Log.log(
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
);
}
Log.addNewLineIfNone();
for (const runtime of new Set(Object.values(runtimeVersions))) {
const platforms = newUpdates
.filter(update => update.runtimeVersion === runtime)
.map(update => update.platform);
const newUpdate = newUpdates.find(update => update.runtimeVersion === runtime);
if (!newUpdate) {
throw new Error(`Publish response is missing updates with runtime ${runtime}.`);
}
Log.log(
formatFields([
{ label: 'branch', value: branchName },
{ label: 'runtime version', value: runtime },
{ label: 'platform', value: platforms.join(', ') },
{ label: 'update group ID', value: newUpdate.group },
{ label: 'message', value: message! },
])
);
Log.addNewLineIfNone();
}
}
}
}
async function getRuntimeVersionObjectAsync(
exp: ExpoConfig,
platformFlag: PlatformFlag,
projectDir: string
): Promise<Record<string, string>> {
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
for (const platform of platforms) {
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
if (isPolicy) {
const isManaged = (await resolveWorkflowAsync(projectDir, platform)) === Workflow.MANAGED;
if (!isManaged) {
throw new Error('Runtime version policies are only supported in the managed workflow.');
}
}
}
return Object.fromEntries(
platforms.map(platform => [platform, Updates.getRuntimeVersion(exp, platform)])
);
}
function formatUpdateTitle(
update: Exclude<
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
null | undefined
>['updates'][number]
): string {
const { message, createdAt, actor, runtimeVersion } = update;
let actorName: string;
switch (actor?.__typename) {
case 'User': {
actorName = (actor as Pick<User, 'username' | 'id'>).username;
break;
}
case 'Robot': {
const { firstName, id } = actor as Pick<Robot, 'firstName' | 'id'>;
actorName = firstName ?? `robot: ${id.slice(0, 4)}...`;
break;
}
default:
actorName = 'unknown';
}
return `[${dateFormat(
createdAt,
'mmm dd HH:MM'
)} by ${actorName}, runtimeVersion: ${runtimeVersion}] ${message}`;
}
async function checkEASUpdateURLIsSetAsync(exp: ExpoConfig): Promise<void> {
const configuredURL = exp.updates?.url;
const expectedURL = await getEASUpdateURLAsync(exp);
if (configuredURL !== expectedURL) {
throw new Error(
`The update URL is incorrectly configured for EAS Update. Please set updates.url to ${expectedURL} in your app.json.`
);
}
}
@@ -14,7 +14,8 @@ export default class WebhookCreate extends EasCommand {
static flags = {
event: flags.enum({
description: 'Event type that triggers the webhook',
options: [WebhookType.Build, WebhookType.Submit],
options: [WebhookType.Build],
default: WebhookType.Build,
}),
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, WebhookType.Submit],
options: [WebhookType.Build],
}),
};
@@ -18,7 +18,8 @@ export default class WebhookUpdate extends EasCommand {
}),
event: flags.enum({
description: 'Event type that triggers the webhook',
options: [WebhookType.Build, WebhookType.Submit],
options: [WebhookType.Build],
default: WebhookType.Build,
}),
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,35 +1,31 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import crypto from 'crypto';
import fs from 'fs-extra';
// (dsokal) We actually want to use node-fetch but the change is in progress.
// See https://github.com/expo/eas-cli/issues/32 for context.
// eslint-disable-next-line no-restricted-imports
import fetch from 'node-fetch';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { Analytics, BuildEvent } from '../../../analytics/events';
import Analytics, { Event } from '../../../build/utils/analytics';
import { AndroidKeystoreType } from '../../../graphql/generated';
import { KeystoreGenerationUrlMutation } from '../../../graphql/mutations/KeystoreGenerationUrlMutation';
import Log from '../../../log';
import { ora } from '../../../ora';
import { getTmpDirectory } from '../../../utils/paths';
import { KeystoreWithType } from '../credentials';
interface KeystoreParams {
keystorePassword: string;
keyAlias: string;
keyPassword: string;
}
export async function keytoolCommandExistsAsync(): Promise<boolean> {
try {
await spawnAsync('keytool');
return true;
} catch (error) {
return false;
}
return true;
}
async function ensureKeytoolCommandExistsAsync(): Promise<void> {
if (!(await keytoolCommandExistsAsync())) {
Log.error('keytool is required to run this command, make sure you have it installed?');
Log.warn('keytool is a part of OpenJDK: https://openjdk.java.net/');
Log.warn('Also make sure that keytool is in your PATH after installation.');
throw new Error('keytool not found');
}
}
enum KeystoreCreateStep {
@@ -39,7 +35,7 @@ enum KeystoreCreateStep {
}
export async function generateRandomKeystoreAsync(projectId: string): Promise<KeystoreWithType> {
const keystoreData: KeystoreParams = {
const keystoreData = {
keystorePassword: crypto.randomBytes(16).toString('hex'),
keyPassword: crypto.randomBytes(16).toString('hex'),
keyAlias: crypto.randomBytes(16).toString('hex'),
@@ -48,38 +44,23 @@ export async function generateRandomKeystoreAsync(projectId: string): Promise<Ke
}
async function createKeystoreAsync(
keystoreParams: KeystoreParams,
credentials: {
keystorePassword: string;
keyAlias: string;
keyPassword: string;
},
projectId: string
): Promise<KeystoreWithType> {
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Attempt,
type: AndroidKeystoreType.Jks,
});
try {
let keystore: KeystoreWithType | undefined;
let localAttempt = false;
if (await keytoolCommandExistsAsync()) {
try {
localAttempt = true;
keystore = await createKeystoreLocallyAsync(keystoreParams);
} catch {
Log.error('Failed to generate keystore locally. Falling back to cloud generation.');
}
}
if (!keystore) {
keystore = await createKeystoreInCloudAsync(keystoreParams, {
showKeytoolDetectionMsg: !localAttempt,
});
}
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Success,
type: AndroidKeystoreType.Jks,
});
return keystore;
await ensureKeytoolCommandExistsAsync();
} catch (error: any) {
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Fail,
reason: error.message,
@@ -87,11 +68,7 @@ async function createKeystoreAsync(
});
throw error;
}
}
async function createKeystoreLocallyAsync(
keystoreParams: KeystoreParams
): Promise<KeystoreWithType> {
await fs.mkdirp(getTmpDirectory());
const keystorePath = path.join(getTmpDirectory(), `${uuidv4()}-keystore.jks`);
try {
@@ -101,13 +78,13 @@ async function createKeystoreLocallyAsync(
'-storetype',
'JKS',
'-storepass',
keystoreParams.keystorePassword,
credentials.keystorePassword,
'-keypass',
keystoreParams.keyPassword,
credentials.keyPassword,
'-keystore',
keystorePath,
'-alias',
keystoreParams.keyAlias,
credentials.keyAlias,
'-keyalg',
'RSA',
'-keysize',
@@ -117,49 +94,27 @@ async function createKeystoreLocallyAsync(
'-dname',
`CN=,OU=,O=,L=,S=,C=US`,
]);
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Success,
type: AndroidKeystoreType.Jks,
});
return {
...keystoreParams,
...credentials,
keystore: await fs.readFile(keystorePath, 'base64'),
type: AndroidKeystoreType.Jks,
};
} catch (error: any) {
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Fail,
reason: error.message,
type: AndroidKeystoreType.Jks,
});
throw error;
} finally {
await fs.remove(keystorePath);
}
}
interface KeystoreServiceResult {
keystoreBase64: string;
keystorePassword: string;
keyPassword: string;
keyAlias: string;
}
async function createKeystoreInCloudAsync(
keystoreParams: KeystoreParams,
{ showKeytoolDetectionMsg = true } = {}
): Promise<KeystoreWithType> {
if (showKeytoolDetectionMsg) {
Log.log(`Detected that you do not have ${chalk.bold('keytool')} installed locally.`);
}
const spinner = ora('Generating keystore in the cloud...').start();
try {
const url = await KeystoreGenerationUrlMutation.createKeystoreGenerationUrlAsync();
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(keystoreParams),
headers: { 'Content-Type': 'application/json' },
});
const result: KeystoreServiceResult = await response.json();
spinner.succeed();
return {
type: AndroidKeystoreType.Jks,
keystore: result.keystoreBase64,
keystorePassword: result.keystorePassword,
keyAlias: result.keyAlias,
keyPassword: result.keyPassword,
};
} catch (err: any) {
spinner.fail();
throw err;
}
}
+5 -17
View File
@@ -1,5 +1,4 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { Env } from '@expo/eas-build-job';
import chalk from 'chalk';
import Log from '../log';
@@ -28,7 +27,6 @@ export class CredentialsContext {
nonInteractive?: boolean;
projectDir: string;
user: Actor;
env?: Env;
}
) {
this.projectDir = options.projectDir;
@@ -37,21 +35,11 @@ export class CredentialsContext {
this.resolvedExp = options.exp;
if (!this.resolvedExp) {
this.resolvedExp =
CredentialsContext.getExpoConfigInProject(this.projectDir, { env: options.env }) ??
undefined;
}
}
static getExpoConfigInProject(
projectDir: string,
{ env }: { env?: Env } = {}
): ExpoConfig | null {
try {
return getExpoConfig(projectDir, { env });
} catch (error) {
// ignore error, context might be created outside of expo project
return null;
try {
this.resolvedExp = getExpoConfig(options.projectDir);
} catch (error) {
// ignore error, context might be created outside of expo project
}
}
}
@@ -64,7 +64,7 @@ export async function provideOrGenerateAscApiKeyAsync(
purpose: AppStoreApiKeyPurpose
): Promise<MinimalAscApiKey> {
if (ctx.nonInteractive) {
throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
return await generateAscApiKeyAsync(ctx, purpose);
}
const userProvided = await promptForAscApiKeyAsync(ctx);
@@ -73,7 +73,7 @@ export async function provideOrGenerateAscApiKeyAsync(
}
if (!ctx.appStore.authCtx) {
Log.warn('Unable to validate App Store Connect API Key, you are not authenticated with Apple.');
Log.warn('Unable to validate App Store Connect Api Key, you are not authenticated with Apple.');
return userProvided;
}
@@ -82,7 +82,7 @@ export async function provideOrGenerateAscApiKeyAsync(
return userProvided;
}
const useUserProvided = await confirmAsync({
message: `App Store Connect API Key with ID ${userProvided.keyId} is not valid on Apple's servers. Proceed anyway?`,
message: `App Store Connect Api Key with ID ${userProvided.keyId} is not valid on Apple's servers. Proceed anyway?`,
});
if (useUserProvided) {
return userProvided;
@@ -94,7 +94,6 @@ async function generateAscApiKeyAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<MinimalAscApiKey> {
//console.log('debug');
await ctx.appStore.ensureAuthenticatedAsync();
const ascApiKey = await ctx.appStore.createAscApiKeyAsync({
nickname: getAscApiKeyName(purpose),
@@ -119,9 +118,7 @@ async function promptForAscApiKeyAsync(ctx: CredentialsContext): Promise<Minimal
async function promptForKeyP8AndIdAsync(): Promise<Pick<AscApiKeyPath, 'keyP8Path' | 'keyId'>> {
Log.log(
chalk.bold(
'An App Store Connect Api key is required to upload your app to the Apple App Store Connect'
)
chalk.bold('An App Store Connect Api key is required to upload your app to the Apple App Store')
);
Log.log(
`If you're not sure what this is or how to create one, ${learnMore(
@@ -132,7 +129,7 @@ async function promptForKeyP8AndIdAsync(): Promise<Pick<AscApiKeyPath, 'keyP8Pat
const { keyP8Path } = await promptAsync({
type: 'text',
name: 'keyP8Path',
message: 'Path to App Store Connect API Key:',
message: 'Path to App Store Connect Api Key:',
initial: 'AuthKey_ABCD.p8',
// eslint-disable-next-line async-protect/async-suffix
validate: async (filePath: string) => {
@@ -192,12 +189,12 @@ export async function selectAscApiKeysFromAccountAsync(
if (filterDifferentAppleTeam) {
const maybeAppleTeamId = ctx.appStore.authCtx?.team.id;
Log.warn(
`There are no App Store Connect API Keys in your EAS account${
`There are no App Store Connect Api Keys in your EAS account${
maybeAppleTeamId ? ` matching Apple Team ID: ${maybeAppleTeamId}.` : '.'
}`
);
} else {
Log.warn(`There are no App Store Connect API Keys available in your EAS account.`);
Log.warn(`There are no App Store Connect Api Keys available in your EAS account.`);
}
return null;
}
@@ -211,7 +208,7 @@ async function selectAscApiKeysAsync(
const { chosenAscApiKey } = await promptAsync({
type: 'select',
name: 'chosenAscApiKey',
message: 'Select an API Key from the list:',
message: 'Select an Api Key from the list:',
choices: sortedAscApiKeys.map(ascApiKey => ({
title: formatAscApiKey(ascApiKey),
value: ascApiKey,
@@ -31,7 +31,7 @@ export class AssignAscApiKey {
throw new Error(`${purpose} is not yet supported.`);
}
Log.succeed(
`App Store Connect API Key assigned to ${this.app.projectName}: ${this.app.bundleIdentifier} for ${purpose}.`
`App Store Connect Api Key assigned to ${this.app.projectName}: ${this.app.bundleIdentifier} for ${purpose}.`
);
return updatedAppCredentials;
}
@@ -12,12 +12,12 @@ export class CreateAscApiKey {
purpose: AppStoreApiKeyPurpose
): Promise<AppStoreConnectApiKeyFragment> {
if (ctx.nonInteractive) {
throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
throw new Error(`A new App Store Connect Api Key cannot be created in non-interactive mode.`);
}
const ascApiKey = await provideOrGenerateAscApiKeyAsync(ctx, purpose);
const result = await ctx.ios.createAscApiKeyAsync(this.account, ascApiKey);
Log.succeed('Created App Store Connect API Key');
Log.succeed('Created App Store Connect Api Key');
return result;
}
}
@@ -12,7 +12,7 @@ export class SelectAndRemoveAscApiKey {
const selected = await selectAscApiKeysFromAccountAsync(ctx, this.account);
if (selected) {
await new RemoveAscApiKey(this.account, selected).runAsync(ctx);
Log.succeed('Removed App Store Connect API Key');
Log.succeed('Removed App Store Connect Api Key');
Log.newLine();
}
}
@@ -23,28 +23,28 @@ export class RemoveAscApiKey {
public async runAsync(ctx: CredentialsContext): Promise<void> {
if (ctx.nonInteractive) {
throw new Error(`Cannot remove App Store Connect API Keys in non-interactive mode.`);
throw new Error(`Cannot remove App Store Connect Api Keys in non-interactive mode.`);
}
// TODO(quin): add an extra edge on AppStoreConnectApiKey to find the apps using it
const confirm = await confirmAsync({
message: `Deleting this API Key may affect your projects that rely on it. Do you want to continue?`,
message: `Deleting this Api Key may affect your projects that rely on it. Do you want to continue?`,
});
if (!confirm) {
Log.log('Aborting');
return;
}
Log.log('Removing API Key');
Log.log('Removing Api Key');
await ctx.ios.deleteAscApiKeyAsync(this.ascApiKey.id);
let shouldRevoke = false;
if (!ctx.nonInteractive) {
shouldRevoke = await confirmAsync({
message: `Do you also want to revoke this API Key on the Apple Developer Portal?`,
message: `Do you also want to revoke this Api Key on the Apple Developer Portal?`,
});
} else if (ctx.nonInteractive) {
Log.log('Skipping API Key revocation on the Apple Developer Portal.');
Log.log('Skipping Api Key revocation on the Apple Developer Portal.');
}
if (shouldRevoke) {
await ctx.appStore.revokeAscApiKeyAsync(this.ascApiKey.keyIdentifier);
@@ -31,10 +31,10 @@ export enum SetupAscApiKeyChoice {
export class SetUpAscApiKey {
public choices: { title: string; value: string }[] = [
{
title: '[Choose an existing ASC API Key]',
title: '[Choose an existing key]',
value: SetupAscApiKeyChoice.USE_EXISTING,
},
{ title: '[Add a new ASC API Key]', value: SetupAscApiKeyChoice.GENERATE },
{ title: '[Add a new key]', value: SetupAscApiKeyChoice.GENERATE },
];
constructor(private app: AppLookupParams, private purpose: AppStoreApiKeyPurpose) {}
@@ -42,15 +42,15 @@ export class SetUpAscApiKey {
public async runAsync(ctx: CredentialsContext): Promise<CommonIosAppCredentialsFragment> {
const isKeySetup = await this.isAscApiKeySetupAsync(ctx, this.purpose);
if (isKeySetup) {
Log.succeed('App Store Connect API Key already set up.');
Log.succeed('App Store Connect Api Key already set up.');
return nullthrows(
await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(this.app),
'iosAppCredentials cannot be null if App Store Connect API Key is already set up'
'iosAppCredentials cannot be null if App Store Connect Api Key is already set up'
);
}
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'App Store Connect API Keys cannot be set up in --non-interactive mode.'
'App Store Connect Api Keys cannot be set up in --non-interactive mode.'
);
}
@@ -88,11 +88,11 @@ export class SetUpAscApiKey {
}
const [autoselectedKey] = sortAscApiKeysByUpdatedAtDesc(validKeys);
const useAutoselected = await confirmAsync({
message: `Reuse this App Store Connect API Key?\n${formatAscApiKey(autoselectedKey)}`,
message: `Reuse this App Store Connect Api Key?\n${formatAscApiKey(autoselectedKey)}`,
});
if (useAutoselected) {
Log.log(`Using App Store Connect API Key with ID ${autoselectedKey.keyIdentifier}`);
Log.log(`Using App Store Connect Api Key with ID ${autoselectedKey.keyIdentifier}`);
return autoselectedKey;
} else {
return null;
@@ -105,7 +105,7 @@ export class SetUpAscApiKey {
): Promise<boolean> {
const appCredentials = await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(this.app);
if (purpose !== AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
throw new Error(`App Store Connect API Key setup is not yet supported for ${purpose}.`);
throw new Error(`App Store Connect Api Key setup is not yet supported for ${purpose}.`);
}
return !!appCredentials?.appStoreConnectApiKeyForSubmissions;
}
@@ -123,7 +123,7 @@ export class SetUpAscApiKey {
const result = await promptAsync({
type: 'select',
name: 'choice',
message: 'Select what to use for your project:',
message: 'Select the App Store Connect Api Key to use for your project:',
choices,
});
choice = result.choice;
@@ -1,70 +0,0 @@
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import wrapAnsi from 'wrap-ansi';
import { CommonIosAppCredentialsFragment } from '../../../graphql/generated';
import Log, { learnMore } from '../../../log';
import { promptAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { UnsupportedCredentialsChoiceError } from '../../errors';
import { AppLookupParams } from '../api/GraphqlClient';
import { AppStoreApiKeyPurpose } from './AscApiKeyUtils';
import { SetUpAscApiKey } from './SetUpAscApiKey';
export const PROMPT_FOR_APP_SPECIFIC_PASSWORD = 'PROMPT_FOR_APP_SPECIFIC_PASSWORD';
export class SetUpSubmissionCredentials {
private setupAscApiKeyAction;
constructor(app: AppLookupParams) {
this.setupAscApiKeyAction = new SetUpAscApiKey(app, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
// Add this unrelated choice to ASC API Key setup for legacy purposes -- we will deprecate it soon
this.setupAscApiKeyAction.choices = this.setupAscApiKeyAction.choices.concat({
title: '[Enter an App Specific Password]',
value: PROMPT_FOR_APP_SPECIFIC_PASSWORD,
});
}
public async runAsync(
ctx: CredentialsContext
): Promise<CommonIosAppCredentialsFragment | string> {
try {
const iosAppCredentials = await this.setupAscApiKeyAction.runAsync(ctx);
const { keyIdentifier, name } = nullthrows(
iosAppCredentials.appStoreConnectApiKeyForSubmissions,
'ASC API Key must be defined for EAS Submit'
);
Log.log(`Using API Key ID: ${keyIdentifier}${name ? ` (${name})` : ''}`);
return iosAppCredentials;
} catch (e) {
if (e instanceof UnsupportedCredentialsChoiceError) {
if (e.choice === PROMPT_FOR_APP_SPECIFIC_PASSWORD) {
return await this.promptForAppSpecificPasswordAsync();
}
}
throw e;
}
}
async promptForAppSpecificPasswordAsync(): Promise<string> {
Log.addNewLineIfNone();
Log.log(`Please enter your Apple app-specific password.`);
Log.log(learnMore('https://expo.fyi/apple-app-specific-password'));
Log.warn(
wrapAnsi(
`This option will be deprecated soon. You will still be able to provide your password with the ${chalk.bold(
'EXPO_APPLE_APP_SPECIFIC_PASSWORD'
)} environment variable.`,
process.stdout.columns || 80
)
);
const { appSpecificPassword } = await promptAsync({
name: 'appSpecificPassword',
message: 'Your Apple app-specific password:',
type: 'password',
validate: (val: string) => val !== '' || 'Apple app-specific password cannot be empty!',
});
return appSpecificPassword;
}
}
@@ -6,7 +6,7 @@ import { AssignAscApiKey } from '../AssignAscApiKey';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
describe(AssignAscApiKey, () => {
it('assigns an App Store Connect API Key in Interactive Mode for EAS Submit', async () => {
it('assigns an App Store Connect Api Key in Interactive Mode for EAS Submit', async () => {
const ctx = createCtxMock({
nonInteractive: false,
});
@@ -12,7 +12,7 @@ jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(CreateAscApiKey, () => {
it('creates a App Store API Key in Interactive Mode', async () => {
it('creates a App Store Api Key in Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -10,7 +10,7 @@ jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(RemoveAscApiKey, () => {
it('removes an Asc API Key', async () => {
it('removes an Asc Api Key', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const removeAscApiKeyAction = new RemoveAscApiKey(
@@ -18,7 +18,7 @@ jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(SetUpAscApiKey, () => {
it('skips setting up a ASC API Key if it is already configured', async () => {
it('skips setting up a ASC Api Key if it is already configured', async () => {
const ctx = createCtxMock({
nonInteractive: false,
ios: {
@@ -35,12 +35,12 @@ describe(SetUpAscApiKey, () => {
);
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers
// expect not to create a new ASC Api Key on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration not to be updated with a new ASC API Key
// expect configuration not to be updated with a new ASC Api Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
});
it('sets up a ASC API Key, creating a new one if there are no existing ASC API Keys', async () => {
it('sets up a ASC Api Key, creating a new one if there are no existing ASC Api Keys', async () => {
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -60,12 +60,12 @@ describe(SetUpAscApiKey, () => {
);
await setupAscApiKeyAction.runAsync(ctx);
// expect to create a new ASC API Key on expo servers
// expect to create a new ASC Api Key on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
// expect configuration to be updated with a new ASC API Key
// expect configuration to be updated with a new ASC Api Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a ASC API Key, use autoselected ASC API Key when there are existing keys', async () => {
it('sets up a ASC Api Key, use autoselected ASC Api Key when there are existing keys', async () => {
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -86,12 +86,12 @@ describe(SetUpAscApiKey, () => {
);
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers, we are using the existing one
// expect not to create a new ASC Api Key on expo servers, we are using the existing one
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration to be updated with a new ASC API Key
// expect configuration to be updated with a new ASC Api Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a ASC API Key, allowing user to choose existing keys', async () => {
it('sets up a ASC Api Key, allowing user to choose existing keys', async () => {
const ctx = createCtxMock({
nonInteractive: false,
ios: {
@@ -110,9 +110,9 @@ describe(SetUpAscApiKey, () => {
);
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers, we are using the existing one
// expect not to create a new ASC Api Key on expo servers, we are using the existing one
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration to be updated with a new ASC API Key
// expect configuration to be updated with a new ASC Api Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('works in Non Interactive Mode if ASC Key is configured', async () => {
@@ -1,61 +0,0 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync, promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import {
getNewIosApiMock,
testCommonIosAppCredentialsFragment,
testTargets,
} from '../../../__tests__/fixtures-ios';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetUpAscApiKey } from '../SetUpAscApiKey';
import {
PROMPT_FOR_APP_SPECIFIC_PASSWORD,
SetUpSubmissionCredentials,
} from '../SetUpSubmissionCredentials';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(SetUpSubmissionCredentials, () => {
it('allows user to enter an App Specific Password', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({
choice: PROMPT_FOR_APP_SPECIFIC_PASSWORD,
}))
.mockImplementationOnce(() => ({
appSpecificPassword: 'super secret',
}));
const ctx = createCtxMock({
nonInteractive: false,
ios: {
...getNewIosApiMock(),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpSubmissionCredentials(appLookupParams);
const asp = await setupAscApiKeyAction.runAsync(ctx);
// prompt to choose ASP, then prompt to input ASP
expect(asMock(promptAsync).mock.calls.length).toBe(2);
expect(asp).toBe('super secret');
});
it('returns an ASC API key', async () => {
try {
jest
.spyOn(SetUpAscApiKey.prototype, 'runAsync')
.mockImplementation(async _ctx => testCommonIosAppCredentialsFragment);
const ctx = createCtxMock();
const appLookupParams = getAppLookupParamsFromContext(
ctx,
findApplicationTarget(testTargets)
);
const setupAscApiKeyAction = new SetUpSubmissionCredentials(appLookupParams);
const asc = await setupAscApiKeyAction.runAsync(ctx);
expect(asc).toEqual(testCommonIosAppCredentialsFragment);
} finally {
jest.restoreAllMocks();
}
});
});
@@ -150,30 +150,6 @@ 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,7 +11,6 @@ import {
} from '@expo/apple-utils';
import { JSONObject, JSONValue } from '@expo/json-file';
import getenv from 'getenv';
import { inspect } from 'util';
import Log from '../../../log';
@@ -101,20 +100,7 @@ export async function syncCapabilitiesForEntitlementsAsync(
);
if (modifiedRequest.length) {
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}`
);
}
}
await bundleId.updateBundleIdCapabilityAsync(modifiedRequest);
}
return { enabled: enabledCapabilityNames, disabled: disabledCapabilityNames };
@@ -162,13 +162,11 @@ export async function ensureAppExistsAsync(
}
): Promise<App> {
const context = getRequestContext(authCtx);
const spinner = ora(`Linking to App Store Connect ${chalk.dim(bundleIdentifier)}`).start();
const spinner = ora(`Linking to App Store ${chalk.dim(bundleIdentifier)}`).start();
let app = await App.findAsync(context, { bundleId: bundleIdentifier });
if (!app) {
spinner.text = `Creating App Store Connect app ${chalk.bold(name)} ${chalk.dim(
bundleIdentifier
)}`;
spinner.text = `Creating App Store app ${chalk.bold(name)} ${chalk.dim(bundleIdentifier)}`;
try {
// Assert contract errors when the user needs to create an app.
await assertContractMessagesAsync(context, spinner);
@@ -196,8 +194,6 @@ export async function ensureAppExistsAsync(
} else {
// TODO: Update app name when API gives us that possibility
}
spinner.succeed(
`Prepared App Store Connect for ${chalk.bold(name)} ${chalk.dim(bundleIdentifier)}`
);
spinner.succeed(`Prepared App Store for ${chalk.bold(name)} ${chalk.dim(bundleIdentifier)}`);
return app;
}
@@ -1,53 +1,41 @@
import { IOSConfig, compileModsAsync } from '@expo/config-plugins';
import { Workflow } from '@expo/eas-build-job';
import { JSONObject } from '@expo/json-file';
import { getPrebuildConfigAsync } from '@expo/prebuild-config';
import plist from '@expo/plist';
import { getPrebuildConfig } from '@expo/prebuild-config';
import fs from 'fs-extra';
import { readPlistAsync } from '../../../utils/plist';
export async function getManagedEntitlementsJsonAsync(projectDir: string): Promise<JSONObject> {
let { exp } = getPrebuildConfig(projectDir, { platforms: ['ios'] });
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;
}
exp = await compileModsAsync(exp, {
projectRoot: projectDir,
platforms: ['ios'],
introspect: true,
});
return exp.ios?.entitlements || {};
}
export async function resolveEntitlementsJsonAsync(
projectDir: string,
workflow: Workflow,
env: Record<string, string>
workflow: Workflow
): Promise<JSONObject> {
if (workflow === Workflow.GENERIC) {
return (await getEntitlementsJsonAsync(projectDir)) || {};
} else if (workflow === Workflow.MANAGED) {
return await getManagedEntitlementsJsonAsync(projectDir, env);
return await getManagedEntitlementsJsonAsync(projectDir);
} else {
throw new Error(`Unknown workflow: ${workflow}`);
}
}
async function getEntitlementsJsonAsync(projectDir: string): Promise<JSONObject | 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;
}
try {
const entitlementsPath = IOSConfig.Paths.getEntitlementsPath(projectDir);
if (entitlementsPath) {
const entitlementsContents = await fs.readFile(entitlementsPath, 'utf8');
return plist.parse(entitlementsContents);
}
} catch {}
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',
@@ -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,
},
{
@@ -1,5 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { AndroidBuildProfile } from '@expo/eas-json';
import assert from 'assert';
import Log, { learnMore } from '../../log';
@@ -10,7 +10,7 @@ import {
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { findAccountByName } from '../../user/Account';
import { ensureActorHasUsername, ensureLoggedInAsync } from '../../user/actions';
import { ensureActorHasUsername } from '../../user/actions';
import { AssignFcm } from '../android/actions/AssignFcm';
import { AssignGoogleServiceAccountKey } from '../android/actions/AssignGoogleServiceAccountKey';
import {
@@ -51,18 +51,12 @@ import {
import { SelectBuildProfileFromEasJson } from './SelectBuildProfileFromEasJson';
export class ManageAndroid {
constructor(private callingAction: Action, private projectDir: string) {}
constructor(private callingAction: Action) {}
async runAsync(currentActions: ActionInfo[] = highLevelActions): Promise<void> {
const hasProjectContext = !!CredentialsContext.getExpoConfigInProject(this.projectDir);
const buildProfile = hasProjectContext
? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.ANDROID).runAsync()
: null;
const ctx = new CredentialsContext({
projectDir: process.cwd(),
user: await ensureLoggedInAsync(),
env: buildProfile?.env,
});
async runAsync(
ctx: CredentialsContext,
currentActions: ActionInfo[] = highLevelActions
): Promise<void> {
const accountName = ctx.hasProjectContext
? getProjectAccountName(ctx.exp, ctx.user)
: ensureActorHasUsername(ctx.user);
@@ -70,12 +64,7 @@ export class ManageAndroid {
if (!account) {
throw new Error(`You do not have access to account: ${accountName}`);
}
let gradleContext;
if (ctx.hasProjectContext) {
assert(buildProfile, 'buildProfile must be defined in a project context');
gradleContext = await this.createProjectContextAsync(ctx, buildProfile);
}
const { gradleContext } = await this.createProjectContextAsync(ctx);
while (true) {
try {
@@ -151,18 +140,30 @@ export class ManageAndroid {
}
}
private async createProjectContextAsync(
ctx: CredentialsContext,
buildProfile: BuildProfile<Platform.ANDROID>
): Promise<GradleBuildContext | undefined> {
assert(ctx.hasProjectContext, 'createProjectContextAsync: must have project context.');
private async createProjectContextAsync(ctx: CredentialsContext): Promise<{
gradleContext?: GradleBuildContext;
buildProfile?: AndroidBuildProfile;
}> {
if (!ctx.hasProjectContext) {
return {};
}
const maybeProjectId = await promptToCreateProjectIfNotExistsAsync(ctx.exp);
if (!maybeProjectId) {
throw new Error(
'Your project must be registered with EAS in order to use the credentials manager.'
);
}
return await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
const buildProfile = await new SelectBuildProfileFromEasJson(
ctx.projectDir,
Platform.ANDROID
).runAsync(ctx);
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
return {
gradleContext,
buildProfile,
};
}
private async runProjectSpecificActionAsync(
@@ -1,5 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { IosBuildProfile } from '@expo/eas-json';
import assert from 'assert';
import nullthrows from 'nullthrows';
@@ -17,7 +17,7 @@ import {
} from '../../project/projectUtils';
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
import { Account, findAccountByName } from '../../user/Account';
import { ensureActorHasUsername, ensureLoggedInAsync } from '../../user/actions';
import { ensureActorHasUsername } from '../../user/actions';
import { CredentialsContext } from '../context';
import {
AppStoreApiKeyPurpose,
@@ -59,18 +59,12 @@ import { SelectBuildProfileFromEasJson } from './SelectBuildProfileFromEasJson';
import { SelectIosDistributionTypeGraphqlFromBuildProfile } from './SelectIosDistributionTypeGraphqlFromBuildProfile';
export class ManageIos {
constructor(private callingAction: Action, private projectDir: string) {}
constructor(private callingAction: Action) {}
async runAsync(currentActions: ActionInfo[] = highLevelActions): Promise<void> {
const hasProjectContext = !!CredentialsContext.getExpoConfigInProject(this.projectDir);
const buildProfile = hasProjectContext
? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.IOS).runAsync()
: null;
const ctx = new CredentialsContext({
projectDir: process.cwd(),
user: await ensureLoggedInAsync(),
env: buildProfile?.env,
});
async runAsync(
ctx: CredentialsContext,
currentActions: ActionInfo[] = highLevelActions
): Promise<void> {
const buildCredentialsActions = getBuildCredentialsActions(ctx);
const pushKeyActions = getPushKeyActions(ctx);
const ascApiKeyActions = getAscApiKeyActions(ctx);
@@ -84,16 +78,7 @@ export class ManageIos {
if (!account) {
throw new Error(`You do not have access to account: ${accountName}`);
}
let app = null;
let targets = null;
if (ctx.hasProjectContext) {
assert(buildProfile, 'buildProfile must be defined in project context');
const projectContext = await this.createProjectContextAsync(ctx, account, buildProfile);
app = projectContext.app;
targets = projectContext.targets;
}
const { app, targets, buildProfile } = await this.createProjectContextAsync(ctx, account);
while (true) {
try {
if (ctx.hasProjectContext) {
@@ -169,13 +154,16 @@ export class ManageIos {
private async createProjectContextAsync(
ctx: CredentialsContext,
account: Account,
buildProfile: BuildProfile<Platform.IOS>
): Promise<{
app: App;
targets: Target[];
}> {
assert(ctx.hasProjectContext, 'createProjectContextAsync: must have project context.');
account: Account
): Promise<{ app: App | null; targets: Target[] | null; buildProfile: IosBuildProfile | null }> {
if (!ctx.hasProjectContext) {
return {
app: null,
targets: null,
buildProfile: null,
};
}
const maybeProjectId = await promptToCreateProjectIfNotExistsAsync(ctx.exp);
if (!maybeProjectId) {
throw new Error(
@@ -184,6 +172,10 @@ export class ManageIos {
}
const app = { account, projectName: ctx.exp.slug };
const buildProfile = await new SelectBuildProfileFromEasJson(
ctx.projectDir,
Platform.IOS
).runAsync(ctx);
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
projectDir: ctx.projectDir,
@@ -199,6 +191,7 @@ export class ManageIos {
return {
app,
targets,
buildProfile,
};
}
@@ -226,7 +219,7 @@ export class ManageIos {
ctx: CredentialsContext,
app: App,
targets: Target[],
buildProfile: BuildProfile<Platform.IOS>,
buildProfile: IosBuildProfile,
action: IosActionType
): Promise<void> {
if (action === IosActionType.SetUpBuildCredentials) {
@@ -236,10 +229,7 @@ export class ManageIos {
distribution: buildProfile.distribution,
enterpriseProvisioning: buildProfile.enterpriseProvisioning,
iosCapabilitiesOptions: {
entitlements: await getManagedEntitlementsJsonAsync(
ctx.projectDir,
buildProfile.env ?? {}
),
entitlements: await getManagedEntitlementsJsonAsync(ctx.projectDir),
},
}).runAsync(ctx);
return;
@@ -370,7 +360,7 @@ export class ManageIos {
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
const confirm = await confirmAsync({
message: `Do you want ${appLookupParams.projectName} to use the new API Key?`,
message: `Do you want ${appLookupParams.projectName} to use the new Api Key?`,
});
if (confirm) {
await new AssignAscApiKey(appLookupParams).runAsync(
@@ -3,6 +3,7 @@ import { BuildProfile, EasJsonReader } from '@expo/eas-json';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { CredentialsContext } from '../context';
export class SelectBuildProfileFromEasJson<T extends Platform> {
private easJsonReader: EasJsonReader;
@@ -11,14 +12,14 @@ export class SelectBuildProfileFromEasJson<T extends Platform> {
this.easJsonReader = new EasJsonReader(projectDir);
}
async runAsync(): Promise<BuildProfile<T>> {
const profileName = await this.getProfileNameFromEasConfigAsync();
const easConfig = await this.easJsonReader.getBuildProfileAsync<T>(this.platform, profileName);
async runAsync(ctx: CredentialsContext): Promise<BuildProfile<T>> {
const profileName = await this.getProfileNameFromEasConfigAsync(ctx);
const easConfig = await this.easJsonReader.readBuildProfileAsync<T>(this.platform, profileName);
Log.succeed(`Using build profile: ${profileName}`);
return easConfig;
}
async getProfileNameFromEasConfigAsync(): Promise<string> {
async getProfileNameFromEasConfigAsync(ctx: CredentialsContext): Promise<string> {
const buildProfileNames = await this.easJsonReader.getBuildProfileNamesAsync();
if (buildProfileNames.length === 0) {
throw new Error(
@@ -27,6 +28,11 @@ export class SelectBuildProfileFromEasJson<T extends Platform> {
} else if (buildProfileNames.length === 1) {
return buildProfileNames[0];
}
if (ctx.nonInteractive) {
throw new Error(
'You have multiple build profiles. Please run this command in interactive mode.'
);
}
const { profileName } = await promptAsync({
type: 'select',
name: 'profileName',
@@ -1,12 +1,11 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { IosBuildProfile } from '@expo/eas-json';
import { IosDistributionType as IosDistributionTypeGraphql } from '../../graphql/generated';
import { promptAsync } from '../../prompts';
import { CredentialsContext } from '../context';
export class SelectIosDistributionTypeGraphqlFromBuildProfile {
constructor(private buildProfile: BuildProfile<Platform.IOS>) {}
constructor(private buildProfile: IosBuildProfile) {}
async runAsync(ctx: CredentialsContext): Promise<IosDistributionTypeGraphql> {
const { distribution, simulator } = this.buildProfile;
@@ -1,9 +1,10 @@
import { promptAsync } from '../../prompts';
import { CredentialsContext } from '../context';
import { ManageAndroid } from './ManageAndroid';
import { ManageIos } from './ManageIos';
export class SelectPlatform {
async runAsync(): Promise<void> {
async runAsync(ctx: CredentialsContext): Promise<void> {
const { platform } = await promptAsync({
type: 'select',
name: 'platform',
@@ -15,8 +16,8 @@ export class SelectPlatform {
});
if (platform === 'ios') {
return await new ManageIos(new SelectPlatform(), process.cwd()).runAsync();
return await new ManageIos(new SelectPlatform()).runAsync(ctx);
}
return await new ManageAndroid(new SelectPlatform(), process.cwd()).runAsync();
return await new ManageAndroid(new SelectPlatform()).runAsync(ctx);
}
}
@@ -1,5 +1,4 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { IosBuildProfile } from '@expo/eas-json';
import { IosDistributionType as IosDistributionTypeGraphql } from '../../../graphql/generated';
import { createCtxMock } from '../../__tests__/fixtures-context';
@@ -9,7 +8,7 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
it('errors with a simulator distribution', async () => {
const buildProfile = {
simulator: true,
} as BuildProfile<Platform.IOS>;
} as IosBuildProfile;
const ctx = createCtxMock({
nonInteractive: false,
@@ -23,7 +22,7 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
it('returns APP_STORE type with a store distribution', async () => {
const buildProfile = {
distribution: 'store',
} as BuildProfile<Platform.IOS>;
} as IosBuildProfile;
const ctx = createCtxMock({
nonInteractive: false,
@@ -38,7 +37,7 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
const buildProfile = {
distribution: 'internal',
enterpriseProvisioning: 'universal',
} as BuildProfile<Platform.IOS>;
} as IosBuildProfile;
const ctx = createCtxMock({
nonInteractive: false,
@@ -53,7 +52,7 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
const buildProfile = {
distribution: 'internal',
enterpriseProvisioning: 'adhoc',
} as BuildProfile<Platform.IOS>;
} as IosBuildProfile;
const ctx = createCtxMock({
nonInteractive: false,
+1 -1
View File
@@ -30,7 +30,7 @@ type SessionHeaders = {
};
export const graphqlClient = createUrqlClient({
url: getExpoApiBaseUrl() + '/graphql',
url: getExpoApiBaseUrl() + '/--/graphql',
exchanges: [
dedupExchange,
cacheExchange,
Loaded 100 of 166 files, more files were not shown because too many files have changed in this diff. Show more