Compare commits

..
2 Commits
Author SHA1 Message Date
Dominik Sokal 9b36ae0ef8 convert to ESM 2021-10-11 16:15:21 +02:00
Dominik Sokal 1a4cb107af add clean script 2021-10-11 16:12:42 +02:00
259 changed files with 5578 additions and 9740 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
+3 -3
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
@@ -28,8 +28,8 @@ jobs:
echo "Invalid /changelog-entry command"
exit 1
fi
CATEGORY=$( cut -d ' ' -f 1 <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
CATEGORY=$( cut -d ' ' -f 1 <<< "$PARAMS" | xargs )
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | xargs )
yarn changelog-entry "$CATEGORY" "$MESSAGE"
env:
GITHUB_PR_AUTHOR: ${{ github.event.issue.user.login }}
+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"
}
+4 -216
View File
@@ -6,235 +6,23 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 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))
### 🎉 New features
### 🐛 Bug fixes
- Pass build profile environment to credentials command. ([#828](https://github.com/expo/eas-cli/pull/828) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
## [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))
- Show initiating user display name when selecting a build to submit. ([#730](https://github.com/expo/eas-cli/pull/730) by [@barthap](https://github.com/barthap))
- Handle Apple servers maintenance error in `eas submit`. ([#738](https://github.com/expo/eas-cli/pull/738) by [@barthap](https://github.com/barthap))
- Integrate ASC API Key with submissions workflow. ([#737](https://github.com/expo/eas-cli/pull/737) by [@quinlanj](https://github.com/quinlanj))
- Change EAS API server domain. ([#744](https://github.com/expo/eas-cli/pull/744) by [@ide](https://github.com/ide))
### 🧹 Chores
- Clean up credentials prompt method. ([#728](https://github.com/expo/eas-cli/pull/728) by [@quinlanj](https://github.com/quinlanj))
- Bump `@expo/apple-utils` to 0.0.0-alpha.26. ([#723](https://github.com/expo/eas-cli/pull/723) by [@brentvatne](https://github.com/brentvatne))
- Grammar: replace setup with set up. ([#735](https://github.com/expo/eas-cli/pull/735) by [@quinlanj](https://github.com/quinlanj))
- Improve VCS workflow migration experience. ([#732](https://github.com/expo/eas-cli/pull/732) by [@wkozyra95](https://github.com/wkozyra95))
## [0.34.1](https://github.com/expo/eas-cli/releases/tag/v0.34.1) - 2021-11-02
### 🐛 Bug fixes
- Don't show commit prompt in no-commit workflow after installing `expo-dev-client`. ([#722](https://github.com/expo/eas-cli/pull/722) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Add App Store Connect API Key graphql type and print support. ([#717](https://github.com/expo/eas-cli/pull/717) by [@quinlanj](https://github.com/quinlanj))
## [0.34.0](https://github.com/expo/eas-cli/releases/tag/v0.34.0) - 2021-11-01
### 🛠 Breaking changes
- Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701) and [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal))
- Don't print logs to `stderr`. ([#708](https://github.com/expo/eas-cli/pull/708) by [@dsokal](https://github.com/dsokal))
- Remove automatic migration for legacy `eas.json` format introduced in [v0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2). ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
- Implement no-commit build workflow. Add required `cli` field in root of `eas.json`. ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Better error message when eas.json is invalid. ([#707](https://github.com/expo/eas-cli/pull/707) by [@dsokal](https://github.com/dsokal))
- Fix credentials workflow for new users: add additionalTypenames for credentials. ([#703](https://github.com/expo/eas-cli/pull/703) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Add additionalTypenames for other queries. ([#704](https://github.com/expo/eas-cli/pull/704) by [@quinlanj](https://github.com/quinlanj))
- Fix errors after pulling down most recent graphql schema changes. ([#713](https://github.com/expo/eas-cli/pull/713) by [@quinlanj](https://github.com/quinlanj))
- Refactor credentials manager. ([#712](https://github.com/expo/eas-cli/pull/712) by [@quinlanj](https://github.com/quinlanj))
- Remove unneeded parameters. ([#710](https://github.com/expo/eas-cli/pull/710) by [@quinlanj](https://github.com/quinlanj))
- Enforce additionalTypenames for graphql queries. ([#709](https://github.com/expo/eas-cli/pull/709) by [@quinlanj](https://github.com/quinlanj))
## [0.33.1](https://github.com/expo/eas-cli/releases/tag/v0.33.1) - 2021-10-22
### 🎉 New features
- Compute runtime version using config-plugins. ([#697](https://github.com/expo/eas-cli/pull/697) by [@jkhales](https://github.com/jkhales))
### 🐛 Bug fixes
- [eas-cli] Skip validating updates scripts integration, which no longer exist in SDK 43+ ([#706](https://github.com/expo/eas-cli/pull/706) by [@brentvatne](https://github.com/brentvatne))
## [0.33.0](https://github.com/expo/eas-cli/releases/tag/v0.33.0) - 2021-10-20
### 🎉 New features
- Make "production" the default profile for building and submitting. ([#677](https://github.com/expo/eas-cli/pull/677)) by [@jonsamp](https://github.com/jonsamp)
- Previously, the default profile when running `eas build` or `eas submit` was "release". We're changing it to a more recognizable name that is consistent with our docs, which is now "production". You can always specify a profile manually with the `--profile` flag. For this major version, if you do not have a profile named "production", and still have a profile named "release", we will fall back to the "release" profile as the default, however you'll see a warning as we're going to remove that behavior in the next major release of EAS CLI.
- To upgrade, update **eas.json** to have a "production" profile in both the `build` and `submit` objects. If you already have a project set up, this will replace the existing "release" profile. After the change, eas.json should have the following profiles:
```json
{
"build": {
"production": { ... }
},
"submit": {
"production": { ... }
}
}
```
### 🐛 Bug fixes
- Skip the second prompt for Apple ID if the user is already signed in with Apple. ([#691](https://github.com/expo/eas-cli/pull/691) by [@dsokal](https://github.com/dsokal))
- Unify reading `app.json`. ([#692](https://github.com/expo/eas-cli/pull/692) by [@dsokal](https://github.com/dsokal))
## [0.32.0](https://github.com/expo/eas-cli/releases/tag/v0.32.0) - 2021-10-15
### 🎉 New features
- Add App Store Connect API Key support to iOS submissions. ([#678](https://github.com/expo/eas-cli/pull/678) by [@quinlanj](https://github.com/quinlanj))
- Create/list/revoke App Store Connect Api keys via Apple apis. ([#687](https://github.com/expo/eas-cli/pull/687) by [@quinlanj](https://github.com/quinlanj))
- Add ability to select a build from a list in `eas submit` interactive mode. ([#688](https://github.com/expo/eas-cli/pull/688) by [@barthap](https://github.com/barthap))
### 🐛 Bug fixes
- Fix printing App Store Connect URL after submission. ([#683](https://github.com/expo/eas-cli/pull/683) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
- Add App Store Connect API Key fields to `eas.json`. ([#684](https://github.com/expo/eas-cli/pull/684) by [@quinlanj](https://github.com/quinlanj))
- Enable no-underscore-dangle eslint rule. ([#686](https://github.com/expo/eas-cli/pull/686) by [@dsokal](https://github.com/dsokal))
## [0.31.1](https://github.com/expo/eas-cli/releases/tag/v0.31.1) - 2021-10-08
## [0.31.1](https://github.com/expo/eas-cli/releases/tag/0.31.1) - 2021-10-08
### 🐛 Bug fixes
- Google Service Account Keys: Fix non-interactive bug in credentials service workflow ([#682](https://github.com/expo/eas-cli/pull/682) by [@quinlanj](https://github.com/quinlanj))
## [0.31.0](https://github.com/expo/eas-cli/releases/tag/v0.31.0) - 2021-10-08
## [0.31.0](https://github.com/expo/eas-cli/releases/tag/0.31.0) - 2021-10-08
### 🛠 Breaking changes
@@ -245,7 +33,7 @@ This is the log of notable changes to EAS CLI and related packages.
- Integrate credentials service with Android submissions. ([#664](https://github.com/expo/eas-cli/pull/664) by [@quinlanj](https://github.com/quinlanj))
- Add option to review ad-hoc devices when reusing provisioning profile. ([#673](https://github.com/expo/eas-cli/pull/673) by [@dsokal](https://github.com/dsokal))
## [0.30.1](https://github.com/expo/eas-cli/releases/tag/v0.30.1) - 2021-10-06
## [0.30.1](https://github.com/expo/eas-cli/releases/tag/0.30.1) - 2021-10-06
### 🐛 Bug fixes
@@ -624,7 +412,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
- Remove "Auto" option for `schemeBuildConfiguration` and make the old "Auto" behaviour the default. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
- `[EAS BUILD API]` Remove "Auto" option for `schemeBuildConfiguration` and make the old "Auto" behaviour the default. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
- Remove `experimental.disableIosBundleIdentifierValidation` flag from eas.json. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
- Deprecate `android.package` and `ios.bundleIdentifier` in app config for generic projects. EAS CLI depends on the values in native code now. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
- Remove application id synchronization (`android.package` and `ios.bundleIdentifier`) between app.json and native code when running builds. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
+7 -1
View File
@@ -1,4 +1,10 @@
## Releasing a new version
1. Run `yarn release` in the repository root folder.
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
2. Wait, the `release` workflow on GitHub Actions is going to take care of the rest (including publishing packages to npm and updating the changelog).
3. Check the changelog. If any of the entries have been tagged with `[EAS BUILD API]`
- Open `package.json` and note the version of `@expo/eas-build-job` used in the eas-cli.
- Find the `Publish` commit in [expo/eas-build](https://github.com/expo/eas-build/commit/) for that version. In most cases, it will be the latest `Publish` commit.
- Find the `eas-cli-local-build-plugin` version published in that commit.
![example publish commit](./.gh-assets/eas-build-publish-commit.png)
- Run `npm dist-tags add eas-cli-local-build-plugin@VERSION latest` with the version from the previous step.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.40.0"
"version": "0.31.1"
}
+6 -8
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,10 +8,10 @@
"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"
"clean": "lerna run clean"
},
"devDependencies": {
"@expo/oclif-dev-cli": "1.24.0-expo",
@@ -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",
+46 -48
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
@@ -73,7 +73,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/account/login.js)_
## `eas account:logout`
@@ -87,7 +87,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/account/logout.js)_
## `eas account:view`
@@ -101,7 +101,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/account/view.js)_
## `eas analytics [STATUS]`
@@ -112,11 +112,11 @@ USAGE
$ eas analytics [STATUS]
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/analytics.js)_
## `eas build`
Start a build
start a build
```
USAGE
@@ -138,15 +138,14 @@ OPTIONS
--non-interactive Run command in non-interactive mode
--profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined
in eas.json.
--profile=PROFILE_NAME [default: release] Name of the build profile from eas.json
--skip-project-configuration Skip project configuration
--[no-]wait Wait for build(s) to complete
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/build/index.js)_
## `eas build:cancel [BUILD_ID]`
@@ -157,7 +156,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/build/cancel.js)_
## `eas build:configure`
@@ -171,7 +170,7 @@ OPTIONS
-p, --platform=(android|ios|all) Platform to configure
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/build/configure.js)_
## `eas build:list`
@@ -204,7 +203,7 @@ OPTIONS
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/build/list.js)_
## `eas build:view [BUILD_ID]`
@@ -218,7 +217,7 @@ OPTIONS
--json Enable JSON output, non-JSON messages will be printed to stderr
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/build/view.js)_
## `eas config`
@@ -233,7 +232,7 @@ OPTIONS
--profile=profile
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/config.js)_
## `eas credentials`
@@ -244,7 +243,7 @@ USAGE
$ eas credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/credentials.js)_
## `eas device:create`
@@ -255,7 +254,7 @@ USAGE
$ eas device:create
```
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/device/create.js)_
## `eas device:list`
@@ -269,7 +268,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/device/list.js)_
## `eas device:view [UDID]`
@@ -280,7 +279,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/device/view.js)_
## `eas diagnostics`
@@ -291,7 +290,7 @@ USAGE
$ eas diagnostics
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/diagnostics.js)_
## `eas help [COMMAND]`
@@ -308,7 +307,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`
@@ -319,7 +318,7 @@ USAGE
$ eas project:info
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/project/info.js)_
## `eas project:init`
@@ -333,7 +332,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/project/init.ts)_
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/project/init.js)_
## `eas secret:create`
@@ -350,7 +349,7 @@ OPTIONS
--value=value Value of the secret
```
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/secret/create.js)_
## `eas secret:delete`
@@ -364,10 +363,10 @@ OPTIONS
--id=id ID of the secret to delete
DESCRIPTION
Unsure where to find the secret's ID? Run eas secret:list
Unsure where to find the secret's ID? Run eas secrets:list
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/secret/delete.js)_
## `eas secret:list`
@@ -378,11 +377,11 @@ USAGE
$ eas secret:list
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/secret/list.js)_
## `eas submit`
Submit build archive to App Store Connect
submit build archive to app store
```
USAGE
@@ -395,7 +394,7 @@ OPTIONS
--non-interactive Run command in non-interactive mode
--path=path Path to the .apk/.aab/.ipa file
--profile=profile Name of the submit profile from eas.json. Defaults to "production" if defined in
--profile=profile Name of the submit profile from eas.json. Defaults to "release" if defined in
eas.json.
--url=url App archive url
@@ -405,14 +404,13 @@ OPTIONS
--[no-]wait Wait for submission to complete
DESCRIPTION
See how to configure submits with eas.json:
https://docs.expo.dev/submit/eas-json/
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/submit.js)_
## `eas webhook:create`
@@ -423,15 +421,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.40.0/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/webhook/create.js)_
## `eas webhook:delete [ID]`
@@ -445,7 +443,7 @@ ARGUMENTS
ID ID of the webhook to delete
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/webhook/delete.js)_
## `eas webhook:list`
@@ -456,10 +454,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.40.0/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/webhook/list.js)_
## `eas webhook:update`
@@ -470,16 +468,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.40.0/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/webhook/update.js)_
## `eas webhook:view ID`
@@ -493,5 +491,5 @@ ARGUMENTS
ID ID of the webhook to view
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.40.0/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/webhook/view.js)_
<!-- commandsstop -->
+1 -1
View File
@@ -1,5 +1,5 @@
overwrite: true
schema: 'https://staging-api.expo.dev/graphql'
schema: 'https://staging.exp.host/--/graphql'
documents:
- 'src/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
+1325 -1850
View File
File diff suppressed because it is too large. Load diff
+19 -18
View File
@@ -1,31 +1,33 @@
{
"name": "eas-cli",
"type": "module",
"description": "EAS command line tool",
"version": "0.40.0",
"version": "0.31.1",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.26",
"@expo/config": "6.0.11",
"@expo/config-plugins": "4.0.11",
"@expo/eas-build-job": "0.2.61",
"@expo/eas-json": "^0.40.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/apple-utils": "0.0.0-alpha.25",
"@expo/config": "6.0.2",
"@expo/config-plugins": "4.0.2",
"@expo/eas-build-job": "0.2.52",
"@expo/eas-json": "^0.31.0",
"@expo/json-file": "8.2.33",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.14",
"@expo/plugin-warn-if-update-available": "1.7.0",
"@expo/prebuild-config": "3.0.2",
"@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 +97,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"
@@ -177,6 +178,6 @@
"test": "jest",
"version": "yarn oclif-dev readme && node patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
"clean": "rm -rf dist build node_modules yarn-error.log"
"clean": "rm -rf build node_modules"
}
}
+2 -2
View File
@@ -8,7 +8,7 @@ import { apiClient, getExpoApiBaseUrl } from '../api';
describe('apiClient', () => {
it('converts Expo APIv2 error to ApiV2Error', async () => {
nock(getExpoApiBaseUrl())
.post('/v2/test')
.post('/--/api/v2/test')
.reply(400, {
errors: [
{
@@ -39,7 +39,7 @@ describe('apiClient', () => {
});
it('does not convert non-APIv2 error to ApiV2Error', async () => {
nock(getExpoApiBaseUrl()).post('/v2/test').reply(500, 'Something went wrong');
nock(getExpoApiBaseUrl()).post('/--/api/v2/test').reply(500, 'Something went wrong');
let error: Error | null = null;
try {
@@ -3,8 +3,14 @@ import os from 'os';
import { URL } from 'url';
import { v4 as uuidv4 } from 'uuid';
import UserSettings from '../user/UserSettings';
import { easCliVersion } from '../utils/easCli';
import UserSettings from './user/UserSettings';
/**
* We use require() to exclude package.json from TypeScript's analysis since it lives outside
* the src directory and would change the directory structure of the emitted files
* under the build directory
*/
const packageJSON = require('../package.json');
const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
darwin: 'Mac',
@@ -91,7 +97,7 @@ export function logEvent(name: string, properties: Record<string, any> = {}): vo
ensureUserIdentified();
const { userId, deviceId } = identifyData ?? {};
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
const commonEventProperties = { source_version: packageJSON?.version, source: 'eas cli' };
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
rudderstackClient.track({
@@ -120,7 +126,7 @@ function getRudderStackContext(): Record<string, any> {
return {
os: { name: platform, version: os.release() },
device: { type: platform, model: platform },
app: { name: 'eas cli', version: easCliVersion ?? undefined },
app: { name: 'eas cli', version: packageJSON?.version ?? undefined },
};
}
-25
View File
@@ -1,25 +0,0 @@
import { Analytics, Event } from './events';
export type TrackingContext = Record<string, string | number | boolean>;
export async function withAnalyticsAsync<Result>(
fn: () => Promise<Result>,
analytics: {
attemptEvent: Event;
successEvent: Event;
failureEvent: Event;
trackingCtx: TrackingContext;
}
): Promise<Result> {
try {
const result = await fn();
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
return result;
} catch (error: any) {
Analytics.logEvent(analytics.failureEvent, {
...analytics.trackingCtx,
reason: error.message,
});
throw error;
}
}
-52
View File
@@ -1,52 +0,0 @@
import { logEvent } from './rudderstackClient';
export type Event = BuildEvent | SubmissionEvent;
export enum SubmissionEvent {
SUBMIT_COMMAND = 'submit cli submit command',
SUBMIT_COMMAND_ATTEMPT = 'submit cli attempt',
SUBMIT_COMMAND_SUCCESS = 'submit cli success',
SUBMIT_COMMAND_FAIL = 'submit cli fail',
GATHER_CREDENTIALS_ATTEMPT = 'submit cli gather credentials attempt',
GATHER_CREDENTIALS_SUCCESS = 'submit cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'submit cli gather credentials fail',
GATHER_ARCHIVE_ATTEMPT = 'submit cli gather archive attempt',
GATHER_ARCHIVE_SUCCESS = 'submit cli gather archive success',
GATHER_ARCHIVE_FAIL = 'submit cli gather archive fail',
SUBMIT_REQUEST_ATTEMPT = 'submit cli request attempt',
SUBMIT_REQUEST_SUCCESS = 'submit cli request success',
SUBMIT_REQUEST_FAIL = 'submit cli request fail',
}
export enum BuildEvent {
BUILD_COMMAND = 'build cli build command',
PROJECT_UPLOAD_ATTEMPT = 'build cli project upload attempt',
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
GATHER_CREDENTIALS_ATTEMPT = 'build cli gather credentials attempt',
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
CONFIGURE_PROJECT_ATTEMPT = 'build cli configure project attempt',
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
BUILD_REQUEST_ATTEMPT = 'build cli build request attempt',
BUILD_REQUEST_SUCCESS = 'build cli build request success',
BUILD_REQUEST_FAIL = 'build cli build request fail',
BUILD_STATUS_COMMAND = 'build cli build status',
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
CREDENTIALS_SYNC_UPDATE_LOCAL_ATTEMPT = 'build cli credentials sync update local attempt',
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
CREDENTIALS_SYNC_UPDATE_REMOTE_ATTEMPT = 'build cli credentials sync update remote attempt',
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
}
export class Analytics {
static logEvent(name: Event, properties: Record<string, any>): void {
logEvent(name, properties);
}
}
+3 -3
View File
@@ -4,7 +4,7 @@ import ApiV2Error from './ApiV2Error';
import { getAccessToken, getSessionSecret } from './user/sessionStorage';
export const apiClient = got.extend({
prefixUrl: getExpoApiBaseUrl() + '/v2/',
prefixUrl: getExpoApiBaseUrl() + '/--/api/v2/',
hooks: {
beforeRequest: [
(options: NormalizedOptions) => {
@@ -40,11 +40,11 @@ export const apiClient = got.extend({
export function getExpoApiBaseUrl(): string {
if (process.env.EXPO_STAGING) {
return `https://staging-api.expo.dev`;
return `https://staging.exp.host`;
} else if (process.env.EXPO_LOCAL) {
return `http://127.0.0.1:3000`;
} else {
return `https://api.expo.dev`;
return `https://exp.host`;
}
}
@@ -1,5 +1,6 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig, AndroidManifest } from '@expo/config-plugins';
import fs from 'fs-extra';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
@@ -9,6 +10,17 @@ import { ensureValidVersions } from '../utils/updates';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
const updatedBuildGradleContents =
AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
projectDir,
buildGradleContents
);
await fs.writeFile(buildGradlePath, updatedBuildGradleContents);
}
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
const androidManifest = await getAndroidManifestAsync(projectDir);
@@ -27,7 +39,7 @@ export async function syncUpdatesConfigurationAsync(
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
try {
await ensureUpdatesConfiguredAsync(projectDir);
await ensureUpdatesConfiguredAsync(projectDir, exp);
} catch (error) {
Log.error(
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
@@ -50,9 +62,15 @@ export async function syncUpdatesConfigurationAsync(
}
}
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in AndroidManifest.xml
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
const gradleScriptApply = AndroidConfig.Updates.formatApplyLineForBuildGradle(projectDir);
throw new Error(`Missing ${gradleScriptApply} in ${buildGradlePath}`);
}
const androidManifest = await getAndroidManifestAsync(projectDir);
if (!AndroidConfig.Updates.isMainApplicationMetaDataSet(androidManifest)) {
@@ -31,7 +31,7 @@ beforeEach(async () => {
await fs.mkdirp(os.tmpdir());
});
// bare workflow
// generic workflow
describe(bumpVersionAsync, () => {
it('throws an informative error when multiple flavors are detected in the android project', async () => {
await initProjectWithGradleFileAsync(
@@ -69,7 +69,7 @@ describe(bumpVersionAsync, () => {
});
it('bumps expo.android.versionCode and buildGradle versionCode when strategy = BumpStrategy.VERSION_CODE', async () => {
const nativeVersionCode = 100; // this should be overwritten by bumpVersionAsync
const fakeExp = initBareWorkflowProject({ versionCode: nativeVersionCode });
const fakeExp = initGenericProject({ versionCode: nativeVersionCode });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
@@ -91,7 +91,7 @@ describe(bumpVersionAsync, () => {
it('bumps expo.version and gradle versionCode when strategy = BumpStrategy.SHORT_VERSION', async () => {
const nativeVersionName = '1.0.0'; // this should be overwritten by bumpVersionAsync
const fakeExp = initBareWorkflowProject({ versionName: nativeVersionName });
const fakeExp = initGenericProject({ versionName: nativeVersionName });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
@@ -112,7 +112,7 @@ describe(bumpVersionAsync, () => {
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initBareWorkflowProject();
const fakeExp = initGenericProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
@@ -185,9 +185,9 @@ describe(bumpVersionInAppJsonAsync, () => {
});
describe(maybeResolveVersionsAsync, () => {
describe('bare project', () => {
describe('generic project', () => {
it('reads the versions from native code', async () => {
const exp = initBareWorkflowProject();
const exp = initGenericProject();
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/repo',
exp,
@@ -211,7 +211,7 @@ describe(maybeResolveVersionsAsync, () => {
});
});
function initBareWorkflowProject({
function initGenericProject({
versionCode = 123,
versionName = '3.0.0',
}: {
+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,15 +1,15 @@
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 {
ensureApplicationIdIsDefinedForManagedProjectAsync,
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject,
warnIfAndroidPackageDefinedInAppConfigForGenericProject,
} from '../../project/android/applicationId';
import { resolveWorkflowAsync } from '../../project/workflow';
import { getVcsClient } from '../../vcs';
import vcs from '../../vcs';
import { ConfigureContext } from '../context';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
@@ -21,12 +21,12 @@ export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void
return;
}
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
warnIfAndroidPackageDefinedInAppConfigForGenericProject(ctx.projectDir, ctx.exp);
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
const easGradlePath = AndroidConfig.EasBuild.getEasBuildGradlePath(ctx.projectDir);
await getVcsClient().trackFileAsync(easGradlePath);
await vcs.trackFileAsync(easGradlePath);
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
@@ -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;
@@ -17,14 +17,15 @@ export function transformJob(job: Android.Job): AndroidJobInput {
artifactPath: job.artifactPath,
username: job.username,
buildType: job.buildType && transformBuildType(job.buildType),
developmentClient: job.developmentClient,
};
}
function transformBuildType(buildType: Android.BuildType): AndroidBuildType {
if (buildType === Android.BuildType.APK) {
return AndroidBuildType.Apk;
} else {
} else if (buildType === Android.BuildType.APP_BUNDLE) {
return AndroidBuildType.AppBundle;
} else {
return AndroidBuildType.DevelopmentClient;
}
}
@@ -1,12 +1,12 @@
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';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { getVcsClient } from '../../vcs';
import vcs from '../../vcs';
import { BuildContext } from '../context';
interface JobData {
@@ -25,9 +25,9 @@ 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)) || '.';
slash(path.relative(await vcs.getRootPathAsync(), ctx.projectDir)) || '.';
const { credentials } = jobData;
const buildCredentials = credentials
? {
@@ -70,11 +70,12 @@ export async function prepareJobAsync(
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
developmentClient: buildProfile.developmentClient,
gradleCommand: buildProfile.gradleCommand,
artifactPath: buildProfile.artifactPath,
buildType,
username,
buildType: buildProfile.developmentClient ? Android.BuildType.DEVELOPMENT_CLIENT : buildType,
};
return sanitizeJob(job);
+37 -18
View File
@@ -3,8 +3,6 @@ import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import { withAnalyticsAsync } from '../analytics/common';
import { BuildEvent } from '../analytics/events';
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
import { BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildQuery } from '../graphql/queries/BuildQuery';
@@ -15,10 +13,12 @@ import { uploadAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
import { getVcsClient } from '../vcs';
import vcs from '../vcs';
import { BuildContext } from './context';
import { runLocalBuildAsync } from './local';
import { 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,22 +55,20 @@ export async function prepareBuildRequestForPlatformAsync<
const credentialsResult = await withAnalyticsAsync(
async () => await builder.ensureCredentialsAsync(ctx),
{
attemptEvent: BuildEvent.GATHER_CREDENTIALS_ATTEMPT,
successEvent: BuildEvent.GATHER_CREDENTIALS_SUCCESS,
failureEvent: BuildEvent.GATHER_CREDENTIALS_FAIL,
successEvent: Event.GATHER_CREDENTIALS_SUCCESS,
failureEvent: Event.GATHER_CREDENTIALS_FAIL,
trackingCtx: ctx.trackingCtx,
}
);
if (!ctx.skipProjectConfiguration) {
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(ctx), {
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
successEvent: Event.CONFIGURE_PROJECT_SUCCESS,
failureEvent: Event.CONFIGURE_PROJECT_FAIL,
trackingCtx: ctx.trackingCtx,
});
}
if (await getVcsClient().isCommitRequiredAsync()) {
if (await vcs.hasUncommittedChangesAsync()) {
Log.addNewLineIfNone();
await reviewAndCommitChangesAsync(
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
@@ -87,7 +86,8 @@ 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,
@@ -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 } = {}
+14 -26
View File
@@ -10,8 +10,7 @@ import { RequestedPlatform } from '../platform';
import { resolveWorkflowAsync } from '../project/workflow';
import { confirmAsync, promptAsync } from '../prompts';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs';
import vcs from '../vcs';
import { configureAndroidAsync } from './android/configure';
import { ConfigureContext } from './context';
import { configureIosAsync } from './ios/configure';
@@ -38,7 +37,7 @@ export async function ensureProjectConfiguredAsync(
projectDir,
platform: requestedPlatform,
});
if (await getVcsClient().isCommitRequiredAsync()) {
if (await vcs.hasUncommittedChangesAsync()) {
error(
'Build process requires clean working tree, please commit all your changes and run `eas build` again',
{ exit: 1 }
@@ -58,6 +57,7 @@ export async function configureAsync(options: {
platform: RequestedPlatform;
projectDir: string;
}): Promise<void> {
await vcs.ensureRepoExistsAsync();
await maybeBailOnRepoStatusAsync();
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
@@ -86,39 +86,31 @@ export async function configureAsync(options: {
await configureIosAsync(ctx);
}
if (await getVcsClient().isCommitRequiredAsync()) {
if (await vcs.hasUncommittedChangesAsync()) {
Log.newLine();
await reviewAndCommitChangesAsync(configureCommitMessage[options.platform]);
} else if (!(await getVcsClient().hasUncommittedChangesAsync())) {
} else {
Log.newLine();
Log.withTick('No changes were necessary, the project is already configured correctly.');
}
}
const EAS_JSON_MANAGED_DEFAULT: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
},
build: {
release: {},
development: {
developmentClient: true,
distribution: 'internal',
},
preview: {
distribution: 'internal',
},
production: {},
},
submit: {
production: {},
release: {},
},
};
const EAS_JSON_BARE_DEFAULT: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
},
const EAS_JSON_GENERIC_DEFAULT: EasJson = {
build: {
release: {},
development: {
distribution: 'internal',
android: {
@@ -128,13 +120,9 @@ const EAS_JSON_BARE_DEFAULT: EasJson = {
buildConfiguration: 'Debug',
},
},
preview: {
distribution: 'internal',
},
production: {},
},
submit: {
production: {},
release: {},
},
};
@@ -143,7 +131,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;
@@ -151,11 +139,11 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
const easJson =
ctx.hasAndroidNativeProject && ctx.hasIosNativeProject
? EAS_JSON_BARE_DEFAULT
? EAS_JSON_GENERIC_DEFAULT
: EAS_JSON_MANAGED_DEFAULT;
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
await getVcsClient().trackFileAsync(easJsonPath);
await vcs.trackFileAsync(easJsonPath);
Log.withTick('Generated eas.json');
}
@@ -189,7 +177,7 @@ async function reviewAndCommitChangesAsync(
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await getVcsClient().showDiffAsync();
await vcs.showDiffAsync();
await reviewAndCommitChangesAsync(initialCommitMessage, false);
}
}
+119 -23
View File
@@ -1,15 +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 { ensureLoggedInAsync } from '../user/actions';
import { TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
export interface ConfigureContext {
user: Actor;
@@ -22,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>;
@@ -54,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;
}
}
-138
View File
@@ -1,138 +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';
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,
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,
local,
nonInteractive,
platform,
projectDir,
projectId,
projectName,
skipProjectConfiguration,
trackingCtx,
user,
workflow,
};
if (platform === Platform.ANDROID) {
const common = commonContext as CommonContext<Platform.ANDROID>;
return {
...common,
android: await createAndroidContextAsync(common),
} as BuildContext<T>;
} else {
const common = commonContext as CommonContext<Platform.IOS>;
return {
...common,
ios: await createIosContextAsync(common),
} as BuildContext<T>;
}
}
function getDevClientEventProperties({
platform,
projectDir,
buildProfile,
}: {
platform: Platform;
projectDir: string;
buildProfile: 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;
}
}
@@ -1,16 +1,30 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import fs from 'fs-extra';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
import { getVcsClient } from '../../vcs';
import vcs from '../../vcs';
import { ensureValidVersions } from '../utils/updates';
import { readPlistAsync, writePlistAsync } from './plist';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
let xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, xcodeProject)) {
xcodeProject = IOSConfig.Updates.ensureBundleReactNativePhaseContainsConfigurationScript(
projectDir,
xcodeProject
);
await fs.writeFile(
IOSConfig.Paths.getPBXProjectPath(projectDir),
// eslint-disable-next-line node/no-sync
xcodeProject.writeSync()
);
}
let expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
@@ -27,7 +41,7 @@ export async function syncUpdatesConfigurationAsync(
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
try {
await ensureUpdatesConfiguredAsync(projectDir);
await ensureUpdatesConfiguredAsync(projectDir, exp);
} catch (error) {
Log.error(
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
@@ -48,9 +62,15 @@ export async function syncUpdatesConfigurationAsync(
}
}
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in Expo.plist
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
const xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, xcodeProject)) {
const script = 'expo-updates/scripts/create-manifest-ios.sh';
const buildPhase = '"Bundle React Native code and images"';
throw new Error(`Path to ${script} is missing in a ${buildPhase} build phase.`);
}
const expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistConfigurationSet(expoPlist)) {
throw new Error('Missing values in Expo.plist');
@@ -59,7 +79,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(
@@ -68,7 +88,7 @@ async function writeExpoPlistAsync(
): Promise<void> {
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
await writePlistAsync(expoPlistPath, expoPlist);
await getVcsClient().trackFileAsync(expoPlistPath);
await vcs.trackFileAsync(expoPlistPath);
}
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
@@ -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,
@@ -49,10 +49,10 @@ describe(evaluateTemplateString, () => {
});
});
// bare workflow
// generic workflow
describe(bumpVersionAsync, () => {
it('bumps expo.ios.buildNumber and CFBundleVersion when strategy = BumpStrategy.BUILD_NUMBER', async () => {
const fakeExp = initBareWorkflowProject();
const fakeExp = initGenericProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
@@ -74,7 +74,7 @@ describe(bumpVersionAsync, () => {
});
it('bumps expo.ios.buildNumber and CFBundleVersion for non default Info.plist location', async () => {
const fakeExp = initBareWorkflowProject({ infoPlistName: 'Info2.plist' });
const fakeExp = initGenericProject({ infoPlistName: 'Info2.plist' });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
@@ -98,7 +98,7 @@ describe(bumpVersionAsync, () => {
});
it('bumps expo.version and CFBundleShortVersionString when strategy = BumpStrategy.SHORT_VERSION', async () => {
const fakeExp = initBareWorkflowProject();
const fakeExp = initGenericProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
@@ -120,7 +120,7 @@ describe(bumpVersionAsync, () => {
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initBareWorkflowProject();
const fakeExp = initGenericProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
@@ -194,9 +194,9 @@ describe(bumpVersionInAppJsonAsync, () => {
});
describe(readBuildNumberAsync, () => {
describe('bare project', () => {
describe('generic project', () => {
it('reads the build number from native code', async () => {
const exp = initBareWorkflowProject();
const exp = initGenericProject();
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
expect(buildNumber).toBe('1');
});
@@ -212,14 +212,14 @@ describe(readBuildNumberAsync, () => {
});
describe(readShortVersionAsync, () => {
describe('bare project', () => {
describe('generic project', () => {
it('reads the short version from native code', async () => {
const exp = initBareWorkflowProject();
const exp = initGenericProject();
const appVersion = await readShortVersionAsync('/repo', exp, {});
expect(appVersion).toBe('1.0.0');
});
it('evaluates interpolated build number', async () => {
const exp = initBareWorkflowProject({
const exp = initGenericProject({
appVersion: '$(CURRENT_PROJECT_VERSION)',
});
const buildNumber = await readShortVersionAsync('/repo', exp, {
@@ -231,7 +231,7 @@ describe(readShortVersionAsync, () => {
describe('managed project', () => {
it('reads the version from app config', async () => {
const exp = initBareWorkflowProject();
const exp = initGenericProject();
const appVersion = await readShortVersionAsync('/repo', exp, {});
expect(appVersion).toBe('1.0.0');
});
@@ -286,7 +286,7 @@ describe(getInfoPlistPath, () => {
});
});
function initBareWorkflowProject({
function initGenericProject({
appVersion = '1.0.0',
version = '1',
infoPlistName = 'Info.plist',
+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,
};
}
+4 -4
View File
@@ -1,12 +1,12 @@
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';
import {
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject,
warnIfBundleIdentifierDefinedInAppConfigForGenericProject,
} from '../../project/ios/bundleIdentifier';
import { resolveWorkflowAsync } from '../../project/workflow';
import { ConfigureContext } from '../context';
@@ -20,7 +20,7 @@ export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
return;
}
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
warnIfBundleIdentifierDefinedInAppConfigForGenericProject(ctx.projectDir, ctx.exp);
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
@@ -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,
+28 -3
View File
@@ -1,7 +1,12 @@
import { Env, Ios } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
import {
DistributionType,
IosBuildType,
IosJobInput,
IosJobSecretsInput,
} from '../../graphql/generated';
import { transformProjectArchive, transformWorkflow } from '../graphql';
export function transformJob(job: Ios.Job): IosJobInput {
@@ -11,18 +16,30 @@ export function transformJob(job: Ios.Job): IosJobInput {
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
updates: job.updates,
distribution: job.distribution && transformDistributionType(job.distribution),
secrets: transformIosSecrets(job.secrets),
builderEnvironment: job.builderEnvironment,
cache: job.cache,
scheme: job.scheme,
buildConfiguration: job.buildConfiguration,
artifactPath: job.artifactPath,
buildType: job.buildType && transformBuildType(job.buildType),
username: job.username,
developmentClient: job.developmentClient,
simulator: job.simulator,
};
}
function transformDistributionType(distributionType: Ios.DistributionType): DistributionType {
if (distributionType === 'store') {
return DistributionType.Store;
} else if (distributionType === 'internal') {
return DistributionType.Internal;
} else {
return DistributionType.Simulator;
}
}
function transformIosSecrets(secrets: {
buildCredentials?: Ios.BuildCredentials;
env?: Env;
@@ -44,3 +61,11 @@ function transformIosSecrets(secrets: {
environmentSecrets: secrets.env,
};
}
function transformBuildType(buildType: Ios.BuildType): IosBuildType {
if (buildType === Ios.BuildType.DEVELOPMENT_CLIENT) {
return IosBuildType.DevelopmentClient;
} else {
return IosBuildType.Release;
}
}
@@ -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 {};
}
}
+8 -4
View File
@@ -5,7 +5,7 @@ import slash from 'slash';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { getVcsClient } from '../../vcs';
import vcs from '../../vcs';
import { BuildContext } from '../context';
interface JobData {
@@ -25,7 +25,7 @@ export async function prepareJobAsync(
jobData: JobData
): Promise<Job> {
const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
slash(path.relative(await vcs.getRootPathAsync(), ctx.projectDir)) || '.';
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
if (jobData.credentials) {
@@ -40,6 +40,7 @@ export async function prepareJobAsync(
platform: Platform.IOS,
projectArchive: jobData.projectArchive,
projectRootDirectory,
distribution: ctx.buildProfile.simulator ? 'simulator' : ctx.buildProfile.distribution,
builderEnvironment: {
image: ctx.buildProfile.image,
node: ctx.buildProfile.node,
@@ -60,11 +61,14 @@ export async function prepareJobAsync(
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
developmentClient: ctx.buildProfile.developmentClient,
simulator: ctx.buildProfile.simulator,
scheme: jobData.buildScheme,
buildConfiguration: ctx.buildProfile.buildConfiguration,
artifactPath: ctx.buildProfile.artifactPath,
buildType: ctx.buildProfile.developmentClient
? Ios.BuildType.DEVELOPMENT_CLIENT
: Ios.BuildType.RELEASE,
username,
};
return sanitizeJob(job);
+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]);
+34 -28
View File
@@ -1,39 +1,45 @@
import { Job } from '@expo/eas-build-job';
import spawnAsync from '@expo/spawn-async';
import semver from 'semver';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import Log from '../log';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.54';
export async function runLocalBuildAsync(job: Job): Promise<void> {
const { command, args } = await getCommandAndArgsAsync(job);
await spawnAsync(command, args, {
stdio: 'inherit',
});
}
async function getCommandAndArgsAsync(job: Job): Promise<{ command: string; args: string[] }> {
const jobBase64 = Buffer.from(JSON.stringify({ job })).toString('base64');
if (process.env.EAS_LOCAL_BUILD_PLUGIN_PATH) {
return {
command: process.env.EAS_LOCAL_BUILD_PLUGIN_PATH,
args: [jobBase64],
};
} else {
const args = [`${PLUGIN_PACKAGE_NAME}@${PLUGIN_PACKAGE_VERSION}`, jobBase64];
if (await isAtLeastNpm7Async()) {
// npx shipped with npm >= 7.0.0 requires the "-y" flag to run commands without
// prompting the user to install a package that is used for the first time
args.unshift('-y');
const execNameOrPath =
process.env.EAS_LOCAL_BUILD_PLUGIN_PATH ??
(await findWithNodeResolutionAsync()) ??
PLUGIN_PACKAGE_NAME;
try {
const arg = Buffer.from(JSON.stringify({ job })).toString('base64');
await spawnAsync(execNameOrPath, [arg], {
stdio: 'inherit',
});
} catch (err: any) {
if (err.code === 'ENOENT') {
Log.warn(`Could not resolve executable ${execNameOrPath}.`);
Log.warn(
`Install ${PLUGIN_PACKAGE_NAME} package from npm e.g. ${chalk.bold(
`npm install -g ${PLUGIN_PACKAGE_NAME}`
)} and make sure it's in PATH.`
);
throw err;
}
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;
}
+45 -43
View File
@@ -1,14 +1,13 @@
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';
import { getVcsClient } from '../vcs';
import vcs from '../vcs';
import {
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
@@ -22,20 +21,29 @@ 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;
}
/**
* We use require() to exclude package.json from TypeScript's analysis since it lives outside
* the src directory and would change the directory structure of the emitted files
* under the build directory
*/
const packageJSON = require('../../package.json');
export type MetadataContext<T extends Platform> = T extends Platform.ANDROID
? AndroidMetadataContext
: IosMetadataContext;
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,20 +52,18 @@ export async function collectMetadataAsync<T extends Platform>(
: ctx.buildProfile.distribution) ?? 'store';
const metadata = {
trackingContext: ctx.trackingCtx,
...(await maybeResolveVersionsAsync(ctx)),
cliVersion: easCliVersion,
...(await maybeResolveVersionsAsync(ctx, platformContext)),
cliVersion: packageJSON.version,
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(),
gitCommitHash: await vcs.getCommitHashAsync(),
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
@@ -69,14 +75,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 +93,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 +159,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 {
+2
View File
@@ -12,3 +12,5 @@ export enum BuildDistributionType {
INTERNAL = 'internal',
SIMULATOR = 'simulator',
}
export type TrackingContext = Record<string, string | number | boolean>;
@@ -0,0 +1,29 @@
import { logEvent } from '../../analytics';
export enum Event {
BUILD_COMMAND = 'build cli build command',
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
BUILD_REQUEST_SUCCESS = 'build cli build request success',
BUILD_REQUEST_FAIL = 'build cli build request fail',
BUILD_STATUS_COMMAND = 'build cli build status',
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
}
export default {
logEvent(name: Event, properties: Record<string, any>) {
logEvent(name, properties);
},
};
+3 -12
View File
@@ -1,6 +1,6 @@
import * as ExpoConfig from '@expo/config';
import JsonFile from '@expo/json-file';
import assert from 'assert';
import fs from 'fs-extra';
export async function updateAppJsonConfigAsync(
{
@@ -15,19 +15,10 @@ export async function updateAppJsonConfigAsync(
const paths = ExpoConfig.getConfigFilePaths(projectDir);
assert(paths.staticConfigPath, "can't update dynamic config");
const rawStaticConfig = readAppJson(paths.staticConfigPath);
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
rawStaticConfig.expo = rawStaticConfig.expo ?? {};
modifyConfig(rawStaticConfig.expo);
await JsonFile.writeAsync(paths.staticConfigPath, rawStaticConfig, { json5: false });
await fs.writeJson(paths.staticConfigPath, rawStaticConfig, { spaces: 2 });
modifyConfig(exp);
}
// TODO: remove this once @expo/config exports getStaticConfig
export function readAppJson(appJsonPath: string): any {
const config = JsonFile.read(appJsonPath, { json5: true });
if (config) {
return config as any;
}
throw new Error(`Failed to read app.json`);
}
+24 -23
View File
@@ -1,4 +1,5 @@
import { Workflow } from '@expo/eas-build-job';
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import resolveFrom from 'resolve-from';
@@ -8,38 +9,40 @@ import Log, { learnMore } from '../../log';
import { appPlatformDisplayNames } from '../../platform';
import { resolveWorkflowAsync } from '../../project/workflow';
import { confirmAsync } from '../../prompts';
import { expoCommandAsync } from '../../utils/expoCli';
import { ProfileData } from '../../utils/profiles';
import { getVcsClient } from '../../vcs';
import { expoCommandAsync } from '../../utils/expoCommand';
import zipObject from '../../utils/expodash/zipObject';
import { reviewAndCommitChangesAsync } from './repository';
export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir,
platforms,
profile,
nonInteractive = false,
buildProfiles = [],
}: {
projectDir: string;
platforms: Platform[];
profile: string;
nonInteractive?: boolean;
buildProfiles?: ProfileData<'build'>[];
}): Promise<void> {
if (await isExpoDevClientInstalledAsync(projectDir)) {
return;
}
const buildProfilesWithDevelopmentClientRequired = buildProfiles.filter(
buildProfile => buildProfile.profile.developmentClient
const easJsonReader = new EasJsonReader(projectDir);
const devClientPerPlatformList = await Promise.all(
platforms.map(async platform => {
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, profile);
return buildProfile.developmentClient ?? false;
})
);
const isDevelopmentClientRequired = buildProfilesWithDevelopmentClientRequired.some(Boolean);
if (!isDevelopmentClientRequired) {
const isDevClientRequired = devClientPerPlatformList.some(i => i);
if (!isDevClientRequired) {
return;
}
const platformsToCheck = buildProfilesWithDevelopmentClientRequired.map(
({ platform }) => platform
);
const devClientPerPlatform = zipObject(platforms, devClientPerPlatformList);
const platformsToCheck = platforms.filter(platform => devClientPerPlatform[platform]);
const workflowPerPlatformList = await Promise.all(
platformsToCheck.map(platform => resolveWorkflowAsync(projectDir, platform))
);
@@ -64,15 +67,15 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
if (install) {
await installExpoDevClientAsync(projectDir, { nonInteractive });
} else {
error(`Install ${chalk.bold('expo-dev-client')} manually and come back later.`, {
error(`Install ${chalk.bold('expo-dev-client')} on your own and come back later.`, {
exit: 1,
});
}
} else {
Log.warn(`You'll need to install ${chalk.bold('expo-dev-client')} manually.`);
Log.warn(`Unfortunately, you need to install ${chalk.bold('expo-dev-client')} on your own.`);
Log.warn(
learnMore('https://docs.expo.dev/clients/installation/', {
learnMoreMessage: 'See installation instructions on how to do it.',
learnMoreMessage: 'See installation instructions on how to do it',
dim: false,
})
);
@@ -107,9 +110,7 @@ async function installExpoDevClientAsync(
Log.newLine();
await expoCommandAsync(projectDir, ['install', 'expo-dev-client']);
Log.newLine();
if (await getVcsClient().isCommitRequiredAsync()) {
await reviewAndCommitChangesAsync('Install expo-dev-client', {
nonInteractive,
});
}
await reviewAndCommitChangesAsync('Install expo-dev-client', {
nonInteractive,
});
}
@@ -120,5 +120,11 @@ export function formatGraphQLBuild(build: BuildFragment): string {
}
const getActorName = (build: BuildFragment): string => {
return build.initiatingActor?.displayName || 'unknown';
if (!build.initiatingActor) {
return 'unknown';
} else if (build.initiatingActor.__typename === 'User') {
return build.initiatingActor.username;
} else {
return build.initiatingActor.firstName ?? 'unknown';
}
};
+7 -11
View File
@@ -10,10 +10,10 @@ import { confirmAsync, promptAsync } from '../../prompts';
import { formatBytes } from '../../utils/files';
import { getTmpDirectory } from '../../utils/paths';
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
import { getVcsClient } from '../../vcs';
import vcs from '../../vcs';
export async function maybeBailOnRepoStatusAsync(): Promise<void> {
if (!(await getVcsClient().isCommitRequiredAsync())) {
if (!(await vcs.hasUncommittedChangesAsync())) {
return;
}
Log.addNewLineIfNone();
@@ -33,7 +33,7 @@ export async function maybeBailOnRepoStatusAsync(): Promise<void> {
}
export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<void> {
if (!(await getVcsClient().isCommitRequiredAsync())) {
if (!(await vcs.hasUncommittedChangesAsync())) {
return;
}
Log.addNewLineIfNone();
@@ -70,7 +70,7 @@ export async function commitPromptAsync({
initial: initialCommitMessage,
validate: (input: string) => input !== '',
});
await getVcsClient().commitAsync({ commitAllFiles, commitMessage: message });
await vcs.commitAsync({ commitAllFiles, commitMessage: message });
}
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
@@ -95,7 +95,7 @@ export async function makeProjectTarballAsync(): Promise<{ path: string; size: n
startTimer(compressTimerLabel);
try {
await getVcsClient().makeShallowCopyAsync(shallowClonePath);
await vcs.makeShallowCopyAsync(shallowClonePath);
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
'.',
]);
@@ -133,11 +133,7 @@ export async function reviewAndCommitChangesAsync(
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
): Promise<void> {
if (process.env.EAS_BUILD_AUTOCOMMIT) {
await getVcsClient().commitAsync({
commitMessage: initialCommitMessage,
commitAllFiles: false,
nonInteractive,
});
await vcs.commitAsync({ commitMessage: initialCommitMessage, commitAllFiles: false });
Log.withTick('Committed changes.');
return;
}
@@ -170,7 +166,7 @@ export async function reviewAndCommitChangesAsync(
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes.');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await getVcsClient().showDiffAsync();
await vcs.showDiffAsync();
await reviewAndCommitChangesAsync(initialCommitMessage, {
nonInteractive,
askedFirstTime: false,
+1 -1
View File
@@ -22,7 +22,7 @@ export function getArtifactUrl(artifactId: string): string {
export function getInternalDistributionInstallUrl(build: BuildFragment): string {
if (build.platform === AppPlatform.Ios) {
return `itms-services://?action=download-manifest;url=${getExpoApiBaseUrl()}/v2/projects/${
return `itms-services://?action=download-manifest;url=${getExpoApiBaseUrl()}/--/api/v2/projects/${
build.project.id
}/builds/${build.id}/manifest.plist`;
}
+7 -7
View File
@@ -3,10 +3,10 @@ import fs from 'fs-extra';
import path from 'path';
import Log, { learnMore } from '../log';
import { getVcsClient } from '../vcs';
import { CommonContext } from './context';
import vcs from '../vcs';
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;
@@ -25,12 +25,12 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
if (!googleServicesFilePath) {
return;
}
const rootDir = path.normalize(await getVcsClient().getRootPathAsync());
const rootDir = path.normalize(await vcs.getRootPathAsync());
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
if (
(await fs.pathExists(absGoogleServicesFilePath)) &&
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
(await getVcsClient().isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
(await vcs.isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
) {
Log.warn(
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
@@ -41,7 +41,7 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
)}`
);
Log.warn(
`If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting your build profile's env.GOOGLE_SERVICES_FILE in eas.json to any non-empty string.`
'If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting GOOGLE_SERVICES_FILE in your build profile in eas.json to any non-falsy value.'
);
Log.newLine();
}
@@ -1,19 +1,13 @@
import { EasJsonReader } from '@expo/eas-json';
import { Command } from '@oclif/command';
import semver from 'semver';
import {
AnalyticsEvent,
flushAsync as flushAnalyticsAsync,
initAsync as initAnalyticsAsync,
logEvent,
} from '../analytics/rudderstackClient';
import { findProjectRootAsync } from '../project/projectUtils';
} from '../analytics';
import { getUserAsync } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { setVcsClient } from '../vcs';
import GitClient from '../vcs/clients/git';
export default abstract class EasCommand extends Command {
/**
@@ -27,7 +21,6 @@ export default abstract class EasCommand extends Command {
// eslint-disable-next-line async-protect/async-suffix
async run(): Promise<any> {
await initAnalyticsAsync();
await this.applyCliConfigAsync();
if (this.requiresAuthentication) {
const { flags } = this.parse();
@@ -49,18 +42,4 @@ export default abstract class EasCommand extends Command {
await flushAnalyticsAsync();
return super.finally(err);
}
private async applyCliConfigAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const easJsonReader = new EasJsonReader(projectDir);
const config = await easJsonReader.getCliConfigAsync();
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
throw new Error(
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
);
}
if (config?.requireCommit) {
setVcsClient(new GitClient());
}
}
}
@@ -1,5 +1,5 @@
import { asMock } from '../../__tests__/utils';
import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClient';
import { flushAsync, initAsync, logEvent } from '../../analytics';
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
import { getUserAsync } from '../../user/User';
import { ensureLoggedInAsync } from '../../user/actions';
@@ -10,8 +10,8 @@ TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
jest.mock('../../user/User');
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
jest.mock('../../analytics/rudderstackClient', () => {
const { AnalyticsEvent } = jest.requireActual('../../analytics/rudderstackClient');
jest.mock('../../analytics', () => {
const { AnalyticsEvent } = jest.requireActual('../../analytics');
return {
AnalyticsEvent,
logEvent: jest.fn(),
@@ -17,7 +17,7 @@ import {
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { getVcsClient } from '../../vcs';
import vcs from '../../vcs';
export async function createUpdateBranchOnAppAsync({
appId,
@@ -90,8 +90,7 @@ export default class BranchCreate extends EasCommand {
name: 'name',
message: 'Please name the branch:',
initial:
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substr(2, 4)}`,
(await vcs.getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`,
validate: value => (value ? true : validationMessage),
}));
}
@@ -43,8 +43,7 @@ async function getBranchInfoAsync({
{
appId,
name,
},
{ additionalTypenames: ['UpdateBranch'] }
}
)
.toPromise()
);
+1 -2
View File
@@ -44,8 +44,7 @@ export async function listBranchesAsync({
{
appId: projectId,
limit: BRANCHES_LIMIT,
},
{ additionalTypenames: ['UpdateBranch'] }
}
)
.toPromise()
);
+464 -2
View File
@@ -1,10 +1,472 @@
import { ExpoConfig, getConfig, getDefaultTarget } from '@expo/config';
import { getRuntimeVersionForSDKVersion } from '@expo/sdk-runtime-versions';
import { flags } from '@oclif/command';
import assert from 'assert';
import chalk from 'chalk';
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 { promptAsync, selectAsync } from '../../prompts';
import { formatUpdate } from '../../update/utils';
import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
import vcs 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,
}
)
.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 = getRuntimeVersionObject(exp, platformFlag, projectDir);
const projectId = await getProjectIdAsync(exp);
if (!branchName && autoFlag) {
branchName =
(await vcs.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 vcs.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 vcs.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();
}
}
}
}
function getRuntimeVersionObject(
exp: ExpoConfig,
platformFlag: string,
projectDir: string
): Record<string, string> {
let { runtimeVersion: defaultRuntimeVersion, sdkVersion } = exp;
// When a SDK version is supplied instead of a runtime version and we're in the managed workflow
// construct the runtimeVersion with special meaning indicating that the runtime is an
// Expo SDK preset runtime that can be launched in Expo Go.
const isManagedProject = getDefaultTarget(projectDir) === 'managed';
if (!defaultRuntimeVersion && sdkVersion && isManagedProject) {
Log.withTick('Generating runtime version from sdk version');
defaultRuntimeVersion = getRuntimeVersionForSDKVersion(sdkVersion);
}
const iOSRuntimeVersion = (exp as any).ios?.runtimeVersion; // TODO-JJ remove cast to any
const androidRuntimeVersion = (exp as any).android?.runtimeVersion; // TODO-JJ remove cast to any
let runtimeVersions: Record<string, string>;
switch (platformFlag) {
case 'ios': {
runtimeVersions = {
ios: iOSRuntimeVersion ?? defaultRuntimeVersion,
};
break;
}
case 'android': {
runtimeVersions = {
android: androidRuntimeVersion ?? defaultRuntimeVersion,
};
break;
}
case 'all': {
runtimeVersions = {
ios: iOSRuntimeVersion ?? defaultRuntimeVersion,
android: androidRuntimeVersion ?? defaultRuntimeVersion,
};
break;
}
default:
throw new Error('Platform flag must be "ios", "android", or "all"');
}
if (Object.values(runtimeVersions).some(runtime => !runtime)) {
throw new Error(
"Couldn't find a 'runtimeVersion' for every platform. Please specify it under the 'expo' key in 'app.json'"
);
}
if (Object.values(runtimeVersions).some(runtime => typeof runtime !== 'string')) {
throw new Error(
`Please ensure that all of the runtime versions defined in the app.json are strings.`
);
}
return runtimeVersions;
}
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}`;
}
+1 -2
View File
@@ -58,8 +58,7 @@ export async function viewUpdateBranchAsync({
appId,
name,
limit: PAGE_LIMIT,
},
{ additionalTypenames: ['UpdateBranch', 'Update'] }
}
)
.toPromise()
);
@@ -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.
+89 -130
View File
@@ -1,22 +1,25 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, EasJsonReader, SubmitProfile } from '@expo/eas-json';
import {
EasJsonReader,
hasMismatchedExtendsAsync,
isUsingDeprecatedFormatAsync,
migrateAsync,
} from '@expo/eas-json';
import { flags } from '@oclif/command';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import figures from 'figures';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { prepareAndroidBuildAsync } from '../../build/android/build';
import { BuildRequestSender, waitForBuildEndAsync } from '../../build/build';
import { ensureProjectConfiguredAsync } from '../../build/configure';
import { BuildContext } from '../../build/context';
import { createBuildContextAsync } from '../../build/createContext';
import { BuildContext, createBuildContextAsync } from '../../build/context';
import { prepareIosBuildAsync } from '../../build/ios/build';
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from '../../build/utils/devClient';
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
import { ensureRepoIsCleanAsync, reviewAndCommitChangesAsync } from '../../build/utils/repository';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { CredentialsContext } from '../../credentials/context';
import {
AppPlatform,
BuildFragment,
@@ -24,7 +27,7 @@ import {
SubmissionFragment,
} from '../../graphql/generated';
import { toAppPlatform, toPlatform } from '../../graphql/types/AppPlatform';
import Log, { learnMore, link } from '../../log';
import Log from '../../log';
import {
RequestedPlatform,
appPlatformDisplayNames,
@@ -32,26 +35,27 @@ import {
selectRequestedPlatformAsync,
toPlatforms,
} from '../../platform';
import {
EAS_UNAVAILABLE_MESSAGE,
isEasEnabledForProjectAsync,
} from '../../project/isEasEnabledForProject';
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
import { findProjectRootAsync } from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
import { confirmAsync } from '../../prompts';
import { createSubmissionContextAsync } from '../../submit/context';
import {
submitAsync,
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../../submit/submit';
import { printSubmissionDetailsUrls } from '../../submit/utils/urls';
import { easCliVersion } from '../../utils/easCli';
import { enableJsonOutput } from '../../utils/json';
import { ProfileData, getProfilesAsync } from '../../utils/profiles';
import { getVcsClient, setVcsClient } from '../../vcs';
import GitClient from '../../vcs/clients/git';
import vcs from '../../vcs';
interface RawBuildFlags {
platform?: string;
'skip-credentials-check': boolean;
'skip-project-configuration': boolean;
profile?: string;
profile: string;
'non-interactive': boolean;
local: boolean;
wait: boolean;
@@ -64,7 +68,7 @@ interface RawBuildFlags {
interface BuildFlags {
requestedPlatform: RequestedPlatform;
skipProjectConfiguration: boolean;
profile?: string;
profile: string;
nonInteractive: boolean;
local: boolean;
wait: boolean;
@@ -75,7 +79,7 @@ interface BuildFlags {
}
export default class Build extends EasCommand {
static description = 'Start a build';
static description = 'start a build';
static flags = {
platform: flags.enum({
@@ -95,8 +99,8 @@ export default class Build extends EasCommand {
description: 'Skip project configuration',
}),
profile: flags.string({
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
default: 'release',
description: 'Name of the build profile from eas.json',
helpValue: 'PROFILE_NAME',
}),
'non-interactive': flags.boolean({
@@ -142,74 +146,50 @@ export default class Build extends EasCommand {
const projectDir = await findProjectRootAsync();
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await getVcsClient().ensureRepoExistsAsync();
await vcs.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
const platforms = toPlatforms(requestedPlatform);
const buildProfiles = await getProfilesAsync({
type: 'build',
projectDir,
platforms,
profileName: flags.profile ?? undefined,
});
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
platforms,
projectDir,
profile: flags.profile,
nonInteractive: flags.nonInteractive,
buildProfiles,
});
const startedBuilds: {
build: BuildFragment;
buildProfile: ProfileData<'build'>;
}[] = [];
const startedBuilds: BuildFragment[] = [];
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
for (const buildProfile of buildProfiles) {
for (const platform of platforms) {
const { build: maybeBuild, buildCtx } = await this.prepareAndStartBuildAsync({
projectDir,
platform,
flags,
moreBuilds: platforms.length > 1,
buildProfile,
});
if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile });
startedBuilds.push(maybeBuild);
}
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
buildCtxByPlatform[toAppPlatform(platform)] = buildCtx;
}
if (flags.local) {
return;
}
Log.newLine();
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
printLogsUrls(startedBuilds);
Log.newLine();
const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) {
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;
for (const build of startedBuilds) {
const submission = await this.prepareAndStartSubmissionAsync({
build: startedBuild.build,
buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]),
build,
credentialsCtx: nullthrows(buildCtxByPlatform[build.platform]?.credentialsCtx),
flags,
moreBuilds: startedBuilds.length > 1,
projectDir,
buildProfile: startedBuild.buildProfile.profile,
submitProfile,
nonInteractive: flags.nonInteractive,
});
submissions.push(submission);
}
@@ -223,7 +203,7 @@ export default class Build extends EasCommand {
return;
}
const builds = await waitForBuildEndAsync(startedBuilds.map(({ build }) => build.id));
const builds = await waitForBuildEndAsync(startedBuilds.map(build => build.id));
printBuildResults(builds, flags.json);
const haveAllBuildsFailedOrCanceled = builds.every(
@@ -278,36 +258,39 @@ export default class Build extends EasCommand {
wait: flags['wait'],
clearCache: flags['clear-cache'],
json: flags['json'],
autoSubmit: flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined,
autoSubmit: flags['auto-submit'],
submitProfile: flags['auto-submit-with-profile'] ?? profile,
};
}
private async prepareAndStartBuildAsync({
private async prepareAndStartBuildAsync<T extends Platform>({
projectDir,
platform,
flags,
moreBuilds,
buildProfile,
}: {
projectDir: string;
platform: T;
flags: BuildFlags;
moreBuilds: boolean;
buildProfile: ProfileData<'build'>;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<T> }> {
const easJsonReader = new EasJsonReader(projectDir);
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, flags.profile);
const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName,
buildProfileName: flags.profile,
clearCache: flags.clearCache,
buildProfile: buildProfile.profile,
buildProfile,
local: flags.local,
nonInteractive: flags.nonInteractive,
platform: buildProfile.platform,
platform,
projectDir,
skipProjectConfiguration: flags.skipProjectConfiguration,
});
if (moreBuilds) {
Log.newLine();
const appPlatform = toAppPlatform(buildProfile.platform);
const appPlatform = toAppPlatform(platform);
Log.log(
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
`${appPlatformDisplayNames[appPlatform]} build`
@@ -320,6 +303,10 @@ export default class Build extends EasCommand {
this.metroConfigValidated = true;
}
if (!buildCtx.local && !(await isEasEnabledForProjectAsync(buildCtx.projectId))) {
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
}
const build = await this.startBuildAsync(buildCtx);
return {
build,
@@ -339,32 +326,30 @@ export default class Build extends EasCommand {
private async prepareAndStartSubmissionAsync({
build,
buildCtx,
credentialsCtx,
flags,
moreBuilds,
projectDir,
buildProfile,
submitProfile,
nonInteractive,
}: {
build: BuildFragment;
buildCtx: BuildContext<Platform>;
credentialsCtx: CredentialsContext;
flags: BuildFlags;
moreBuilds: boolean;
projectDir: string;
buildProfile: BuildProfile;
submitProfile: SubmitProfile;
nonInteractive: boolean;
}): Promise<SubmissionFragment> {
const easJsonReader = new EasJsonReader(projectDir);
const platform = toPlatform(build.platform);
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, flags.profile);
const submitProfile = await easJsonReader.readSubmitProfileAsync(platform, flags.submitProfile);
const submissionCtx = await createSubmissionContextAsync({
platform,
projectDir,
projectId: build.project.id,
profile: submitProfile,
archiveFlags: { id: build.id },
nonInteractive,
nonInteractive: flags.nonInteractive,
env: buildProfile.env,
credentialsCtx: buildCtx.credentialsCtx,
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
credentialsCtx,
});
if (moreBuilds) {
@@ -393,62 +378,36 @@ export async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
if (!(await fs.pathExists(easJsonPath))) {
return;
}
const easJsonReader = new EasJsonReader(projectDir);
const rawEasJson = await easJsonReader.readAsync();
if (rawEasJson?.cli) {
return;
}
if (await isUsingDeprecatedFormatAsync(projectDir)) {
const hasMismatchedExtendsKeys = await hasMismatchedExtendsAsync(projectDir);
if (nonInteractive) {
Log.error('We detected that your eas.json is using a deprecated format.');
Log.error(
'We will convert it automatically if run this command without --non-interactive flag. Alternatively, you can update eas.json manually according to https://docs.expo.dev/build/eas-json'
);
error('Unsupported eas.json format', { exit: 1 });
}
if (nonInteractive) {
Log.warn(
`${
figures.warning
} Action required: the default behavior of EAS CLI has changed and your eas.json must be updated to remove ambiguity around which Git integration workflow to use. Refer to ${link(
'https://expo.fyi/eas-vcs-workflow'
)} for more information.`
);
Log.warn(
'This warning will become an error in an upcoming EAS CLI release. For now, we will proceed with the old default behavior to avoid disruption of your builds.'
);
setVcsClient(new GitClient());
return;
}
Log.log(
`${chalk.bold(
'eas-cli@>=0.34.0 no longer requires that you commit changes to Git before starting a build.'
)} ${learnMore('https://expo.fyi/eas-vcs-workflow')}`
);
Log.log(
`If you want to continue using the Git integration, you can opt in with ${chalk.bold(
'cli.requireCommit'
)} in ${chalk.bold('eas.json')} or with the following prompt.`
);
Log.newLine();
const mode = await selectAsync('Select your preferred Git integration', [
{ title: 'Require changes to be committed in Git (old default)', value: 'requireCommit' },
{ title: 'Allow builds with dirty Git working tree (new default)', value: 'noCommit' },
]);
if (mode === 'requireCommit') {
setVcsClient(new GitClient());
await ensureRepoIsCleanAsync(nonInteractive);
}
rawEasJson.cli =
mode === 'requireCommit'
? { version: `>= ${easCliVersion}`, requireCommit: true }
: { version: `>= ${easCliVersion}` };
await fs.writeJSON(easJsonPath, rawEasJson, { spaces: 2 });
Log.withTick('Updated eas.json');
if (mode === 'requireCommit') {
await getVcsClient().trackFileAsync(easJsonPath);
await reviewAndCommitChangesAsync('Set cli.requireCommit to true in eas.json', {
nonInteractive,
const confirm = await confirmAsync({
message:
'We detected that your eas.json is using a deprecated format, do you want to migrate to the new format?',
});
if (confirm) {
await migrateAsync(projectDir);
if (hasMismatchedExtendsKeys) {
Log.warn(
'"extends" keyword can only be migrated automatically to the new format if both Android and iOS profiles extend base profiles with the same names for both platforms'
);
Log.warn(
'Migration was successful, but you need to manually adjust the extend rules for your profiles'
);
error('Fix eas.json manually', { exit: 1 });
}
} else {
Log.error(
"Aborting, update your eas.json according to https://docs.expo.dev/build/eas-json and run 'eas build' again"
);
error('Unsupported eas.json format', { exit: 1 });
}
}
}
@@ -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,
@@ -49,8 +49,7 @@ async function getChannelByNameForAppAsync({
}
}
`,
{ appId, channelName },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch'] }
{ appId, channelName }
)
.toPromise()
);
@@ -89,7 +88,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!;
}
@@ -58,8 +58,7 @@ async function getAllUpdateChannelForAppAsync({
}
}
`,
{ appId, offset: 0, limit: CHANNEL_LIMIT },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
{ appId, offset: 0, limit: CHANNEL_LIMIT }
)
.toPromise()
);
@@ -133,8 +133,7 @@ export async function getUpdateChannelByNameForAppAsync({
}
}
`,
{ appId, channelName },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
{ appId, channelName }
)
.toPromise()
);
@@ -181,7 +180,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 +238,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 +255,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);
}
}
+4 -23
View File
@@ -1,11 +1,9 @@
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';
const packageJSON = require('../../package.json');
export default class Diagnostics extends EasCommand {
static description = 'log environment info to the console';
@@ -32,26 +30,9 @@ export default class Diagnostics extends EasCommand {
npmGlobalPackages: ['eas-cli', 'expo-cli'],
},
{
title: `EAS CLI ${easCliVersion} environment info`,
title: `EAS CLI ${packageJSON.version} environment info`,
}
);
Log.log(info.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);
}
}
@@ -22,8 +22,7 @@ async function projectInfoByIdAsync(appId: string): Promise<AppInfoQuery> {
}
}
`,
{ appId },
{ additionalTypenames: ['App'] }
{ appId }
)
.toPromise()
);
@@ -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.';
+11 -1
View File
@@ -10,6 +10,10 @@ import {
EnvironmentSecretsQuery,
} from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log';
import {
isEasEnabledForProjectAsync,
warnEasUnavailable,
} from '../../project/isEasEnabledForProject';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
@@ -19,7 +23,7 @@ import { promptAsync, toggleConfirmAsync } from '../../prompts';
export default class EnvironmentSecretDelete extends EasCommand {
static description = `Delete an environment secret by ID.
Unsure where to find the secret's ID? Run ${chalk.bold('eas secret:list')}`;
Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
static flags = {
id: flags.string({
@@ -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");
}
+17 -15
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,11 +15,11 @@ 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';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { getProfilesAsync } from '../utils/profiles';
interface RawFlags {
platform?: string;
@@ -42,7 +43,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'];
@@ -53,7 +54,7 @@ See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit
}),
profile: flags.string({
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
'Name of the submit profile from eas.json. Defaults to "release" if defined in eas.json.',
}),
latest: flags.boolean({
description: 'Submit the latest build for specified platform',
@@ -94,28 +95,29 @@ 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);
const platforms = toPlatforms(flags.requestedPlatform);
const submissionProfiles = await getProfilesAsync({
type: 'submit',
projectDir,
platforms,
profileName: flags.profile,
});
if (!(await isEasEnabledForProjectAsync(projectId))) {
warnEasUnavailable();
process.exitCode = 1;
return;
}
const easJsonReader = new EasJsonReader(projectDir);
const platforms = toPlatforms(flags.requestedPlatform);
const submissions: SubmissionFragment[] = [];
for (const submissionProfile of submissionProfiles) {
for (const platform of platforms) {
const profile = await easJsonReader.readSubmitProfileAsync(platform, flags.profile);
const ctx = await createSubmissionContextAsync({
platform: submissionProfile.platform,
platform,
projectDir,
projectId,
profile: submissionProfile.profile,
profile,
archiveFlags: flags.archiveFlags,
nonInteractive: flags.nonInteractive,
});
if (submissionProfiles.length > 1) {
if (platforms.length > 1) {
Log.newLine();
const appPlatform = toAppPlatform(submissionProfile.platform);
const appPlatform = toAppPlatform(platform);
Log.log(
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
`${appPlatformDisplayNames[appPlatform]} submission`
@@ -1,108 +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_RUNTIME_VERSION = { policy: 'sdkVersion' } as const;
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
): Promise<void> {
const easUpdateURL = await getEASUpdateURLAsync(exp);
const preexistingRuntimeVersion = exp.runtimeVersion;
const result = await modifyConfigAsync(projectDir, {
runtimeVersion: preexistingRuntimeVersion ?? DEFAULT_RUNTIME_VERSION,
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(DEFAULT_RUNTIME_VERSION)}" 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,
});
await configureProjectForEASUpdateAsync(projectDir, exp);
const hasAndroidNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
const hasIosNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.IOS)) === Workflow.GENERIC;
Log.addNewLineIfNone();
if (hasAndroidNativeProject || hasIosNativeProject) {
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,483 +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 });
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 {
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: republish
? `Republish "${oldMessage!}" - group: ${group}`
: (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.`
);
}
}
+1 -2
View File
@@ -39,8 +39,7 @@ export async function viewUpdateAsync({
`,
{
groupId,
},
{ additionalTypenames: ['Update'] }
}
)
.toPromise()
);
@@ -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',
@@ -4,7 +4,7 @@ import { AuthCtx } from '../ios/appstore/authenticate';
export const testAuthCtx: AuthCtx = {
appleId: 'test-apple-id',
appleIdPassword: 'test-apple-password',
team: { id: 'test-apple-team-identifier', name: 'test-team-name', inHouse: false },
team: { id: 'test-team-id', name: 'test-team-name', inHouse: false },
};
export function getAppstoreMock(): AppStoreApi {
@@ -22,9 +22,5 @@ export function getAppstoreMock(): AppStoreApi {
createProvisioningProfileAsync: jest.fn(),
revokeProvisioningProfileAsync: jest.fn(),
createOrReuseAdhocProvisioningProfileAsync: jest.fn(),
createAscApiKeyAsync: jest.fn(),
getAscApiKeyAsync: jest.fn(),
revokeAscApiKeyAsync: jest.fn(),
listAscApiKeysAsync: jest.fn(),
} as any;
}
@@ -1,9 +1,5 @@
import { UserRole } from '@expo/apple-utils';
import {
AppFragment,
AppStoreConnectApiKeyFragment,
AppStoreConnectUserRole,
AppleAppIdentifierFragment,
AppleDistributionCertificateFragment,
AppleProvisioningProfileFragment,
@@ -14,27 +10,12 @@ import {
IosDistributionType,
} from '../../graphql/generated';
import * as IosGraphqlClient from '../ios/api/GraphqlClient';
import {
AscApiKey,
DistributionCertificate,
ProvisioningProfile,
} from '../ios/appstore/Credentials.types';
import { DistributionCertificate, ProvisioningProfile } from '../ios/appstore/Credentials.types';
import { Target } from '../ios/types';
import { testProvisioningProfileBase64 } from './fixtures-base64-data';
const now = new Date();
export const testAscApiKey: AscApiKey = {
keyId: 'test-keyIdentifier',
issuerId: 'test-issuer-id-from-apple',
teamId: 'test-team-id',
teamName: 'test-team-name',
name: 'test-name',
roles: [UserRole.ADMIN],
isRevoked: false,
keyP8: 'test-key-p8',
};
export const testProvisioningProfile: ProvisioningProfile = {
provisioningProfileId: 'test-id',
provisioningProfile: testProvisioningProfileBase64,
@@ -57,17 +38,6 @@ export const testAppFragment: AppFragment = {
slug: 'testapp',
};
export const testAscApiKeyFragment: AppStoreConnectApiKeyFragment = {
id: 'test-asc-api-key-id',
appleTeam: { ...testAppleTeamFragment },
issuerIdentifier: 'test-issuerIdentifier',
keyIdentifier: 'test-keyIdentifier',
name: 'test-name',
roles: [AppStoreConnectUserRole.Admin],
updatedAt: now,
createdAt: now,
};
export const testPushKey: ApplePushKeyFragment = {
id: 'test-push-key-id',
keyIdentifier: 'test-key-identifier',
@@ -142,7 +112,6 @@ export const testCommonIosAppCredentialsFragment: CommonIosAppCredentialsFragmen
appleAppIdentifier: testAppleAppIdentifierFragment,
pushKey: testPushKey,
iosAppBuildCredentialsList: [testIosAppBuildCredentialsFragment],
appStoreConnectApiKeyForSubmissions: testAscApiKeyFragment,
};
export const testAppleTeam = {
@@ -170,9 +139,6 @@ export function getNewIosApiMock(): { [key in keyof typeof IosGraphqlClient]?: a
createPushKeyAsync: jest.fn(),
getPushKeyForAppAsync: jest.fn(),
deletePushKeyAsync: jest.fn(),
createAscApiKeyAsync: jest.fn(),
deleteAscApiKeyAsync: jest.fn(),
getAscApiKeysForAccountAsync: jest.fn(() => []),
};
}
@@ -5,7 +5,7 @@ import nullthrows from 'nullthrows';
import { AndroidAppBuildCredentialsFragment } from '../../graphql/generated';
import { CredentialsContext } from '../context';
import * as credentialsJsonReader from '../credentialsJson/read';
import { SetUpBuildCredentials } from './actions/SetUpBuildCredentials';
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
import { AppLookupParams } from './api/GraphqlClient';
import { Keystore } from './credentials';
@@ -34,7 +34,7 @@ export default class AndroidCredentialsProvider {
}
private async getRemoteAsync(): Promise<AndroidCredentials> {
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: this.options.app });
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: this.options.app });
const buildCredentials = await setupBuildCredentialsAction.runAsync(this.ctx);
return this.toAndroidCredentials(buildCredentials);
}
@@ -9,7 +9,7 @@ export class RemoveFcm {
async runAsync(ctx: CredentialsContext): Promise<void> {
if (ctx.nonInteractive) {
throw new Error(
"Deleting an FCM API Key is a destructive operation. Start the CLI without the '--non-interactive' flag to delete the credentials."
"Deleting an FCM Api Key is a destructive operation. Start the CLI without the '--non-interactive' flag to delete the credentials."
);
}
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(
@@ -18,7 +18,7 @@ export class RemoveFcm {
const fcm = appCredentials?.androidFcm;
if (!fcm) {
Log.warn(
`There is no valid FCM API Key defined for ${formatProjectFullName(this.app)}, ${
`There is no valid FCM Api Key defined for ${formatProjectFullName(this.app)}, ${
this.app.androidApplicationIdentifier
}`
);
@@ -26,7 +26,7 @@ export class RemoveFcm {
}
const confirm = await confirmAsync({
message: 'Permanently delete the FCM API Key from Expo servers?',
message: 'Permanently delete the FCM Api Key from Expo servers?',
initial: false,
});
if (!confirm) {
@@ -34,6 +34,6 @@ export class RemoveFcm {
}
await ctx.android.deleteFcmAsync(fcm);
Log.succeed('FCM API Key removed');
Log.succeed('FCM Api Key removed');
}
}
@@ -25,7 +25,7 @@ interface Options {
* Sets up Build Credentials for Android
* @name: sets up build credentials for the specified configuration. If no name is specified, the default configuration is setup
*/
export class SetUpBuildCredentials {
export class SetupBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: CredentialsContext): Promise<AndroidAppBuildCredentialsFragment> {
@@ -14,7 +14,7 @@ import { AppLookupParams } from '../api/GraphqlClient';
import { getKeystoreWithType } from '../utils/keystoreNew';
import { BackupKeystore } from './DownloadKeystore';
export class SetUpBuildCredentialsFromCredentialsJson {
export class SetupBuildCredentialsFromCredentialsJson {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: CredentialsContext): Promise<AndroidAppBuildCredentialsFragment | null> {
@@ -13,7 +13,7 @@ import { AssignGoogleServiceAccountKey } from './AssignGoogleServiceAccountKey';
import { CreateGoogleServiceAccountKey } from './CreateGoogleServiceAccountKey';
import { UseExistingGoogleServiceAccountKey } from './UseExistingGoogleServiceAccountKey';
export class SetUpGoogleServiceAccountKey {
export class SetupGoogleServiceAccountKey {
constructor(private app: AppLookupParams) {}
public async runAsync(ctx: CredentialsContext): Promise<CommonAndroidAppCredentialsFragment> {
@@ -12,7 +12,7 @@ jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(RemoveFcm, () => {
it('removes an FCM API Key', async () => {
it('removes an FCM Api Key', async () => {
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -9,7 +9,7 @@ import { createCtxMock } from '../../../__tests__/fixtures-context';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { generateRandomKeystoreAsync } from '../../utils/keystore';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetUpBuildCredentials } from '../SetUpBuildCredentials';
import { SetupBuildCredentials } from '../SetupBuildCredentials';
jest.mock('../../../../ora');
jest.mock('../../../../project/ensureProjectExists');
@@ -17,7 +17,7 @@ jest.mock('../../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() =
jest.mock('../../../../prompts', () => ({ confirmAsync: jest.fn(() => true) }));
jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn() }));
describe('SetUpBuildCredentials', () => {
describe('SetupBuildCredentials', () => {
beforeEach(() => {
asMock(generateRandomKeystoreAsync).mockReset();
});
@@ -33,7 +33,7 @@ describe('SetUpBuildCredentials', () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
await setupBuildCredentialsAction.runAsync(ctx);
// expect keystore not to be created
@@ -48,7 +48,7 @@ describe('SetUpBuildCredentials', () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
await setupBuildCredentialsAction.runAsync(ctx);
// expect keystore to be created
@@ -63,7 +63,7 @@ describe('SetUpBuildCredentials', () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
);
@@ -14,7 +14,7 @@ import {
SelectAndroidBuildCredentialsResultType,
} from '../../../manager/SelectAndroidBuildCredentials';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetUpBuildCredentialsFromCredentialsJson } from '../SetUpBuildCredentialsFromCredentialsJson';
import { SetupBuildCredentialsFromCredentialsJson } from '../SetupBuildCredentialsFromCredentialsJson';
jest.mock('fs');
@@ -26,7 +26,7 @@ beforeEach(() => {
vol.reset();
});
describe(SetUpBuildCredentialsFromCredentialsJson, () => {
describe(SetupBuildCredentialsFromCredentialsJson, () => {
it('sets up a new build configuration from credentials.json upon user request', async () => {
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
return {
@@ -59,7 +59,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
'keystore.jks': 'some-binary-content',
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
appLookupParams
);
await setupBuildCredentialsAction.runAsync(ctx);
@@ -109,7 +109,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
'keystore.jks': 'some-binary-content',
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
appLookupParams
);
await setupBuildCredentialsAction.runAsync(ctx);
@@ -132,7 +132,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
nonInteractive: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
appLookupParams
);
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError();
@@ -10,7 +10,7 @@ import {
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetUpGoogleServiceAccountKey } from '../SetUpGoogleServiceAccountKey';
import { SetupGoogleServiceAccountKey } from '../SetupGoogleServiceAccountKey';
jest.mock('../../../../prompts');
jest.mock('fs');
@@ -22,7 +22,7 @@ beforeEach(() => {
vol.reset();
});
describe(SetUpGoogleServiceAccountKey, () => {
describe(SetupGoogleServiceAccountKey, () => {
it('skips setup when there is a Google Service Account Key already assigned to the project', async () => {
const ctx = createCtxMock({
nonInteractive: false,
@@ -34,7 +34,7 @@ describe(SetUpGoogleServiceAccountKey, () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
await setupGoogleServiceAccountKeyAction.runAsync(ctx);
expect(ctx.android.createGoogleServiceAccountKeyAsync).not.toHaveBeenCalled();
@@ -57,7 +57,7 @@ describe(SetUpGoogleServiceAccountKey, () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
await setupGoogleServiceAccountKeyAction.runAsync(ctx);
expect(ctx.android.createGoogleServiceAccountKeyAsync).toHaveBeenCalledTimes(1);
@@ -68,7 +68,7 @@ describe(SetUpGoogleServiceAccountKey, () => {
nonInteractive: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
await expect(setupGoogleServiceAccountKeyAction.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
);
@@ -29,9 +29,6 @@ export const GoogleServiceAccountKeyQuery = {
`,
{
accountName,
},
{
additionalTypenames: ['GoogleServiceAccountKey'],
}
)
.toPromise()
@@ -1,35 +1,31 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import crypto from 'crypto';
import fs from 'fs-extra';
// (dsokal) We actually want to use node-fetch but the change is in progress.
// See https://github.com/expo/eas-cli/issues/32 for context.
// eslint-disable-next-line no-restricted-imports
import fetch from 'node-fetch';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { Analytics, BuildEvent } from '../../../analytics/events';
import Analytics, { Event } from '../../../build/utils/analytics';
import { AndroidKeystoreType } from '../../../graphql/generated';
import { KeystoreGenerationUrlMutation } from '../../../graphql/mutations/KeystoreGenerationUrlMutation';
import Log from '../../../log';
import { ora } from '../../../ora';
import { getTmpDirectory } from '../../../utils/paths';
import { KeystoreWithType } from '../credentials';
interface KeystoreParams {
keystorePassword: string;
keyAlias: string;
keyPassword: string;
}
export async function keytoolCommandExistsAsync(): Promise<boolean> {
try {
await spawnAsync('keytool');
return true;
} catch (error) {
return false;
}
return true;
}
async function ensureKeytoolCommandExistsAsync(): Promise<void> {
if (!(await keytoolCommandExistsAsync())) {
Log.error('keytool is required to run this command, make sure you have it installed?');
Log.warn('keytool is a part of OpenJDK: https://openjdk.java.net/');
Log.warn('Also make sure that keytool is in your PATH after installation.');
throw new Error('keytool not found');
}
}
enum KeystoreCreateStep {
@@ -39,7 +35,7 @@ enum KeystoreCreateStep {
}
export async function generateRandomKeystoreAsync(projectId: string): Promise<KeystoreWithType> {
const keystoreData: KeystoreParams = {
const keystoreData = {
keystorePassword: crypto.randomBytes(16).toString('hex'),
keyPassword: crypto.randomBytes(16).toString('hex'),
keyAlias: crypto.randomBytes(16).toString('hex'),
@@ -48,38 +44,23 @@ export async function generateRandomKeystoreAsync(projectId: string): Promise<Ke
}
async function createKeystoreAsync(
keystoreParams: KeystoreParams,
credentials: {
keystorePassword: string;
keyAlias: string;
keyPassword: string;
},
projectId: string
): Promise<KeystoreWithType> {
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Attempt,
type: AndroidKeystoreType.Jks,
});
try {
let keystore: KeystoreWithType | undefined;
let localAttempt = false;
if (await keytoolCommandExistsAsync()) {
try {
localAttempt = true;
keystore = await createKeystoreLocallyAsync(keystoreParams);
} catch {
Log.error('Failed to generate keystore locally. Falling back to cloud generation.');
}
}
if (!keystore) {
keystore = await createKeystoreInCloudAsync(keystoreParams, {
showKeytoolDetectionMsg: !localAttempt,
});
}
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Success,
type: AndroidKeystoreType.Jks,
});
return keystore;
await ensureKeytoolCommandExistsAsync();
} catch (error: any) {
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Fail,
reason: error.message,
@@ -87,11 +68,7 @@ async function createKeystoreAsync(
});
throw error;
}
}
async function createKeystoreLocallyAsync(
keystoreParams: KeystoreParams
): Promise<KeystoreWithType> {
await fs.mkdirp(getTmpDirectory());
const keystorePath = path.join(getTmpDirectory(), `${uuidv4()}-keystore.jks`);
try {
@@ -101,13 +78,13 @@ async function createKeystoreLocallyAsync(
'-storetype',
'JKS',
'-storepass',
keystoreParams.keystorePassword,
credentials.keystorePassword,
'-keypass',
keystoreParams.keyPassword,
credentials.keyPassword,
'-keystore',
keystorePath,
'-alias',
keystoreParams.keyAlias,
credentials.keyAlias,
'-keyalg',
'RSA',
'-keysize',
@@ -117,49 +94,27 @@ async function createKeystoreLocallyAsync(
'-dname',
`CN=,OU=,O=,L=,S=,C=US`,
]);
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Success,
type: AndroidKeystoreType.Jks,
});
return {
...keystoreParams,
...credentials,
keystore: await fs.readFile(keystorePath, 'base64'),
type: AndroidKeystoreType.Jks,
};
} catch (error: any) {
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Fail,
reason: error.message,
type: AndroidKeystoreType.Jks,
});
throw error;
} finally {
await fs.remove(keystorePath);
}
}
interface KeystoreServiceResult {
keystoreBase64: string;
keystorePassword: string;
keyPassword: string;
keyAlias: string;
}
async function createKeystoreInCloudAsync(
keystoreParams: KeystoreParams,
{ showKeytoolDetectionMsg = true } = {}
): Promise<KeystoreWithType> {
if (showKeytoolDetectionMsg) {
Log.log(`Detected that you do not have ${chalk.bold('keytool')} installed locally.`);
}
const spinner = ora('Generating keystore in the cloud...').start();
try {
const url = await KeystoreGenerationUrlMutation.createKeystoreGenerationUrlAsync();
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(keystoreParams),
headers: { 'Content-Type': 'application/json' },
});
const result: KeystoreServiceResult = await response.json();
spinner.succeed();
return {
type: AndroidKeystoreType.Jks,
keystore: result.keystoreBase64,
keystorePassword: result.keystorePassword,
keyAlias: result.keyAlias,
keyPassword: result.keyPassword,
};
} catch (err: any) {
spinner.fail();
throw err;
}
}
+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
}
}
}
@@ -6,8 +6,8 @@ import { AndroidAppBuildCredentialsFragment, IosDistributionType } from '../../g
import Log from '../../log';
import { findApplicationTarget, findTargetByName } from '../../project/ios/target';
import zipObject from '../../utils/expodash/zipObject';
import { getVcsClient } from '../../vcs';
import GitClient from '../../vcs/clients/git';
import vcs from '../../vcs';
import GitClient from '../../vcs/git';
import { CredentialsContext } from '../context';
import { App, Target, TargetCredentials } from '../ios/types';
import { readRawAsync } from './read';
@@ -294,9 +294,8 @@ async function updateFileAsync(
}
async function isFileUntrackedAsync(path: string): Promise<boolean> {
const vcsClient = getVcsClient();
if (vcsClient instanceof GitClient) {
return await vcsClient.isFileUntrackedAsync(path);
if (vcs instanceof GitClient) {
return await vcs.isFileUntrackedAsync(path);
}
return false;
}
@@ -5,15 +5,3 @@ export class MissingCredentialsNonInteractiveError extends Error {
);
}
}
export class MissingCredentialsError extends Error {
constructor(message?: string) {
super(message ?? 'Credentials are not set up.');
}
}
export class UnsupportedCredentialsChoiceError extends Error {
constructor(message: string, public choice: string) {
super(message);
}
}
@@ -9,8 +9,8 @@ import { CredentialsContext } from '../context';
import * as credentialsJsonReader from '../credentialsJson/read';
import { ensureAllTargetsAreConfigured } from '../credentialsJson/utils';
import { getAppFromContext } from './actions/BuildCredentialsUtils';
import { SetUpBuildCredentials } from './actions/SetUpBuildCredentials';
import { SetUpPushKey } from './actions/SetUpPushKey';
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
import { SetupPushKey } from './actions/SetupPushKey';
import { IosCapabilitiesOptions } from './appstore/ensureAppExists';
import { App, IosCredentials, Target } from './types';
import { isAdHocProfile } from './utils/provisioningProfile';
@@ -58,7 +58,7 @@ export default class IosCredentialsProvider {
}
private async getRemoteAsync(): Promise<IosCredentials> {
return await new SetUpBuildCredentials({
return await new SetupBuildCredentials({
app: this.options.app,
targets: this.options.targets,
distribution: this.options.distribution,
@@ -83,7 +83,7 @@ export default class IosCredentialsProvider {
parentBundleIdentifier: applicationTarget.parentBundleIdentifier,
};
const setupPushKeyAction = new SetUpPushKey(appLookupParams);
const setupPushKeyAction = new SetupPushKey(appLookupParams);
const isPushKeySetup = await setupPushKeyAction.isPushKeySetupAsync(ctx);
if (isPushKeySetup) {
Log.succeed(
@@ -93,7 +93,7 @@ export default class IosCredentialsProvider {
}
const confirmSetup = await confirmAsync({
message: `Would you like to set up Push Notifications for your project?`,
message: `Would you like to setup Push Notifications for your project?`,
});
if (!confirmSetup) {
return null;
@@ -1,256 +0,0 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import { nanoid } from 'nanoid';
import path from 'path';
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
import Log, { learnMore } from '../../../log';
import { confirmAsync, promptAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { fromNow } from '../../../utils/date';
import { CredentialsContext } from '../../context';
import {
getCredentialsFromUserAsync,
shouldAutoGenerateCredentialsAsync,
} from '../../utils/promptForCredentials';
import { AscApiKey } from '../appstore/Credentials.types';
import {
AscApiKeyPath,
MinimalAscApiKey,
ascApiKeyIdSchema,
ascApiKeyIssuerIdSchema,
} from '../credentials';
import { isAscApiKeyValidAndTrackedAsync } from '../validators/validateAscApiKey';
import { formatAppleTeam } from './AppleTeamUtils';
export enum AppStoreApiKeyPurpose {
SUBMISSION_SERVICE = 'EAS Submit',
}
export async function promptForAscApiKeyPathAsync(ctx: CredentialsContext): Promise<AscApiKeyPath> {
const { keyId, keyP8Path } = await promptForKeyP8AndIdAsync();
const bestEffortIssuerId = await getBestEffortIssuerIdAsync(ctx, keyId);
if (bestEffortIssuerId) {
Log.log(`Detected Issuer ID: ${bestEffortIssuerId}`);
return { keyId, issuerId: bestEffortIssuerId, keyP8Path };
}
const issuerId = await promptForIssuerIdAsync();
return { keyId, issuerId, keyP8Path };
}
export async function promptForIssuerIdAsync(): Promise<string> {
Log.log(chalk.bold('An App Store Connect Issuer ID is required'));
Log.log(
`If you're not sure what this is or how to find yours, ${learnMore(
'https://expo.fyi/asc-issuer-id'
)}`
);
// Do not perform uuid validation - Apple's issuerIds are not RFC4122 compliant
const { issuerId } = await getCredentialsFromUserAsync(ascApiKeyIssuerIdSchema);
return issuerId;
}
export async function getMinimalAscApiKeyAsync(ascApiKey: AscApiKey): Promise<MinimalAscApiKey> {
return {
...ascApiKey,
issuerId: ascApiKey.issuerId ?? (await promptForIssuerIdAsync()),
};
}
export async function provideOrGenerateAscApiKeyAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<MinimalAscApiKey> {
if (ctx.nonInteractive) {
throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
}
const userProvided = await promptForAscApiKeyAsync(ctx);
if (!userProvided) {
return await generateAscApiKeyAsync(ctx, purpose);
}
if (!ctx.appStore.authCtx) {
Log.warn('Unable to validate App Store Connect API Key, you are not authenticated with Apple.');
return userProvided;
}
const isValidAndTracked = await isAscApiKeyValidAndTrackedAsync(ctx, userProvided);
if (isValidAndTracked) {
return userProvided;
}
const useUserProvided = await confirmAsync({
message: `App Store Connect API Key with ID ${userProvided.keyId} is not valid on Apple's servers. Proceed anyway?`,
});
if (useUserProvided) {
return userProvided;
}
return await provideOrGenerateAscApiKeyAsync(ctx, purpose);
}
async function generateAscApiKeyAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<MinimalAscApiKey> {
//console.log('debug');
await ctx.appStore.ensureAuthenticatedAsync();
const ascApiKey = await ctx.appStore.createAscApiKeyAsync({
nickname: getAscApiKeyName(purpose),
});
return await getMinimalAscApiKeyAsync(ascApiKey);
}
export function getAscApiKeyName(purpose: AppStoreApiKeyPurpose): string {
const nameParts = ['[Expo]', purpose, nanoid(10)];
return nameParts.join(' ');
}
async function promptForAscApiKeyAsync(ctx: CredentialsContext): Promise<MinimalAscApiKey | null> {
const shouldAutoGenerateCredentials = await shouldAutoGenerateCredentialsAsync(ascApiKeyIdSchema);
if (shouldAutoGenerateCredentials) {
return null;
}
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
const { keyP8Path, keyId, issuerId } = ascApiKeyPath;
return { keyP8: await fs.readFile(keyP8Path, 'utf-8'), keyId, issuerId };
}
async function promptForKeyP8AndIdAsync(): Promise<Pick<AscApiKeyPath, 'keyP8Path' | 'keyId'>> {
Log.log(
chalk.bold(
'An App Store Connect Api key is required to upload your app to the Apple App Store Connect'
)
);
Log.log(
`If you're not sure what this is or how to create one, ${learnMore(
'https://expo.fyi/creating-asc-api-key'
)}`
);
const { keyP8Path } = await promptAsync({
type: 'text',
name: 'keyP8Path',
message: 'Path to App Store Connect API Key:',
initial: 'AuthKey_ABCD.p8',
// eslint-disable-next-line async-protect/async-suffix
validate: async (filePath: string) => {
try {
const stats = await fs.stat(filePath);
if (stats.isFile()) {
return true;
}
return 'Input is not a file.';
} catch {
return 'File does not exist.';
}
},
});
const regex = /^AuthKey_(?<keyId>\w+)\.p8$/; // Common ASC Api file name downloaded from Apple
const bestEffortKeyId = path.basename(keyP8Path).match(regex)?.groups?.keyId;
const { keyId } = await getCredentialsFromUserAsync(ascApiKeyIdSchema, {
keyId: bestEffortKeyId,
});
return { keyId, keyP8Path };
}
async function getBestEffortIssuerIdAsync(
ctx: CredentialsContext,
ascApiKeyId: string
): Promise<string | null> {
if (!ctx.appStore.authCtx) {
return null;
}
const ascApiKeyInfo = await ctx.appStore.getAscApiKeyAsync(ascApiKeyId);
return ascApiKeyInfo?.issuerId ?? null;
}
export async function getAscApiKeysFromAccountAsync(
ctx: CredentialsContext,
account: Account,
{ filterDifferentAppleTeam }: { filterDifferentAppleTeam?: boolean } = {}
): Promise<AppStoreConnectApiKeyFragment[]> {
const ascApiKeysForAccount = await ctx.ios.getAscApiKeysForAccountAsync(account);
if (!filterDifferentAppleTeam) {
return ascApiKeysForAccount;
} else {
return filterKeysFromDifferentAppleTeam(ctx, ascApiKeysForAccount);
}
}
export async function selectAscApiKeysFromAccountAsync(
ctx: CredentialsContext,
account: Account,
{ filterDifferentAppleTeam }: { filterDifferentAppleTeam?: boolean } = {}
): Promise<AppStoreConnectApiKeyFragment | null> {
const ascApiKeysForAccount = await getAscApiKeysFromAccountAsync(ctx, account, {
filterDifferentAppleTeam,
});
if (ascApiKeysForAccount.length === 0) {
if (filterDifferentAppleTeam) {
const maybeAppleTeamId = ctx.appStore.authCtx?.team.id;
Log.warn(
`There are no App Store Connect API Keys in your EAS account${
maybeAppleTeamId ? ` matching Apple Team ID: ${maybeAppleTeamId}.` : '.'
}`
);
} else {
Log.warn(`There are no App Store Connect API Keys available in your EAS account.`);
}
return null;
}
return selectAscApiKeysAsync(ascApiKeysForAccount);
}
async function selectAscApiKeysAsync(
ascApiKeys: AppStoreConnectApiKeyFragment[]
): Promise<AppStoreConnectApiKeyFragment | null> {
const sortedAscApiKeys = sortAscApiKeysByUpdatedAtDesc(ascApiKeys);
const { chosenAscApiKey } = await promptAsync({
type: 'select',
name: 'chosenAscApiKey',
message: 'Select an API Key from the list:',
choices: sortedAscApiKeys.map(ascApiKey => ({
title: formatAscApiKey(ascApiKey),
value: ascApiKey,
})),
});
return chosenAscApiKey;
}
function filterKeysFromDifferentAppleTeam(
ctx: CredentialsContext,
keys: AppStoreConnectApiKeyFragment[]
): AppStoreConnectApiKeyFragment[] {
if (!ctx.appStore.authCtx) {
return keys;
}
const teamId = ctx.appStore.authCtx.team.id;
return keys.filter(key => !key.appleTeam || key.appleTeam?.appleTeamIdentifier === teamId);
}
export function sortAscApiKeysByUpdatedAtDesc(
keys: AppStoreConnectApiKeyFragment[]
): AppStoreConnectApiKeyFragment[] {
return keys.sort(
(keyA, keyB) => new Date(keyB.updatedAt).getTime() - new Date(keyA.updatedAt).getTime()
);
}
export function formatAscApiKey(ascApiKey: AppStoreConnectApiKeyFragment): string {
const { keyIdentifier, appleTeam, name, updatedAt } = ascApiKey;
let line: string = '';
line += `Key ID: ${keyIdentifier}`;
if (name) {
line += chalk.gray(`\n Name: ${name}`);
}
if (appleTeam) {
line += chalk.gray(`\n ${formatAppleTeam(appleTeam)}`);
}
line += chalk.gray(`\n Updated: ${fromNow(new Date(updatedAt))} ago`);
return line;
}
@@ -1,38 +0,0 @@
import {
AppStoreConnectApiKeyFragment,
CommonIosAppCredentialsFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { AppStoreApiKeyPurpose } from './AscApiKeyUtils';
export class AssignAscApiKey {
constructor(private app: AppLookupParams) {}
public async runAsync(
ctx: CredentialsContext,
ascApiKey: AppStoreConnectApiKeyFragment,
purpose: AppStoreApiKeyPurpose
): Promise<CommonIosAppCredentialsFragment> {
const appleTeam =
(await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app)) ?? ascApiKey.appleTeam ?? null;
const appCredentials = await ctx.ios.createOrGetIosAppCredentialsWithCommonFieldsAsync(
this.app,
{ appleTeam: appleTeam ?? undefined }
);
let updatedAppCredentials;
if (purpose === AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
updatedAppCredentials = await ctx.ios.updateIosAppCredentialsAsync(appCredentials, {
ascApiKeyIdForSubmissions: ascApiKey.id,
});
} else {
throw new Error(`${purpose} is not yet supported.`);
}
Log.succeed(
`App Store Connect API Key assigned to ${this.app.projectName}: ${this.app.bundleIdentifier} for ${purpose}.`
);
return updatedAppCredentials;
}
}
@@ -1,23 +0,0 @@
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { Account } from '../../../user/Account';
import { CredentialsContext } from '../../context';
import { AppStoreApiKeyPurpose, provideOrGenerateAscApiKeyAsync } from './AscApiKeyUtils';
export class CreateAscApiKey {
constructor(private account: Account) {}
public async runAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<AppStoreConnectApiKeyFragment> {
if (ctx.nonInteractive) {
throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
}
const ascApiKey = await provideOrGenerateAscApiKeyAsync(ctx, purpose);
const result = await ctx.ios.createAscApiKeyAsync(this.account, ascApiKey);
Log.succeed('Created App Store Connect API Key');
return result;
}
}
@@ -10,7 +10,7 @@ export class CreateDistributionCertificate {
public async runAsync(
ctx: CredentialsContext
): Promise<AppleDistributionCertificateMutationResult> {
const distCert = await provideOrGenerateDistributionCertificateAsync(ctx);
const distCert = await provideOrGenerateDistributionCertificateAsync(ctx, this.account.name);
const result = await ctx.ios.createDistributionCertificateAsync(this.account, distCert);
Log.succeed('Created distribution certificate');
return result;
Loaded 100 of 259 files, more files were not shown because too many files have changed in this diff. Show more