Compare commits

..
Author SHA1 Message Date
Test User f06de30d17 remove bumpVersionAsync 2021-09-20 09:17:17 -07:00
Test User 13ff2c06d1 add changelog 2021-09-20 08:03:24 -07:00
Test User b08da004fd smaller change 2021-09-20 07:52:45 -07:00
Test User 15033f6f98 [eas-json][eas-cli] add autoIncrement validation checks and android types 2021-09-20 07:39:16 -07:00
348 changed files with 5522 additions and 10624 deletions

No files matched your search

+1 -3
View File
@@ -1,7 +1,7 @@
module.exports = {
root: true,
extends: ['universe/node'],
plugins: ['async-protect', 'node'],
plugins: ['async-protect'],
rules: {
'no-console': 'warn',
'no-constant-condition': ['warn', { checkLoops: false }],
@@ -34,7 +34,6 @@ module.exports = {
],
},
],
'no-underscore-dangle': ['error', { allow: ['__typename'] }],
'@typescript-eslint/explicit-function-return-type': [
'warn',
{
@@ -42,7 +41,6 @@ module.exports = {
},
],
'async-protect/async-suffix': 'error',
'node/no-sync': 'error',
},
overrides: [
{
Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

+1 -1
View File
@@ -2,7 +2,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 added an entry to [CHANGELOG.md](https://github.com/expo/eas-cli/blob/main/CHANGELOG.md#main) if necessary.
- [ ] 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
-46
View File
@@ -1,46 +0,0 @@
on:
issue_comment:
types: [created]
name: Changelog
jobs:
pr-commented:
runs-on: ubuntu-latest
if: ${{ github.event.issue.pull_request && github.event.issue.user.login == github.event.comment.user.login && startsWith(github.event.comment.body, '/changelog-entry ') }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '16'
- name: Checkout Pull Request
run: hub pr checkout ${{ github.event.issue.number }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
working-directory: ./scripts
run: yarn install --frozen-lockfile --check-files
- name: Add changelog entry
working-directory: ./scripts
run: |
PARAMS=${PR_COMMENT_BODY#/changelog-entry }
if [[ ! "$PARAMS" =~ " " ]]; then
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:]]*//' )
yarn changelog-entry "$CATEGORY" "$MESSAGE"
env:
GITHUB_PR_AUTHOR: ${{ github.event.issue.user.login }}
GITHUB_PR_NUMBER: ${{ github.event.issue.number }}
PR_COMMENT_BODY: ${{ github.event.comment.body }}
- name: Commit and push
run: |
# use the last commit author name and email
git config --global user.name "$(git --no-pager log --format=format:'%an' -n 1)"
git config --global user.email "$(git --no-pager log --format=format:'%ae' -n 1)"
git add CHANGELOG.md
git commit -m "update CHANGELOG.md"
git push
+1 -68
View File
@@ -15,7 +15,7 @@ jobs:
- id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
tag_name: ${{ github.ref }}
draft: true
@@ -113,70 +113,3 @@ jobs:
asset_path: packages/eas-cli/dist/win/${{ steps.x86.outputs.filename }}
asset_name: ${{ steps.x86.outputs.filename }}
asset_content_type: application/gzip
publish-to-npm:
name: Publish to npm
runs-on: ubuntu-latest
needs: [build-linux, build-mac, build-windows]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
registry-url: 'https://registry.npmjs.org/'
scope: 'expo'
node-version: '16'
- name: Install dependencies
run: yarn install --frozen-lockfile --check-files
- name: Build
run: yarn build
- name: Publish packages to npm
run: yarn lerna publish from-package --yes --no-verify-access
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
release-changelog:
name: Update changelog and publish release
runs-on: ubuntu-latest
needs: publish-to-npm
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '16'
- name: Install dependencies
working-directory: ./scripts
run: yarn install --frozen-lockfile --check-files
- name: Resolve version
run: echo "EAS_CLI_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Update changelog
working-directory: ./scripts
run: yarn --silent release-changelog $EAS_CLI_VERSION > /tmp/current-changelog
- name: Publish release
run: hub release edit v$EAS_CLI_VERSION --draft=false -F /tmp/current-changelog
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Commit and push
run: |
git fetch
git checkout main
git config --global user.email "[email protected]"
git config --global user.name "Expo CI"
git add CHANGELOG.md
git commit -m "update CHANGELOG.md"
git push
- name: Prepare changelog for Slack
run: |
tail -n +3 /tmp/current-changelog > /tmp/changelog.md
npx slackmd /tmp/changelog.md
echo 'SLACK_CHANGELOG<<EOF' >> $GITHUB_ENV
cat /tmp/changelog.slack.md >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Slack Notification
uses: rtCamp/action-slack-notify@v2
env:
SLACK_CHANNEL: eas-cli
SLACK_COLOR: ${{ job.status }}
SLACK_ICON: https://github.com/expo-ci.png?size=48
SLACK_TITLE: 'Released eas-cli@${{ env.EAS_CLI_VERSION }}'
SLACK_MESSAGE: ${{ env.SLACK_CHANGELOG }}
SLACK_USERNAME: 'Expo CI'
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_FOOTER: ''
+2 -184
View File
@@ -6,199 +6,17 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
### 🎉 New features
- Add ASC Api Key generation workflow. ([#718](https://github.com/expo/eas-cli/pull/718) by [@quinlanj](https://github.com/quinlanj))
- Add support for removal of ASC Api Keys. ([#721](https://github.com/expo/eas-cli/pull/721) by [@quinlanj](https://github.com/quinlanj))
- Allow users to assign an ASC Api Key to their project. ([#719](https://github.com/expo/eas-cli/pull/719) by [@quinlanj](https://github.com/quinlanj))
- Add setup support for ASC Api Keys. ([#733](https://github.com/expo/eas-cli/pull/733) by [@quinlanj](https://github.com/quinlanj))
- Show initiating user display name when selecting a build to submit. ([#730](https://github.com/expo/eas-cli/pull/730) by [@barthap](https://github.com/barthap))
- Handle Apple servers maintenance error in `eas submit`. ([#738](https://github.com/expo/eas-cli/pull/738) by [@barthap](https://github.com/barthap))
- Integrate ASC Api Key with submissions workflow. ([#737](https://github.com/expo/eas-cli/pull/737) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
### 🧹 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/0.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/0.34.0) - 2021-11-01
### 🛠 Breaking changes
- `[EAS BUILD API]` Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701) and [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal))
- Don't print logs to `stderr`. ([#708](https://github.com/expo/eas-cli/pull/708) by [@dsokal](https://github.com/dsokal))
- Remove automatic migration for legacy `eas.json` format introduced in [v0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2). ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
- Add required `cli` field in root of `eas.json` ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 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/0.33.1) - 2021-10-22
- Only allow autoIncrementing on managed projects. ([#630](https://github.com/expo/eas-cli/pull/630) by [@jkhales](https://github.com/jkhales))
### 🎉 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/0.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/0.32.0) - 2021-10-15
### 🎉 New features
- Add App Store Connect Api Key support to iOS submissions. ([#678](https://github.com/expo/eas-cli/pull/678) by [@quinlanj](https://github.com/quinlanj))
- 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/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/0.31.0) - 2021-10-08
### 🛠 Breaking changes
- Don't resolve the iOS builder image on the client side. EAS Build will use the appropriate iOS image for a given Expo SDK version unless the `image` is defined. This only applies to managed projects. ([#675](https://github.com/expo/eas-cli/pull/675) by [@wkozyra95](https://github.com/wkozyra95) + [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Integrate credentials service with Android submissions. ([#664](https://github.com/expo/eas-cli/pull/664) by [@quinlanj](https://github.com/quinlanj))
- Add option to review ad-hoc devices when reusing provisioning profile. ([#673](https://github.com/expo/eas-cli/pull/673) by [@dsokal](https://github.com/dsokal))
## [0.30.1](https://github.com/expo/eas-cli/releases/tag/0.30.1) - 2021-10-06
### 🐛 Bug fixes
- Fix `--json` flag when running EAS CLI on GitHub actions. ([#669](https://github.com/expo/eas-cli/pull/669) by [@dsokal](https://github.com/dsokal))
- Fix `"ios: mods.ios.infoPlist: Failed to find Info.plist linked to Xcode project."` warning when running `eas build` in a managed project.([#670](https://github.com/expo/eas-cli/pull/670) by [@brentvatne](https://github.com/brentvatne))
- Fix building monorepo projects on Windows. ([#671](https://github.com/expo/eas-cli/pull/671) by [@dsokal](https://github.com/dsokal))
## [0.30.0](https://github.com/expo/eas-cli/releases/tag/v0.30.0) - 2021-10-05
### 🛠 Breaking changes
- Enable auto-incrementing on Android. ([#645](https://github.com/expo/eas-cli/pull/645) by [@jkhales](https://github.com/jkhales))
### 🧹 Chores
- Ports detection for Google Service Account Keys into the credentials service. ([#660](https://github.com/expo/eas-cli/pull/660) by [@quinlanj](https://github.com/quinlanj))
- Improve iOS credentials printing. ([#657](https://github.com/expo/eas-cli/pull/657) by [@quinlanj](https://github.com/quinlanj))
- Automate `eas-cli` releases. ([#668](https://github.com/expo/eas-cli/pull/668) by [@dsokal](https://github.com/dsokal))
## [0.29.1](https://github.com/expo/eas-cli/releases/tag/v0.29.1) - 2021-09-29
### 🎉 New features
- More upload support for Google Service Account Keys. ([#649](https://github.com/expo/eas-cli/pull/649) by [@quinlanj](https://github.com/quinlanj))
- Allow the user to assign an existing Google Service Account Key to their project. ([#650](https://github.com/expo/eas-cli/pull/650) by [@quinlanj](https://github.com/quinlanj))
- Allow the user to remove a Google Service Account Key from their account. ([#658](https://github.com/expo/eas-cli/pull/658) by [@quinlanj](https://github.com/quinlanj))
- Adds setup support for Google Service Account Keys. ([#659](https://github.com/expo/eas-cli/pull/659) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Fix double prompt for Apple credentials when running `eas build --auto-submit`. ([#654](https://github.com/expo/eas-cli/pull/654) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Always use async `fs` functions. ([#652](https://github.com/expo/eas-cli/pull/652) by [@dsokal](https://github.com/dsokal))
- Improve Android credentials printing. ([#656](https://github.com/expo/eas-cli/pull/656) by [@quinlanj](https://github.com/quinlanj))
## [0.29.0](https://github.com/expo/eas-cli/releases/tag/v0.29.0) - 2021-09-28
### 🎉 New features
- Added upload support for Google Service Account Keys. ([#642](https://github.com/expo/eas-cli/pull/642) by [@quinlanj](https://github.com/quinlanj))
- Add Xcode 13 image. ([#651](https://github.com/expo/eas-cli/pull/651) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Credentials manager: stop prompting for an Android build profile every iteration. ([#641](https://github.com/expo/eas-cli/pull/641) by [@quinlanj](https://github.com/quinlanj))
- Don't display prompt in non-interactive mode when the metro config seems invalid. ([#644](https://github.com/expo/eas-cli/pull/644) by [@dsokal](https://github.com/dsokal))
- Read versions from correct `Info.plist`. ([#635](https://github.com/expo/eas-cli/pull/635) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Move away from weird convention of exports. ([#647](https://github.com/expo/eas-cli/pull/647) by [@quinlanj](https://github.com/quinlanj))
## [0.28.2](https://github.com/expo/eas-cli/releases/tag/v0.28.2) - 2021-09-23
### 🐛 Bug fixes
- Use `log-symbols` to unify green ticks style. ([#639](https://github.com/expo/eas-cli/pull/639) by [@dsokal](https://github.com/dsokal))
- Fix `eas build --auto-submit` in fresh projects. ([#640](https://github.com/expo/eas-cli/pull/640) by [@dsokal](https://github.com/dsokal))
## [0.28.1](https://github.com/expo/eas-cli/releases/tag/v0.28.1) - 2021-09-22
### 🐛 Bug fixes
- Prevent ora spinners breaking when debug logs are enabled. ([#575](https://github.com/expo/eas-cli/pull/575) by [@EvanBacon](https://github.com/EvanBacon))
- Do not fail `eas build` when transient network errors occur. ([#638](https://github.com/expo/eas-cli/pull/638) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Add GraphQL support for GoogleServiceAccountKey. ([#632](https://github.com/expo/eas-cli/pull/632) by [@quinlanj](https://github.com/quinlanj))
- Remove GraphQL support for the App Specific Password. ([#633](https://github.com/expo/eas-cli/pull/633) by [@quinlanj](https://github.com/quinlanj))
## [0.28.0](https://github.com/expo/eas-cli/releases/tag/v0.28.0) - 2021-09-20
## [0.28.0](https://github.com/expo/eas-cli/releases/tag/v0.28.0) - 2021-09-10
### 🎉 New features
-7
View File
@@ -1,7 +0,0 @@
packages/eas-cli/src/build @dsokal @wkozyra95
packages/eas-cli/src/commands/build @dsokal @wkozyra95
packages/eas-cli/src/submit @barthap @dsokal
packages/eas-cli/src/commands/submit.ts @barthap @dsokal
packages/eas-json @dsokal @wkozyra95
+33 -4
View File
@@ -1,10 +1,39 @@
## Releasing a new version
1. Run `yarn release` in the repository root folder.
2. Wait, the `release` workflow on GitHub Actions is going to take care of the rest (including publishing packages to npm and updating the changelog).
3. Check the changelog. If any of the entries have been tagged with `[EAS BUILD API]`
1. Run `yarn release` in the repository root folder. This will create a new tag like `v1.0.0`.
1. **Wait until the `release` GitHub workflow has finished.**
![Wait for all three jobs to complete](./.gh-assets/release-workflow.png)
1. Run `yarn lerna publish from-git` in the repository root folder. This will publish the packages to npm.
- This command works only if `HEAD` is on the release commit, e.g. when `git log -1` prints:
```
commit cbb2de9e118bfbcb5a597b261d2dbef0bb5d4b0b (tag: v0.11.1)
Author: Brent Vatne <[email protected]>
Date: Tue Apr 20 15:00:15 2021 -0700
v0.11.1
```
- If you run `yarn lerna publish from-git` on any other commit, it'll print something like:
```
lerna notice from-git No tagged release found
lerna success No changed packages to publish
```
- If you ever run into the above issue, use `git checkout RELEASE_COMMIT_HASH` to checkout to the release commit. Later, when you've published the packages to npm, run `git checkout main` to go back to the latest commit.
1. Update `CHANGELOG.md`.
- Add the new section with the release version in the title.
- Leave the `main` section empty (without any changelog entries).
1. Once the release is ready to go, you can go to https://github.com/expo/eas-cli/releases and mark it as not draft.
- Remember to copy the changelog entries from `CHANGELOG.md`.
- Before hitting the submit button, make sure there are 5 assets available under the release entry.
![There are 5 assets for each release](./.gh-assets/draft-release.png)
1. If any of the changelog entries have been tagged with `[EAS BUILD API]`
- Open `package.json` and note the version of `@expo/eas-build-job` used in the eas-cli.
- Find the `Publish` commit in [expo/eas-build](https://github.com/expo/eas-build/commits) for that version. In most cases, it will be the latest `Publish` commit.
- Find the `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.34.1"
"version": "0.28.0"
}
+6 -6
View File
@@ -1,6 +1,8 @@
{
"private": true,
"workspaces": ["packages/*"],
"workspaces": [
"packages/*"
],
"scripts": {
"build": "lerna run build",
"build:tarballs:linux": "yarn build && yarn workspace eas-cli oclif-dev pack --targets linux-x64",
@@ -10,8 +12,7 @@
"eas": "packages/eas-cli/bin/run",
"lint": "eslint . --ext .ts --ext .js",
"release": "lerna version",
"test": "jest",
"clean": "lerna run clean && rm -rf node_modules coverage"
"test": "jest"
},
"devDependencies": {
"@expo/oclif-dev-cli": "1.24.0-expo",
@@ -20,7 +21,6 @@
"eslint": "7.32.0",
"eslint-config-universe": "8.0.0",
"eslint-plugin-async-protect": "1.1.0",
"eslint-plugin-node": "11.1.0",
"jest": "27.1.0",
"jest-watch-typeahead": "0.6.4",
"lerna": "4.0.0",
@@ -29,7 +29,7 @@
"typescript": "4.4.2"
},
"volta": {
"node": "16.10.0",
"yarn": "1.22.15"
"node": "16.8.0",
"yarn": "1.22.11"
}
}
+30 -32
View File
@@ -73,7 +73,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/login.js)_
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/account/login.js)_
## `eas account:logout`
@@ -87,7 +87,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/logout.js)_
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/account/logout.js)_
## `eas account:view`
@@ -101,7 +101,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/view.js)_
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/account/view.js)_
## `eas analytics [STATUS]`
@@ -112,7 +112,7 @@ USAGE
$ eas analytics [STATUS]
```
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/analytics.js)_
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/analytics.js)_
## `eas build`
@@ -138,15 +138,14 @@ OPTIONS
--non-interactive Run command in non-interactive mode
--profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined
in eas.json.
--profile=PROFILE_NAME [default: release] Name of the build profile from eas.json
--skip-project-configuration Skip project configuration
--[no-]wait Wait for build(s) to complete
```
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/index.js)_
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/build/index.js)_
## `eas build:cancel [BUILD_ID]`
@@ -157,7 +156,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/cancel.js)_
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/build/cancel.js)_
## `eas build:configure`
@@ -171,7 +170,7 @@ OPTIONS
-p, --platform=(android|ios|all) Platform to configure
```
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/configure.js)_
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/build/configure.js)_
## `eas build:list`
@@ -204,7 +203,7 @@ OPTIONS
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/list.js)_
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/build/list.js)_
## `eas build:view [BUILD_ID]`
@@ -218,7 +217,7 @@ OPTIONS
--json Enable JSON output, non-JSON messages will be printed to stderr
```
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/view.js)_
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/build/view.js)_
## `eas config`
@@ -233,7 +232,7 @@ OPTIONS
--profile=profile
```
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/config.js)_
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/config.js)_
## `eas credentials`
@@ -244,7 +243,7 @@ USAGE
$ eas credentials
```
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/credentials.js)_
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/credentials.js)_
## `eas device:create`
@@ -255,7 +254,7 @@ USAGE
$ eas device:create
```
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/create.js)_
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/device/create.js)_
## `eas device:list`
@@ -269,7 +268,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/list.js)_
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/device/list.js)_
## `eas device:view [UDID]`
@@ -280,7 +279,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/view.js)_
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/device/view.js)_
## `eas diagnostics`
@@ -291,7 +290,7 @@ USAGE
$ eas diagnostics
```
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/diagnostics.js)_
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/diagnostics.js)_
## `eas help [COMMAND]`
@@ -319,7 +318,7 @@ USAGE
$ eas project:info
```
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/project/info.js)_
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/project/info.js)_
## `eas project:init`
@@ -333,7 +332,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/project/init.js)_
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/project/init.js)_
## `eas secret:create`
@@ -350,7 +349,7 @@ OPTIONS
--value=value Value of the secret
```
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/create.js)_
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/secret/create.js)_
## `eas secret:delete`
@@ -364,10 +363,10 @@ OPTIONS
--id=id ID of the secret to delete
DESCRIPTION
Unsure where to find the secret's ID? Run eas secret:list
Unsure where to find the secret's ID? Run eas secrets:list
```
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/delete.js)_
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/secret/delete.js)_
## `eas secret:list`
@@ -378,7 +377,7 @@ USAGE
$ eas secret:list
```
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/list.js)_
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/secret/list.js)_
## `eas submit`
@@ -395,7 +394,7 @@ OPTIONS
--non-interactive Run command in non-interactive mode
--path=path Path to the .apk/.aab/.ipa file
--profile=profile Name of the submit profile from eas.json. Defaults to "production" if defined in
--profile=profile Name of the submit profile from eas.json. Defaults to "release" if defined in
eas.json.
--url=url App archive url
@@ -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.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/submit.js)_
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/submit.js)_
## `eas webhook:create`
@@ -431,7 +429,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/create.js)_
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.28.0/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.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/delete.js)_
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/webhook/delete.js)_
## `eas webhook:list`
@@ -459,7 +457,7 @@ OPTIONS
--event=(BUILD) Event type that triggers the webhook
```
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/list.js)_
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/webhook/list.js)_
## `eas webhook:update`
@@ -479,7 +477,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/update.js)_
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.28.0/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.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/view.js)_
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.28.0/packages/eas-cli/src/commands/webhook/view.js)_
<!-- commandsstop -->
-2
View File
@@ -1,10 +1,8 @@
import { fs } from 'memfs';
// needed because of a weird bug with tempy (dependency of @expo/config-plugins)
// eslint-disable-next-line node/no-sync
fs.mkdirSync('/tmp');
if (process.env.TMPDIR) {
// eslint-disable-next-line node/no-sync
fs.mkdirSync(process.env.TMPDIR, { recursive: true });
}
@@ -1,4 +1,4 @@
export const ora = jest.fn().mockReturnValue({
export default jest.fn().mockReturnValue({
start: jest.fn().mockReturnValue({
fail: jest.fn(),
succeed: jest.fn(),
+1155 -1345
View File
File diff suppressed because it is too large. Load diff
+10 -12
View File
@@ -1,25 +1,25 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.34.1",
"version": "0.28.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.26",
"@expo/config": "6.0.3",
"@expo/config-plugins": "4.0.3",
"@expo/eas-build-job": "0.2.57",
"@expo/eas-json": "^0.34.1",
"@expo/apple-utils": "0.0.0-alpha.25",
"@expo/config": "5.0.9",
"@expo/config-plugins": "3.1.0",
"@expo/eas-build-job": "0.2.46",
"@expo/eas-json": "^0.28.0",
"@expo/json-file": "8.2.33",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.14",
"@expo/plugin-warn-if-update-available": "1.7.0",
"@expo/prebuild-config": "3.0.3",
"@expo/prebuild-config": "2.1.0",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.0",
"@expo/rudder-sdk-node": "1.0.7",
"@expo/sdk-runtime-versions": "1.0.0",
"@expo/spawn-async": "1.5.0",
"@expo/timeago.js": "1.0.0",
@@ -48,7 +48,6 @@
"jks-js": "1.0.1",
"joi": "17.4.2",
"keychain": "1.3.0",
"log-symbols": "4.1.0",
"mime": "2.5.2",
"minimatch": "3.0.4",
"nanoid": "3.1.25",
@@ -61,7 +60,6 @@
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
"semver": "7.3.5",
"slash": "3.0.0",
"strip-ansi": "6.0.0",
"tar": "6.1.11",
"terminal-link": "2.1.1",
@@ -113,6 +111,7 @@
"cli"
],
"license": "MIT",
"main": "build/index.js",
"oclif": {
"bin": "eas",
"commands": "./build/commands",
@@ -176,7 +175,6 @@
"typecheck": "tsc",
"test": "jest",
"version": "yarn oclif-dev readme && node patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
"clean": "rm -rf dist build node_modules yarn-error.log"
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml"
}
}
+8 -10
View File
@@ -1,12 +1,10 @@
const { promises: fs } = require('fs');
const fs = require('fs');
(async () => {
// Patch `oclif-dev readme` path and link generation
let readmeContent = await fs.readFile('README.md', 'utf8');
readmeContent = readmeContent.replace(/\[build\//g, '[src/');
readmeContent = readmeContent.replace(/build\/commands/g, 'packages/eas-cli/src/commands');
await fs.writeFile('README.md', readmeContent);
// Patch `oclif-dev readme` path and link generation
let readmeContent = fs.readFileSync('README.md', 'utf8');
readmeContent = readmeContent.replace(/\[build\//g, '[src/');
readmeContent = readmeContent.replace(/build\/commands/g, 'packages/eas-cli/src/commands');
fs.writeFileSync('README.md', readmeContent);
// eslint-disable-next-line no-console
console.log('Patched README path generation');
})();
// eslint-disable-next-line no-console
console.log('Patched README path generation');
@@ -25,7 +25,7 @@ interface AppJSON {
};
}
export function createTestProject(
function createTestProject(
user: Actor,
appJSONExtraData?: Record<string, object | string>
): MockProject {
@@ -58,3 +58,5 @@ export function createTestProject(
},
};
}
export { createTestProject };
+18 -9
View File
@@ -4,7 +4,13 @@ import { URL } from 'url';
import { v4 as uuidv4 } from 'uuid';
import UserSettings from './user/UserSettings';
import { easCliVersion } from './utils/easCli';
/**
* 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',
@@ -59,6 +65,7 @@ export async function initAsync(): Promise<void> {
flushInterval: 300,
}
);
rudderstackClient.logger.silent = true;
}
}
@@ -80,7 +87,7 @@ export async function setUserDataAsync(userId: string, traits: Record<string, an
export async function flushAsync(): Promise<void> {
if (rudderstackClient) {
await rudderstackClient.flush();
rudderstackClient.flush();
}
}
@@ -91,7 +98,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({
@@ -107,11 +114,13 @@ function ensureUserIdentified(): void {
return;
}
rudderstackClient.identify({
userId: identifyData.userId,
anonymousId: identifyData.deviceId,
traits: identifyData.traits,
});
if (rudderstackClient) {
rudderstackClient.identify({
userId: identifyData.userId,
anonymousId: identifyData.deviceId,
traits: identifyData.traits,
});
}
userIdentified = true;
}
@@ -120,7 +129,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 },
};
}
@@ -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)) {
@@ -6,17 +6,17 @@ import { readChannelSafelyAsync } from '../UpdatesModule';
jest.mock('fs');
afterAll(async () => {
afterAll(() => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.remove(os.tmpdir());
fs.removeSync(os.tmpdir());
});
beforeEach(async () => {
beforeEach(() => {
vol.reset();
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.mkdirp(os.tmpdir());
fs.mkdirpSync(os.tmpdir());
});
const testChannel = 'test-channel';
@@ -1,204 +1,42 @@
import { ExpoConfig } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import assert from 'assert';
import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import path from 'path';
import { getAppBuildGradleAsync, resolveConfigValue } from '../../../project/android/gradleUtils';
import {
BumpStrategy,
bumpVersionAsync,
bumpVersionInAppJsonAsync,
maybeResolveVersionsAsync,
} from '../version';
import { maybeResolveVersionsAsync } from '../version';
const fsReal = jest.requireActual('fs').promises as typeof fs;
jest.mock('fs');
afterAll(async () => {
afterAll(() => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.remove(os.tmpdir());
fs.removeSync(os.tmpdir());
});
beforeEach(async () => {
beforeEach(() => {
vol.reset();
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.mkdirp(os.tmpdir());
});
// bare workflow
describe(bumpVersionAsync, () => {
it('throws an informative error when multiple flavors are detected in the android project', async () => {
await initProjectWithGradleFileAsync(
/*buildGradlePath*/ path.join(
__dirname,
'../../../project/android/__tests__/fixtures/multiflavor-build.gradle'
)
);
await expect(
bumpVersionAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/multiflavor',
exp: {} as any,
})
).rejects.toThrow(
'Automatic version bumping is not supported for multi-flavor Android projects.'
);
});
it('throws an informative error when multiple flavor dimensions are defined in an android project', async () => {
await initProjectWithGradleFileAsync(
/*buildGradlePath*/ path.join(
__dirname,
'../../../project/android/__tests__/fixtures/multiflavor-with-dimensions-build.gradle'
)
);
await expect(
bumpVersionAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/multiflavor',
exp: {} as any,
})
).rejects.toThrow(
'Automatic version bumping is not supported for multi-flavor Android projects.'
);
});
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 });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('3.0.0');
expect(fakeExp.android?.versionCode).toBe(124);
expect(appJSON.expo.version).toBe('3.0.0');
expect(appJSON.expo.android.versionCode).toBe(124);
const buildGradle = await getAppBuildGradleAsync('/repo');
assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('124');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
});
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 });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('3.0.1');
expect(fakeExp.android?.versionCode).toBe(123);
expect(appJSON.expo.version).toBe('3.0.1');
expect(appJSON.expo.android.versionCode).toBe(123);
const buildGradle = await getAppBuildGradleAsync('/repo');
assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.1');
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initBareWorkflowProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('3.0.0');
expect(fakeExp.android?.versionCode).toBe(123);
expect(appJSON.expo.version).toBe('3.0.0');
expect(appJSON.expo.android.versionCode).toBe(123);
const buildGradle = await getAppBuildGradleAsync('/repo');
assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
});
});
// managed workflow
describe(bumpVersionInAppJsonAsync, () => {
it('bumps expo.android.versionCode when strategy = BumpStrategy.VERSION_CODE', async () => {
const fakeExp = initManagedProject();
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('5.0.0');
expect(fakeExp.android?.versionCode).toBe(127);
expect(appJSON.expo.version).toBe('5.0.0');
expect(appJSON.expo.android.versionCode).toBe(127);
});
it('bumps expo.version when strategy = BumpStrategy.SHORT_VERSION', async () => {
const fakeExp = initManagedProject();
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('5.0.1');
expect(fakeExp.android?.versionCode).toBe(126);
expect(appJSON.expo.version).toBe('5.0.1');
expect(appJSON.expo.android.versionCode).toBe(126);
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initManagedProject();
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.NOOP,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('5.0.0');
expect(fakeExp.android?.versionCode).toBe(126);
expect(appJSON.expo.version).toBe('5.0.0');
expect(appJSON.expo.android.versionCode).toBe(126);
});
fs.mkdirpSync(os.tmpdir());
});
describe(maybeResolveVersionsAsync, () => {
describe('bare project', () => {
it('reads the versions from native code', async () => {
const exp = initBareWorkflowProject();
describe('generic project', () => {
it('reads the version code from native code', async () => {
const exp = initGenericProject();
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/repo',
exp,
{} as BuildProfile<Platform.ANDROID>
);
expect(appVersion).toBe('3.0.0');
expect(appVersion).toBe('2.0');
expect(appBuildVersion).toBe('123');
});
});
describe('managed project', () => {
it('reads the versions from expo config', async () => {
it('reads the version code from expo config', async () => {
const exp = initManagedProject();
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/repo',
@@ -211,31 +49,22 @@ describe(maybeResolveVersionsAsync, () => {
});
});
function initBareWorkflowProject({
versionCode = 123,
versionName = '3.0.0',
}: {
versionCode?: number;
versionName?: string;
} = {}): ExpoConfig {
const fakeExp: ExpoConfig = {
name: 'myproject',
slug: 'myproject',
version: '3.0.0',
android: {
versionCode: 123,
},
};
function initGenericProject(): ExpoConfig {
vol.fromJSON(
{
'./app.json': JSON.stringify({
expo: fakeExp,
expo: {
version: '1.0.0',
android: {
versionCode: 1,
},
},
}),
'./android/app/build.gradle': `android {
defaultConfig {
applicationId "com.expo.testapp"
versionCode ${versionCode}
versionName "${versionName}"
versionCode 123
versionName "2.0"
}
}`,
'./android/app/src/main/AndroidManifest.xml': 'fake',
@@ -243,10 +72,32 @@ function initBareWorkflowProject({
'/repo'
);
const fakeExp: ExpoConfig = {
name: 'myproject',
slug: 'myproject',
version: '3.0.0',
android: {
versionCode: 124,
},
};
return fakeExp;
}
function initManagedProject(): ExpoConfig {
vol.fromJSON(
{
'./app.json': JSON.stringify({
expo: {
version: '4.0.0',
android: {
versionCode: 125,
},
},
}),
},
'/repo'
);
const fakeExp: ExpoConfig = {
name: 'myproject',
slug: 'myproject',
@@ -255,26 +106,5 @@ function initManagedProject(): ExpoConfig {
versionCode: 126,
},
};
vol.fromJSON(
{
'./app.json': JSON.stringify({
expo: fakeExp,
}),
},
'/repo'
);
return fakeExp;
}
async function initProjectWithGradleFileAsync(gradleFilePath: string): Promise<void> {
vol.fromJSON(
{
'android/app/build.gradle': await fsReal.readFile(gradleFilePath, 'utf-8'),
'./app.json': JSON.stringify({
expo: {} as any,
}),
},
'/multiflavor'
);
}
+20 -15
View File
@@ -5,6 +5,7 @@ import nullthrows from 'nullthrows';
import AndroidCredentialsProvider, {
AndroidCredentials,
} from '../../credentials/android/AndroidCredentialsProvider';
import { createCredentialsContextAsync } from '../../credentials/context';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import Log from '../../log';
import {
@@ -64,11 +65,9 @@ This means that it will most likely produce an AAB and you will not be able to i
return await ensureAndroidCredentialsAsync(ctx, gradleContext);
},
ensureProjectConfiguredAsync: async () => {
await validateAndSyncProjectConfigurationAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile,
});
if (ctx.workflow === Workflow.GENERIC) {
await validateAndSyncProjectConfigurationAsync(ctx.projectDir, ctx.exp);
}
},
getMetadataContext: () => ({
gradleContext,
@@ -111,16 +110,22 @@ async function ensureAndroidCredentialsAsync(
ctx.exp,
gradleContext
);
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
app: {
account: nullthrows(
findAccountByName(ctx.user.accounts, ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.projectName,
androidApplicationIdentifier,
},
});
const provider = new AndroidCredentialsProvider(
await createCredentialsContextAsync(ctx.projectDir, {
exp: ctx.exp,
nonInteractive: ctx.nonInteractive,
}),
{
app: {
account: nullthrows(
findAccountByName(ctx.user.accounts, ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.projectName,
androidApplicationIdentifier,
},
}
);
const { credentialsSource } = ctx.buildProfile;
logCredentialsSource(credentialsSource, Platform.ANDROID);
return {
+14 -44
View File
@@ -1,19 +1,15 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
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';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void> {
if (!ctx.hasAndroidNativeProject) {
@@ -21,12 +17,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);
@@ -34,42 +30,16 @@ export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void
Log.withTick('Android project configured');
}
export async function validateAndSyncProjectConfigurationAsync({
projectDir,
exp,
buildProfile,
}: {
projectDir: string;
exp: ExpoConfig;
buildProfile: AndroidBuildProfile;
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const { autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
if (!(await AndroidConfig.EasBuild.isEasBuildGradleConfiguredAsync(projectDir))) {
throw new Error(
'Project is not configured. Please run "eas build:configure" to configure the project.'
);
}
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
} else {
await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
export async function validateAndSyncProjectConfigurationAsync(
projectDir: string,
exp: ExpoConfig
): Promise<void> {
if (!(await AndroidConfig.EasBuild.isEasBuildGradleConfiguredAsync(projectDir))) {
throw new Error(
'Project is not configured. Please run "eas build:configure" to configure the project.'
);
}
}
function resolveVersionBumpStrategy(autoIncrement: AndroidVersionAutoIncrement): BumpStrategy {
if (autoIncrement === true) {
return BumpStrategy.VERSION_CODE;
} else if (autoIncrement === false) {
return BumpStrategy.NOOP;
} else if (autoIncrement === 'versionCode') {
return BumpStrategy.VERSION_CODE;
} else {
return BumpStrategy.APP_VERSION;
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
}
}
@@ -17,14 +17,15 @@ export function transformJob(job: Android.Job): AndroidJobInput {
artifactPath: job.artifactPath,
username: job.username,
buildType: job.buildType && transformBuildType(job.buildType),
developmentClient: job.developmentClient,
};
}
function transformBuildType(buildType: Android.BuildType): AndroidBuildType {
if (buildType === Android.BuildType.APK) {
return AndroidBuildType.Apk;
} else {
} else if (buildType === Android.BuildType.APP_BUNDLE) {
return AndroidBuildType.AppBundle;
} else {
return AndroidBuildType.DevelopmentClient;
}
}
@@ -1,12 +1,11 @@
import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
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 {
@@ -26,8 +25,7 @@ export async function prepareJobAsync(
): Promise<Job> {
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildProfile: AndroidBuildProfile = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
const { credentials } = jobData;
const buildCredentials = credentials
? {
@@ -53,7 +51,7 @@ export async function prepareJobAsync(
projectRootDirectory,
projectArchive: jobData.projectArchive,
builderEnvironment: {
image: buildProfile.image,
image: buildProfile.image ?? 'default',
node: buildProfile.node,
yarn: buildProfile.yarn,
ndk: buildProfile.ndk,
@@ -70,11 +68,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);
@@ -2,89 +2,13 @@ import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import Log from '../../log';
import {
getAppBuildGradleAsync,
parseGradleCommand,
resolveConfigValue,
} from '../../project/android/gradleUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
export enum BumpStrategy {
APP_VERSION,
VERSION_CODE,
NOOP,
}
export async function bumpVersionAsync({
bumpStrategy,
projectDir,
exp,
}: {
projectDir: string;
exp: ExpoConfig;
bumpStrategy: BumpStrategy;
}): Promise<void> {
if (bumpStrategy === BumpStrategy.NOOP) {
return;
}
ensureStaticConfigExists(projectDir);
const buildGradle = await getAppBuildGradleAsync(projectDir);
const isMultiFlavor =
buildGradle.android?.productFlavors || buildGradle.android?.flavorDimensions;
if (isMultiFlavor) {
throw new Error(
'Automatic version bumping is not supported for multi-flavor Android projects.'
);
}
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
Log.log('Updated versions in app.json');
await writeVersionsToBuildGradleAsync({
projectDir,
exp,
});
Log.log('Synchronized versions with build gradle');
}
export async function bumpVersionInAppJsonAsync({
bumpStrategy,
projectDir,
exp,
}: {
bumpStrategy: BumpStrategy;
projectDir: string;
exp: ExpoConfig;
}): Promise<void> {
if (bumpStrategy === BumpStrategy.NOOP) {
return;
}
ensureStaticConfigExists(projectDir);
Log.addNewLineIfNone();
if (bumpStrategy === BumpStrategy.APP_VERSION) {
const appVersion = AndroidConfig.Version.getVersionName(exp) ?? '1.0.0';
await bumpAppVersionAsync({ appVersion, projectDir, exp });
} else {
const versionCode = AndroidConfig.Version.getVersionCode(exp);
const bumpedVersionCode = versionCode + 1;
Log.log(
`Bumping ${chalk.bold('expo.android.versionCode')} from ${chalk.bold(
versionCode
)} to ${chalk.bold(bumpedVersionCode)}`
);
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.android = { ...config.android, versionCode: bumpedVersionCode };
});
}
}
export async function maybeResolveVersionsAsync(
projectDir: string,
@@ -115,39 +39,3 @@ export async function maybeResolveVersionsAsync(
};
}
}
async function writeVersionsToBuildGradleAsync({
projectDir,
exp,
}: {
projectDir: string;
exp: ExpoConfig;
}): Promise<string> {
const buildGradle = await readBuildGradleAsync(projectDir);
if (!buildGradle) {
throw new Error('This project is missing a build.gradle file.');
}
let updatedBuildGradle = AndroidConfig.Version.setVersionName(exp, buildGradle);
updatedBuildGradle = AndroidConfig.Version.setVersionCode(exp, updatedBuildGradle);
await writeBuildGradleAsync({ projectDir, buildGradle: updatedBuildGradle });
return updatedBuildGradle;
}
async function readBuildGradleAsync(projectDir: string): Promise<string | undefined> {
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
if (!(await fs.pathExists(buildGradlePath))) {
return undefined;
}
return await fs.readFile(buildGradlePath, 'utf8');
}
async function writeBuildGradleAsync({
projectDir,
buildGradle,
}: {
projectDir: string;
buildGradle: string;
}): Promise<void> {
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
await fs.writeFile(buildGradlePath, buildGradle);
}
+32 -38
View File
@@ -2,6 +2,7 @@ import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo
import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
import { BuildResult } from '../graphql/mutations/BuildMutation';
@@ -13,7 +14,7 @@ import { uploadAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
import { getVcsClient } from '../vcs';
import vcs from '../vcs';
import { BuildContext } from './context';
import { runLocalBuildAsync } from './local';
import { MetadataContext, collectMetadataAsync } from './metadata';
@@ -68,7 +69,7 @@ export async function prepareBuildRequestForPlatformAsync<
});
}
if (await getVcsClient().isCommitRequiredAsync()) {
if (await vcs.hasUncommittedChangesAsync()) {
Log.addNewLineIfNone();
await reviewAndCommitChangesAsync(
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
@@ -244,46 +245,39 @@ export async function waitForBuildEndAsync(
try {
return await BuildQuery.byIdAsync(buildId, { useCache: false });
} catch (err) {
Log.debug('Failed to fetch the build status', err);
return null;
}
})
);
if (builds.length === 1) {
const [build] = builds;
if (build !== null) {
switch (build.status) {
case BuildStatus.Finished:
spinner.succeed('Build finished');
const build = nullthrows(builds[0]);
switch (build.status) {
case BuildStatus.Finished:
spinner.succeed('Build finished');
return builds;
case BuildStatus.New:
spinner.text = 'Build created';
break;
case BuildStatus.InQueue:
spinner.text = 'Build queued...';
break;
case BuildStatus.Canceled:
spinner.text = 'Build canceled';
spinner.stopAndPersist();
return builds;
case BuildStatus.InProgress:
spinner.text = 'Build in progress...';
break;
case BuildStatus.Errored:
spinner.fail('Build failed');
if (build.error) {
return builds;
case BuildStatus.New:
spinner.text = 'Build created';
break;
case BuildStatus.InQueue:
spinner.text = 'Build queued...';
break;
case BuildStatus.Canceled:
spinner.text = 'Build canceled';
spinner.stopAndPersist();
return builds;
case BuildStatus.InProgress:
spinner.text = 'Build in progress...';
break;
case BuildStatus.Errored:
spinner.fail('Build failed');
if (build.error) {
return builds;
} else {
throw new Error(`Standalone build failed!`);
}
default:
spinner.warn('Unknown status');
throw new Error(`Unknown build status: ${build.status} - aborting!`);
}
} else {
if (!spinner.text) {
spinner.text = 'Could not fetch the build status. Check your network connection.';
}
} else {
throw new Error(`Standalone build failed!`);
}
default:
spinner.warn('Unknown status.');
throw new Error(`Unknown status: ${builds} - aborting!`);
}
} else {
if (builds.filter(build => build?.status === BuildStatus.Finished).length === builds.length) {
@@ -307,7 +301,7 @@ export async function waitForBuildEndAsync(
const errored = builds.filter(build => build?.status === BuildStatus.Errored).length;
const finished = builds.filter(build => build?.status === BuildStatus.Finished).length;
const canceled = builds.filter(build => build?.status === BuildStatus.Canceled).length;
const unknown = builds.length - newBuilds - inQueue - inProgress - errored - finished;
const unknownState = builds.length - newBuilds - inQueue - inProgress - errored - finished;
spinner.text = [
newBuilds && `Builds created: ${newBuilds}`,
inQueue && `Builds in queue: ${inQueue}`,
@@ -315,7 +309,7 @@ export async function waitForBuildEndAsync(
canceled && `Builds canceled: ${canceled}`,
errored && chalk.red(`Builds failed: ${errored}`),
finished && chalk.green(`Builds finished: ${finished}`),
unknown && chalk.red(`Builds with unknown status: ${unknown}`),
unknownState && chalk.red(`Builds in unknown state: ${unknownState}`),
]
.filter(i => i)
.join('\t');
+13 -26
View File
@@ -10,8 +10,7 @@ import { RequestedPlatform } from '../platform';
import { resolveWorkflowAsync } from '../project/workflow';
import { confirmAsync, promptAsync } from '../prompts';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs';
import vcs from '../vcs';
import { configureAndroidAsync } from './android/configure';
import { ConfigureContext } from './context';
import { configureIosAsync } from './ios/configure';
@@ -38,7 +37,7 @@ export async function ensureProjectConfiguredAsync(
projectDir,
platform: requestedPlatform,
});
if (await getVcsClient().isCommitRequiredAsync()) {
if (await vcs.hasUncommittedChangesAsync()) {
error(
'Build process requires clean working tree, please commit all your changes and run `eas build` again',
{ exit: 1 }
@@ -58,7 +57,7 @@ export async function configureAsync(options: {
platform: RequestedPlatform;
projectDir: string;
}): Promise<void> {
await getVcsClient().ensureRepoExistsAsync();
await vcs.ensureRepoExistsAsync();
await maybeBailOnRepoStatusAsync();
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
@@ -87,39 +86,31 @@ export async function configureAsync(options: {
await configureIosAsync(ctx);
}
if (await getVcsClient().isCommitRequiredAsync()) {
if (await vcs.hasUncommittedChangesAsync()) {
Log.newLine();
await reviewAndCommitChangesAsync(configureCommitMessage[options.platform]);
} else if (!(await getVcsClient().hasUncommittedChangesAsync())) {
} else {
Log.newLine();
Log.withTick('No changes were necessary, the project is already configured correctly.');
}
}
const EAS_JSON_MANAGED_DEFAULT: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
},
build: {
release: {},
development: {
developmentClient: true,
distribution: 'internal',
},
preview: {
distribution: 'internal',
},
production: {},
},
submit: {
production: {},
release: {},
},
};
const EAS_JSON_BARE_DEFAULT: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
},
const EAS_JSON_GENERIC_DEFAULT: EasJson = {
build: {
release: {},
development: {
distribution: 'internal',
android: {
@@ -129,13 +120,9 @@ const EAS_JSON_BARE_DEFAULT: EasJson = {
buildConfiguration: 'Debug',
},
},
preview: {
distribution: 'internal',
},
production: {},
},
submit: {
production: {},
release: {},
},
};
@@ -152,11 +139,11 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
const easJson =
ctx.hasAndroidNativeProject && ctx.hasIosNativeProject
? EAS_JSON_BARE_DEFAULT
? EAS_JSON_GENERIC_DEFAULT
: EAS_JSON_MANAGED_DEFAULT;
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
await getVcsClient().trackFileAsync(easJsonPath);
await vcs.trackFileAsync(easJsonPath);
Log.withTick('Generated eas.json');
}
@@ -190,7 +177,7 @@ async function reviewAndCommitChangesAsync(
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await getVcsClient().showDiffAsync();
await vcs.showDiffAsync();
await reviewAndCommitChangesAsync(initialCommitMessage, false);
}
}
+1 -12
View File
@@ -5,7 +5,6 @@ import JsonFile from '@expo/json-file';
import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
import { CredentialsContext } from '../credentials/context';
import { RequestedPlatform } from '../platform';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
@@ -32,7 +31,6 @@ export interface BuildContext<T extends Platform> {
buildProfile: BuildProfile<T>;
buildProfileName: string;
clearCache: boolean;
credentialsCtx: CredentialsContext;
exp: ExpoConfig;
local: boolean;
nonInteractive: boolean;
@@ -71,16 +69,9 @@ export async function createBuildContextAsync<T extends Platform>({
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,
@@ -96,13 +87,11 @@ export async function createBuildContextAsync<T extends Platform>({
...devClientProperties,
};
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
return {
accountName,
buildProfile,
buildProfileName,
clearCache,
credentialsCtx,
exp,
local,
nonInteractive,
@@ -1,16 +1,26 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import fs from 'fs-extra';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { getVcsClient } from '../../vcs';
import vcs from '../../vcs';
import { ensureValidVersions } from '../utils/updates';
import { readPlistAsync, writePlistAsync } from './plist';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
let xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, xcodeProject)) {
xcodeProject = IOSConfig.Updates.ensureBundleReactNativePhaseContainsConfigurationScript(
projectDir,
xcodeProject
);
await fs.writeFile(IOSConfig.Paths.getPBXProjectPath(projectDir), xcodeProject.writeSync());
}
let expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
@@ -27,7 +37,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 +58,15 @@ export async function syncUpdatesConfigurationAsync(
}
}
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in Expo.plist
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
const xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, xcodeProject)) {
const script = 'expo-updates/scripts/create-manifest-ios.sh';
const buildPhase = '"Bundle React Native code and images"';
throw new Error(`Path to ${script} is missing in a ${buildPhase} build phase.`);
}
const expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistConfigurationSet(expoPlist)) {
throw new Error('Missing values in Expo.plist');
@@ -68,7 +84,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> {
@@ -6,17 +6,17 @@ import { readChannelSafelyAsync } from '../UpdatesModule';
jest.mock('fs');
afterAll(async () => {
afterAll(() => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.remove(os.tmpdir());
fs.removeSync(os.tmpdir());
});
beforeEach(async () => {
beforeEach(() => {
vol.reset();
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.mkdirp(os.tmpdir());
fs.mkdirpSync(os.tmpdir());
});
const testChannel = 'test-channel';
@@ -1,148 +1,37 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { readPlistAsync } from '../plist';
import {
BumpStrategy,
bumpVersionAsync,
bumpVersionInAppJsonAsync,
evaluateTemplateString,
getInfoPlistPath,
readBuildNumberAsync,
readShortVersionAsync,
} from '../version';
jest.mock('fs');
afterAll(async () => {
afterAll(() => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.remove(os.tmpdir());
fs.removeSync(os.tmpdir());
});
beforeEach(async () => {
beforeEach(() => {
vol.reset();
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.mkdirp(os.tmpdir());
fs.mkdirpSync(os.tmpdir());
});
describe(evaluateTemplateString, () => {
it('evaluates the template string when value is a number', () => {
it('evaluates the template string', () => {
expect(evaluateTemplateString('$(BLAH_BLAH)', { BLAH_BLAH: 123 })).toBe('123');
});
it('evaluates the template string when value is a string', () => {
expect(evaluateTemplateString('$(BLAH_BLAH)', { BLAH_BLAH: '123' })).toBe('123');
});
it('evaluates the template string when template is not the only element', () => {
expect(evaluateTemplateString('before$(BLAH_BLAH)after', { BLAH_BLAH: '123' })).toBe(
'before123after'
);
});
it('evaluates the template string when template value is double quoted', () => {
expect(evaluateTemplateString('before$(BLAH_BLAH)after', { BLAH_BLAH: '"123"' })).toBe(
'before123after'
);
});
});
// bare workflow
describe(bumpVersionAsync, () => {
it('bumps expo.ios.buildNumber and CFBundleVersion when strategy = BumpStrategy.BUILD_NUMBER', async () => {
const fakeExp = initBareWorkflowProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
projectDir: '/repo',
exp: fakeExp,
buildSettings: {},
});
const appJSON = await fs.readJSON('/repo/app.json');
const infoPlist = (await readPlistAsync(
'/repo/ios/myproject/Info.plist'
)) as IOSConfig.InfoPlist;
expect(fakeExp.version).toBe('1.0.0');
expect(fakeExp.ios?.buildNumber).toBe('2');
expect(appJSON.expo.version).toBe('1.0.0');
expect(appJSON.expo.ios.buildNumber).toBe('2');
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
expect(infoPlist['CFBundleVersion']).toBe('2');
});
it('bumps expo.ios.buildNumber and CFBundleVersion for non default Info.plist location', async () => {
const fakeExp = initBareWorkflowProject({ infoPlistName: 'Info2.plist' });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
projectDir: '/repo',
exp: fakeExp,
buildSettings: {
INFOPLIST_FILE: '$(SRCROOT)/myproject/Info2.plist',
},
});
const appJSON = await fs.readJSON('/repo/app.json');
const infoPlist = (await readPlistAsync(
'/repo/ios/myproject/Info2.plist'
)) as IOSConfig.InfoPlist;
expect(fakeExp.version).toBe('1.0.0');
expect(fakeExp.ios?.buildNumber).toBe('2');
expect(appJSON.expo.version).toBe('1.0.0');
expect(appJSON.expo.ios.buildNumber).toBe('2');
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
expect(infoPlist['CFBundleVersion']).toBe('2');
});
it('bumps expo.version and CFBundleShortVersionString when strategy = BumpStrategy.SHORT_VERSION', async () => {
const fakeExp = initBareWorkflowProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
projectDir: '/repo',
exp: fakeExp,
buildSettings: {},
});
const appJSON = await fs.readJSON('/repo/app.json');
const infoPlist = (await readPlistAsync(
'/repo/ios/myproject/Info.plist'
)) as IOSConfig.InfoPlist;
expect(fakeExp.version).toBe('1.0.1');
expect(fakeExp.ios?.buildNumber).toBe('1');
expect(appJSON.expo.version).toBe('1.0.1');
expect(appJSON.expo.ios.buildNumber).toBe('1');
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.1');
expect(infoPlist['CFBundleVersion']).toBe('1');
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initBareWorkflowProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
projectDir: '/repo',
exp: fakeExp,
buildSettings: {},
});
const appJSON = await fs.readJSON('/repo/app.json');
const infoPlist = (await readPlistAsync(
'/repo/ios/myproject/Info.plist'
)) as IOSConfig.InfoPlist;
expect(fakeExp.version).toBe('1.0.0');
expect(fakeExp.ios?.buildNumber).toBe('1');
expect(appJSON.expo.version).toBe('1.0.0');
expect(appJSON.expo.ios.buildNumber).toBe('1');
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
expect(infoPlist['CFBundleVersion']).toBe('1');
});
});
// managed workflow
describe(bumpVersionInAppJsonAsync, () => {
it('bumps expo.ios.buildNumber when strategy = BumpStrategy.BUILD_NUMBER', async () => {
const fakeExp = initManagedProject();
@@ -164,7 +53,7 @@ describe(bumpVersionInAppJsonAsync, () => {
const fakeExp = initManagedProject();
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
bumpStrategy: BumpStrategy.SHORT_VERSION,
projectDir: '/repo',
exp: fakeExp,
});
@@ -194,9 +83,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,15 +101,15 @@ describe(readBuildNumberAsync, () => {
});
describe(readShortVersionAsync, () => {
describe('bare project', () => {
describe('generic project', () => {
it('reads the short version from native code', async () => {
const exp = initBareWorkflowProject();
const appVersion = await readShortVersionAsync('/repo', exp, {});
expect(appVersion).toBe('1.0.0');
const exp = initGenericProject();
const shortVersion = await readShortVersionAsync('/repo', exp, {});
expect(shortVersion).toBe('1.0.0');
});
it('evaluates interpolated build number', async () => {
const exp = initBareWorkflowProject({
appVersion: '$(CURRENT_PROJECT_VERSION)',
const exp = initGenericProject({
shortVersion: '$(CURRENT_PROJECT_VERSION)',
});
const buildNumber = await readShortVersionAsync('/repo', exp, {
CURRENT_PROJECT_VERSION: '1.0.0',
@@ -231,85 +120,33 @@ describe(readShortVersionAsync, () => {
describe('managed project', () => {
it('reads the version from app config', async () => {
const exp = initBareWorkflowProject();
const appVersion = await readShortVersionAsync('/repo', exp, {});
expect(appVersion).toBe('1.0.0');
const exp = initGenericProject();
const shortVersion = await readShortVersionAsync('/repo', exp, {});
expect(shortVersion).toBe('1.0.0');
});
});
});
describe(getInfoPlistPath, () => {
it('returns default path if INFOPLIST_FILE is not specified', () => {
vol.fromJSON(
{
'./ios/testapp/Info.plist': '',
},
'/repo'
);
const plistPath = getInfoPlistPath('/repo', {});
expect(plistPath).toBe('/repo/ios/testapp/Info.plist');
});
it('returns INFOPLIST_FILE if specified', () => {
vol.fromJSON(
{
'./ios/testapp/Info.plist': '',
},
'/repo'
);
const plistPath = getInfoPlistPath('/repo', { INFOPLIST_FILE: './qwert/NotInfo.plist' });
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
});
it('evaluates SRCROOT in Info.plist', () => {
vol.fromJSON(
{
'./ios/testapp/Info.plist': '',
},
'/repo'
);
const plistPath = getInfoPlistPath('/repo', {
INFOPLIST_FILE: '$(SRCROOT)/qwert/NotInfo.plist',
});
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
});
it('evaluates BuildSettings in Info.plist', () => {
vol.fromJSON(
{
'./ios/testapp/Info.plist': '',
},
'/repo'
);
const plistPath = getInfoPlistPath('/repo', {
INFOPLIST_FILE: '$(SRCROOT)/qwert/$(TARGET_NAME).plist',
TARGET_NAME: 'NotInfo',
});
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
});
});
function initBareWorkflowProject({
appVersion = '1.0.0',
function initGenericProject({
shortVersion = '1.0.0',
version = '1',
infoPlistName = 'Info.plist',
}: { appVersion?: string; version?: string; infoPlistName?: string } = {}): ExpoConfig {
const fakeExp: ExpoConfig = {
name: 'myproject',
slug: 'myproject',
version: '1.0.0',
ios: {
buildNumber: '1',
},
};
}: { shortVersion?: string; version?: string } = {}): ExpoConfig {
vol.fromJSON(
{
'./app.json': JSON.stringify({
expo: fakeExp,
expo: {
version: '1.0.0',
ios: {
buildNumber: '1',
},
},
}),
[`./ios/myproject/${infoPlistName}`]: `<?xml version="1.0" encoding="UTF-8"?>
'./ios/myproject/Info.plist': `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleShortVersionString</key>
<string>${appVersion}</string>
<string>${shortVersion}</string>
<key>CFBundleVersion</key>
<string>${version}</string>
</dict>
@@ -318,10 +155,6 @@ function initBareWorkflowProject({
'/repo'
);
return fakeExp;
}
function initManagedProject(): ExpoConfig {
const fakeExp: ExpoConfig = {
name: 'myproject',
slug: 'myproject',
@@ -330,15 +163,31 @@ function initManagedProject(): ExpoConfig {
buildNumber: '1',
},
};
return fakeExp;
}
function initManagedProject(): ExpoConfig {
vol.fromJSON(
{
'./app.json': JSON.stringify({
expo: fakeExp,
expo: {
version: '1.0.0',
ios: {
buildNumber: '1',
},
},
}),
},
'/repo'
);
const fakeExp: ExpoConfig = {
name: 'myproject',
slug: 'myproject',
version: '1.0.0',
ios: {
buildNumber: '1',
},
};
return fakeExp;
}
+16 -32
View File
@@ -1,8 +1,7 @@
import { IOSConfig } from '@expo/config-plugins';
import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import type { XCBuildConfiguration } from 'xcode';
import { IosCredentials, Target } from '../../credentials/ios/types';
import { IosCredentials } from '../../credentials/ios/types';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
@@ -17,12 +16,6 @@ import { ensureIosCredentialsAsync } from './credentials';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
interface BuildConfiguration {
targetName?: string;
buildConfiguration?: string;
buildSettings: XCBuildConfiguration['buildSettings'];
}
export async function prepareIosBuildAsync(
ctx: BuildContext<Platform.IOS>
): Promise<BuildRequestSender> {
@@ -50,7 +43,6 @@ export async function prepareIosBuildAsync(
},
xcodeBuildContext
);
const buildConfiguration = resolveBuildConfiguration(ctx, targets);
return await prepareBuildRequestForPlatformAsync({
ctx,
@@ -62,11 +54,24 @@ export async function prepareIosBuildAsync(
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile,
buildSettings: buildConfiguration.buildSettings,
});
},
getMetadataContext: (): IosMetadataContext => {
return buildConfiguration;
if (ctx.workflow === Workflow.MANAGED) {
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,
});
const buildSettings = xcBuildConfiguration?.buildSettings ?? {};
return {
buildSettings,
targetName: applicationTarget.targetName,
buildConfiguration: applicationTarget.buildConfiguration,
};
},
prepareJobAsync: async (
ctx: BuildContext<Platform.IOS>,
@@ -89,24 +94,3 @@ export async function prepareIosBuildAsync(
},
});
}
function resolveBuildConfiguration(
ctx: BuildContext<Platform.IOS>,
targets: Target[]
): BuildConfiguration {
if (ctx.workflow === Workflow.MANAGED) {
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,
});
const buildSettings = xcBuildConfiguration?.buildSettings ?? {};
return {
buildSettings,
targetName: applicationTarget.targetName,
buildConfiguration: applicationTarget.buildConfiguration,
};
}
+6 -10
View File
@@ -1,18 +1,17 @@
import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { IosBuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import { IosBuildProfile, VersionAutoIncrement } from '@expo/eas-json';
import Log from '../../log';
import {
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject,
warnIfBundleIdentifierDefinedInAppConfigForGenericProject,
} from '../../project/ios/bundleIdentifier';
import { resolveWorkflowAsync } from '../../project/workflow';
import { ConfigureContext } from '../context';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
import { BumpStrategy, bumpVersionInAppJsonAsync } from './version';
export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
if (!ctx.hasIosNativeProject) {
@@ -20,7 +19,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);
@@ -32,12 +31,10 @@ export async function validateAndSyncProjectConfigurationAsync({
projectDir,
exp,
buildProfile,
buildSettings,
}: {
projectDir: string;
exp: ExpoConfig;
buildProfile: IosBuildProfile;
buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const { autoIncrement } = buildProfile;
@@ -47,13 +44,12 @@ export async function validateAndSyncProjectConfigurationAsync({
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, buildSettings });
} else {
await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
}
}
function resolveVersionBumpStrategy(autoIncrement: IosVersionAutoIncrement): BumpStrategy {
function resolveVersionBumpStrategy(autoIncrement: VersionAutoIncrement): BumpStrategy {
if (autoIncrement === true) {
return BumpStrategy.BUILD_NUMBER;
} else if (autoIncrement === false) {
@@ -61,6 +57,6 @@ function resolveVersionBumpStrategy(autoIncrement: IosVersionAutoIncrement): Bum
} else if (autoIncrement === 'buildNumber') {
return BumpStrategy.BUILD_NUMBER;
} else {
return BumpStrategy.APP_VERSION;
return BumpStrategy.SHORT_VERSION;
}
}
@@ -1,5 +1,6 @@
import { Platform } from '@expo/eas-build-job';
import { createCredentialsContextAsync } from '../../credentials/context';
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
import { resolveEntitlementsJsonAsync } from '../../credentials/ios/appstore/entitlements';
@@ -16,8 +17,13 @@ export async function ensureIosCredentialsAsync(
return;
}
const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, {
app: getAppFromContext(buildCtx.credentialsCtx),
const credentialsCtx = await createCredentialsContextAsync(buildCtx.projectDir, {
exp: buildCtx.exp,
nonInteractive: buildCtx.nonInteractive,
});
const provider = new IosCredentialsProvider(credentialsCtx, {
app: getAppFromContext(credentialsCtx),
targets,
iosCapabilitiesOptions: {
entitlements: await resolveEntitlementsJsonAsync(buildCtx.projectDir, buildCtx.workflow),
+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;
}
}
+21 -7
View File
@@ -1,11 +1,11 @@
import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
import path from 'path';
import slash from 'slash';
import semver from 'semver';
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 {
@@ -24,8 +24,7 @@ export async function prepareJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData
): Promise<Job> {
const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
if (jobData.credentials) {
@@ -40,8 +39,9 @@ export async function prepareJobAsync(
platform: Platform.IOS,
projectArchive: jobData.projectArchive,
projectRootDirectory,
distribution: ctx.buildProfile.simulator ? 'simulator' : ctx.buildProfile.distribution,
builderEnvironment: {
image: ctx.buildProfile.image,
image: resolveImage(ctx),
node: ctx.buildProfile.node,
yarn: ctx.buildProfile.yarn,
bundler: ctx.buildProfile.bundler,
@@ -60,16 +60,30 @@ 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);
}
function resolveImage(ctx: BuildContext<Platform.IOS>): Ios.BuilderEnvironment['image'] {
// see https://linear.app/expo/issue/ENG-1396/make-default-image-dependent-on-sdk-version
if (!ctx.buildProfile.image && ctx.exp.sdkVersion) {
const majorSdkVersion = semver.major(ctx.exp.sdkVersion);
if (majorSdkVersion <= 41) {
return 'macos-catalina-10.15-xcode-12.4';
}
}
return ctx.buildProfile.image ?? 'default';
}
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
return {
provisioningProfileBase64: targetCredentials.provisioningProfile,
+32 -93
View File
@@ -2,44 +2,22 @@ import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import chalk from 'chalk';
import path from 'path';
import nullthrows from 'nullthrows';
import semver from 'semver';
import type { XCBuildConfiguration } from 'xcode';
import Log from '../../log';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync } from '../utils/version';
import { readPlistAsync, writePlistAsync } from './plist';
import { readPlistAsync } from './plist';
export enum BumpStrategy {
APP_VERSION,
SHORT_VERSION,
BUILD_NUMBER,
NOOP,
}
export async function bumpVersionAsync({
bumpStrategy,
projectDir,
exp,
buildSettings,
}: {
projectDir: string;
exp: ExpoConfig;
bumpStrategy: BumpStrategy;
buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<void> {
if (bumpStrategy === BumpStrategy.NOOP) {
return;
}
ensureStaticConfigExists(projectDir);
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
Log.log('Updated versions in app.json');
await writeVersionsToInfoPlistAsync({ projectDir, exp, infoPlist, buildSettings });
Log.log('Synchronized versions with Info.plist');
}
export async function bumpVersionInAppJsonAsync({
bumpStrategy,
projectDir,
@@ -54,9 +32,29 @@ export async function bumpVersionInAppJsonAsync({
}
ensureStaticConfigExists(projectDir);
Log.addNewLineIfNone();
if (bumpStrategy === BumpStrategy.APP_VERSION) {
const appVersion = IOSConfig.Version.getVersion(exp);
await bumpAppVersionAsync({ appVersion, projectDir, exp });
if (bumpStrategy === BumpStrategy.SHORT_VERSION) {
const shortVersion = IOSConfig.Version.getVersion(exp);
if (semver.valid(shortVersion)) {
const bumpedShortVersion = nullthrows(semver.inc(shortVersion, 'patch'));
Log.log(
`Bumping ${chalk.bold('expo.version')} from ${chalk.bold(shortVersion)} to ${chalk.bold(
bumpedShortVersion
)}`
);
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.version = bumpedShortVersion;
});
} else {
Log.log(`${chalk.bold('expo.version')} = ${chalk.bold(shortVersion)} is not a valid semver`);
const { bumpedShortVersion } = await promptAsync({
type: 'text',
name: 'bumpedShortVersion',
message: 'What is the next version?',
});
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.version = bumpedShortVersion;
});
}
} else {
const buildNumber = IOSConfig.Version.getBuildNumber(exp);
if (buildNumber.match(/^\d+(\.\d+)*$/)) {
@@ -92,7 +90,7 @@ export async function readShortVersionAsync(
): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
const infoPlist = await readInfoPlistAsync(projectDir);
return (
infoPlist.CFBundleShortVersionString &&
evaluateTemplateString(infoPlist.CFBundleShortVersionString, buildSettings)
@@ -109,7 +107,7 @@ export async function readBuildNumberAsync(
): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
const infoPlist = await readInfoPlistAsync(projectDir);
return (
infoPlist.CFBundleVersion && evaluateTemplateString(infoPlist.CFBundleVersion, buildSettings)
);
@@ -133,63 +131,11 @@ export async function maybeResolveVersionsAsync(
}
}
async function writeVersionsToInfoPlistAsync({
projectDir,
exp,
infoPlist,
buildSettings,
}: {
projectDir: string;
exp: ExpoConfig;
infoPlist: IOSConfig.InfoPlist;
buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<IOSConfig.InfoPlist> {
let updatedInfoPlist = IOSConfig.Version.setVersion(exp, infoPlist);
updatedInfoPlist = IOSConfig.Version.setBuildNumber(exp, updatedInfoPlist);
await writeInfoPlistAsync({ projectDir, infoPlist: updatedInfoPlist, buildSettings });
return updatedInfoPlist;
}
export function getInfoPlistPath(
projectDir: string,
buildSettings: XCBuildConfiguration['buildSettings']
): string {
if (buildSettings.INFOPLIST_FILE) {
const infoPlistFile = buildSettings.INFOPLIST_FILE.startsWith('"')
? buildSettings.INFOPLIST_FILE.slice(1, -1)
: buildSettings.INFOPLIST_FILE;
const iosDir = path.join(projectDir, 'ios');
const plistPath = evaluateTemplateString(infoPlistFile, {
...buildSettings,
SRCROOT: iosDir,
});
return path.isAbsolute(plistPath) ? plistPath : path.resolve(iosDir, plistPath);
} else {
return IOSConfig.Paths.getInfoPlistPath(projectDir);
}
}
async function readInfoPlistAsync(
projectDir: string,
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<IOSConfig.InfoPlist> {
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
async function readInfoPlistAsync(projectDir: string): Promise<IOSConfig.InfoPlist> {
const infoPlistPath = IOSConfig.Paths.getInfoPlistPath(projectDir);
return (await readPlistAsync(infoPlistPath)) as IOSConfig.InfoPlist;
}
async function writeInfoPlistAsync({
projectDir,
infoPlist,
buildSettings,
}: {
projectDir: string;
infoPlist: IOSConfig.InfoPlist;
buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<void> {
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
await writePlistAsync(infoPlistPath, infoPlist);
}
function ensureStaticConfigExists(projectDir: string): void {
const paths = getConfigFilePaths(projectDir);
if (!paths.staticConfigPath) {
@@ -198,12 +144,5 @@ function ensureStaticConfigExists(projectDir: string): void {
}
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]);
return value.startsWith('"') ? value.slice(1, -1) : value;
} else {
return match;
}
});
return s.replace(/\$\((\w+)\)/g, (match, key) => (vars.hasOwnProperty(key) ? vars[key] : match));
}
+10 -5
View File
@@ -7,8 +7,7 @@ import { GradleBuildContext } from '../project/android/gradle';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
import { getUsername } from '../project/projectUtils';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs';
import vcs from '../vcs';
import {
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
@@ -22,6 +21,13 @@ import {
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
import { isExpoUpdatesInstalled } from './utils/updates';
/**
* We use require() to exclude package.json from TypeScript's analysis since it lives outside
* the src directory and would change the directory structure of the emitted files
* under the build directory
*/
const packageJSON = require('../../package.json');
export type MetadataContext<T extends Platform> = T extends Platform.ANDROID
? AndroidMetadataContext
: IosMetadataContext;
@@ -47,7 +53,7 @@ export async function collectMetadataAsync<T extends Platform>(
const metadata = {
trackingContext: ctx.trackingCtx,
...(await maybeResolveVersionsAsync(ctx, platformContext)),
cliVersion: easCliVersion,
cliVersion: packageJSON.version,
workflow: ctx.workflow,
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
@@ -57,8 +63,7 @@ export async function collectMetadataAsync<T extends Platform>(
appName: ctx.exp.name,
appIdentifier: await resolveAppIdentifierAsync(ctx, platformContext),
buildProfile: ctx.buildProfileName,
gitCommitHash: await getVcsClient().getCommitHashAsync(),
isGitWorkingTreeDirty: await getVcsClient().hasUncommittedChangesAsync(),
gitCommitHash: await vcs.getCommitHashAsync(),
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
+3 -12
View File
@@ -1,6 +1,6 @@
import * as ExpoConfig from '@expo/config';
import JsonFile from '@expo/json-file';
import assert from 'assert';
import fs from 'fs-extra';
export async function updateAppJsonConfigAsync(
{
@@ -15,19 +15,10 @@ export async function updateAppJsonConfigAsync(
const paths = ExpoConfig.getConfigFilePaths(projectDir);
assert(paths.staticConfigPath, "can't update dynamic config");
const rawStaticConfig = readAppJson(paths.staticConfigPath);
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
rawStaticConfig.expo = rawStaticConfig.expo ?? {};
modifyConfig(rawStaticConfig.expo);
await JsonFile.writeAsync(paths.staticConfigPath, rawStaticConfig, { json5: false });
await fs.writeJson(paths.staticConfigPath, rawStaticConfig, { spaces: 2 });
modifyConfig(exp);
}
// TODO: remove this once @expo/config exports getStaticConfig
export function readAppJson(appJsonPath: string): any {
const config = JsonFile.read(appJsonPath, { json5: true });
if (config) {
return config as any;
}
throw new Error(`Failed to read app.json`);
}
+24 -24
View File
@@ -1,5 +1,5 @@
import { Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import resolveFrom from 'resolve-from';
@@ -9,38 +9,40 @@ import Log, { learnMore } from '../../log';
import { appPlatformDisplayNames } from '../../platform';
import { resolveWorkflowAsync } from '../../project/workflow';
import { confirmAsync } from '../../prompts';
import { expoCommandAsync } from '../../utils/expoCli';
import { ProfileData } from '../../utils/profiles';
import { getVcsClient } from '../../vcs';
import { expoCommandAsync } from '../../utils/expoCommand';
import zipObject from '../../utils/expodash/zipObject';
import { reviewAndCommitChangesAsync } from './repository';
export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir,
platforms,
profile,
nonInteractive = false,
buildProfiles = [],
}: {
projectDir: string;
platforms: Platform[];
profile: string;
nonInteractive?: boolean;
buildProfiles?: ProfileData<BuildProfile>[];
}): Promise<void> {
if (await isExpoDevClientInstalledAsync(projectDir)) {
return;
}
const buildProfilesWithDevelopmentClientRequired = buildProfiles.filter(
buildProfile => buildProfile.profile.developmentClient
const easJsonReader = new EasJsonReader(projectDir);
const devClientPerPlatformList = await Promise.all(
platforms.map(async platform => {
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, profile);
return buildProfile.developmentClient ?? false;
})
);
const isDevelopmentClientRequired = buildProfilesWithDevelopmentClientRequired.some(Boolean);
if (!isDevelopmentClientRequired) {
const isDevClientRequired = devClientPerPlatformList.some(i => i);
if (!isDevClientRequired) {
return;
}
const platformsToCheck = buildProfilesWithDevelopmentClientRequired.map(
({ platform }) => platform
);
const devClientPerPlatform = zipObject(platforms, devClientPerPlatformList);
const platformsToCheck = platforms.filter(platform => devClientPerPlatform[platform]);
const workflowPerPlatformList = await Promise.all(
platformsToCheck.map(platform => resolveWorkflowAsync(projectDir, platform))
);
@@ -65,15 +67,15 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
if (install) {
await installExpoDevClientAsync(projectDir, { nonInteractive });
} else {
error(`Install ${chalk.bold('expo-dev-client')} manually and come back later.`, {
error(`Install ${chalk.bold('expo-dev-client')} on your own and come back later.`, {
exit: 1,
});
}
} else {
Log.warn(`You'll need to install ${chalk.bold('expo-dev-client')} manually.`);
Log.warn(`Unfortunately, you need to install ${chalk.bold('expo-dev-client')} on your own.`);
Log.warn(
learnMore('https://docs.expo.dev/clients/installation/', {
learnMoreMessage: 'See installation instructions on how to do it.',
learnMoreMessage: 'See installation instructions on how to do it',
dim: false,
})
);
@@ -108,9 +110,7 @@ async function installExpoDevClientAsync(
Log.newLine();
await expoCommandAsync(projectDir, ['install', 'expo-dev-client']);
Log.newLine();
if (await getVcsClient().isCommitRequiredAsync()) {
await reviewAndCommitChangesAsync('Install expo-dev-client', {
nonInteractive,
});
}
await reviewAndCommitChangesAsync('Install expo-dev-client', {
nonInteractive,
});
}
@@ -120,5 +120,11 @@ export function formatGraphQLBuild(build: BuildFragment): string {
}
const getActorName = (build: BuildFragment): string => {
return build.initiatingActor?.displayName || 'unknown';
if (!build.initiatingActor) {
return 'unknown';
} else if (build.initiatingActor.__typename === 'User') {
return build.initiatingActor.username;
} else {
return build.initiatingActor.firstName ?? 'unknown';
}
};
@@ -24,9 +24,7 @@ export function printLogsUrls(builds: BuildFragment[]): void {
builds.forEach(build => {
const logsUrl = getBuildLogsUrl(build);
Log.log(
`${appPlatformEmojis[build.platform]} ${
appPlatformDisplayNames[build.platform]
} build details: ${chalk.underline(logsUrl)}`
`${appPlatformDisplayNames[build.platform]} build details: ${chalk.underline(logsUrl)}`
);
});
}
+7 -10
View File
@@ -10,10 +10,10 @@ import { confirmAsync, promptAsync } from '../../prompts';
import { formatBytes } from '../../utils/files';
import { getTmpDirectory } from '../../utils/paths';
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
import { getVcsClient } from '../../vcs';
import vcs from '../../vcs';
export async function maybeBailOnRepoStatusAsync(): Promise<void> {
if (!(await getVcsClient().isCommitRequiredAsync())) {
if (!(await vcs.hasUncommittedChangesAsync())) {
return;
}
Log.addNewLineIfNone();
@@ -33,7 +33,7 @@ export async function maybeBailOnRepoStatusAsync(): Promise<void> {
}
export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<void> {
if (!(await getVcsClient().isCommitRequiredAsync())) {
if (!(await vcs.hasUncommittedChangesAsync())) {
return;
}
Log.addNewLineIfNone();
@@ -70,7 +70,7 @@ export async function commitPromptAsync({
initial: initialCommitMessage,
validate: (input: string) => input !== '',
});
await getVcsClient().commitAsync({ commitAllFiles, commitMessage: message });
await vcs.commitAsync({ commitAllFiles, commitMessage: message });
}
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
@@ -95,7 +95,7 @@ export async function makeProjectTarballAsync(): Promise<{ path: string; size: n
startTimer(compressTimerLabel);
try {
await getVcsClient().makeShallowCopyAsync(shallowClonePath);
await vcs.makeShallowCopyAsync(shallowClonePath);
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
'.',
]);
@@ -133,10 +133,7 @@ export async function reviewAndCommitChangesAsync(
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
): Promise<void> {
if (process.env.EAS_BUILD_AUTOCOMMIT) {
await getVcsClient().commitAsync({
commitMessage: initialCommitMessage,
commitAllFiles: false,
});
await vcs.commitAsync({ commitMessage: initialCommitMessage, commitAllFiles: false });
Log.withTick('Committed changes.');
return;
}
@@ -169,7 +166,7 @@ export async function reviewAndCommitChangesAsync(
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes.');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await getVcsClient().showDiffAsync();
await vcs.showDiffAsync();
await reviewAndCommitChangesAsync(initialCommitMessage, {
nonInteractive,
askedFirstTime: false,
+1 -1
View File
@@ -2,7 +2,7 @@ import { ExpoConfig, getPackageJson } from '@expo/config';
export function isExpoUpdatesInstalled(projectDir: string): boolean {
const packageJson = getPackageJson(projectDir);
return !!(packageJson.dependencies && 'expo-updates' in packageJson.dependencies);
return packageJson.dependencies && 'expo-updates' in packageJson.dependencies;
}
export function ensureValidVersions(exp: ExpoConfig): void {
@@ -1,47 +0,0 @@
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import semver from 'semver';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { updateAppJsonConfigAsync } from './appJson';
export function ensureStaticConfigExists(projectDir: string): void {
const paths = getConfigFilePaths(projectDir);
if (!paths.staticConfigPath) {
throw new Error('autoIncrement option is not supported when using app.config.js');
}
}
export async function bumpAppVersionAsync({
appVersion,
projectDir,
exp,
}: {
appVersion: string;
projectDir: string;
exp: ExpoConfig;
}): Promise<void> {
let bumpedAppVersion: string;
if (semver.valid(appVersion)) {
bumpedAppVersion = nullthrows(semver.inc(appVersion, 'patch'));
Log.log(
`Bumping ${chalk.bold('expo.version')} from ${chalk.bold(appVersion)} to ${chalk.bold(
bumpedAppVersion
)}`
);
} else {
Log.log(`${chalk.bold('expo.version')} = ${chalk.bold(appVersion)} is not a valid semver`);
bumpedAppVersion = (
await promptAsync({
type: 'text',
name: 'bumpedAppVersion',
message: 'What is the next version?',
})
).bumpedAppVersion;
}
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.version = bumpedAppVersion;
});
}
+4 -4
View File
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
import path from 'path';
import Log, { learnMore } from '../log';
import { getVcsClient } from '../vcs';
import vcs from '../vcs';
import { BuildContext } from './context';
export function checkNodeEnvVariable(ctx: BuildContext<Platform>): void {
@@ -25,12 +25,12 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
if (!googleServicesFilePath) {
return;
}
const rootDir = path.normalize(await getVcsClient().getRootPathAsync());
const rootDir = path.normalize(await vcs.getRootPathAsync());
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
if (
(await fs.pathExists(absGoogleServicesFilePath)) &&
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
(await getVcsClient().isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
(await vcs.isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
) {
Log.warn(
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
@@ -41,7 +41,7 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
)}`
);
Log.warn(
`If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting your build profile's env.GOOGLE_SERVICES_FILE in eas.json to any non-empty string.`
'If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting GOOGLE_SERVICES_FILE in your build profile in eas.json to any non-falsy value.'
);
Log.newLine();
}
@@ -1,6 +1,4 @@
import { EasJsonReader } from '@expo/eas-json';
import { Command } from '@oclif/command';
import semver from 'semver';
import {
AnalyticsEvent,
@@ -8,13 +6,8 @@ import {
initAsync as initAnalyticsAsync,
logEvent,
} from '../analytics';
import Log from '../log';
import { findProjectRootAsync } from '../project/projectUtils';
import { getUserAsync } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { setVcsClient } from '../vcs';
import GitClient from '../vcs/clients/git';
export default abstract class EasCommand extends Command {
/**
@@ -27,10 +20,7 @@ export default abstract class EasCommand extends Command {
// eslint-disable-next-line async-protect/async-suffix
async run(): Promise<any> {
EasJsonReader.setLog(Log);
await initAnalyticsAsync();
await this.applyCliConfigAsync();
if (this.requiresAuthentication) {
const { flags } = this.parse();
@@ -52,18 +42,4 @@ export default abstract class EasCommand extends Command {
await flushAnalyticsAsync();
return super.finally(err);
}
private async applyCliConfigAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const easJsonReader = new EasJsonReader(projectDir);
const config = await easJsonReader.getCliConfigAsync();
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
throw new Error(
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
);
}
if (config?.requireCommit) {
setVcsClient(new GitClient());
}
}
}
@@ -1,4 +1,3 @@
import { asMock } from '../../__tests__/utils';
import { flushAsync, initAsync, logEvent } from '../../analytics';
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
import { getUserAsync } from '../../user/User';
@@ -32,15 +31,11 @@ afterAll(() => {
});
beforeEach(() => {
asMock(getUserAsync)
.mockReset()
.mockImplementation(() => mockJester);
asMock(ensureLoggedInAsync)
.mockReset()
.mockImplementation(() => mockJester);
asMock(initAsync).mockReset();
asMock(flushAsync).mockReset();
asMock(logEvent).mockReset();
(getUserAsync as jest.Mock).mockReset().mockImplementation(() => mockJester);
(ensureLoggedInAsync as jest.Mock).mockReset().mockImplementation(() => mockJester);
(initAsync as jest.Mock).mockReset();
(flushAsync as jest.Mock).mockReset();
(logEvent as jest.Mock).mockReset();
});
describe(EasCommand.name, () => {
@@ -59,7 +54,7 @@ describe(EasCommand.name, () => {
}, 15_000);
it('ensures the user data is read from cache', async () => {
asMock(TestEasCommand.prototype.authValue).mockImplementationOnce(() => false);
(TestEasCommand.prototype.authValue as jest.Mock).mockImplementationOnce(() => false);
await TestEasCommand.run();
@@ -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,
@@ -75,7 +75,11 @@ export default class BranchCreate extends EasCommand {
flags,
} = this.parse(BranchCreate);
const projectDir = await findProjectRootAsync();
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
@@ -90,8 +94,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()
);
@@ -99,7 +98,10 @@ export default class BranchDelete extends EasCommand {
flags: { json: jsonFlag },
} = this.parse(BranchDelete);
const projectDir = await findProjectRootAsync();
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
+5 -3
View File
@@ -44,8 +44,7 @@ export async function listBranchesAsync({
{
appId: projectId,
limit: BRANCHES_LIMIT,
},
{ additionalTypenames: ['UpdateBranch'] }
}
)
.toPromise()
);
@@ -68,7 +67,10 @@ export default class BranchList extends EasCommand {
async runAsync(): Promise<void> {
const { flags } = this.parse(BranchList);
const projectDir = await findProjectRootAsync();
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const branches = await listBranchesAsync({ projectId });
+62 -29
View File
@@ -1,6 +1,5 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { ExpoConfig, getConfig, getDefaultTarget } from '@expo/config';
import { getRuntimeVersionForSDKVersion } from '@expo/sdk-runtime-versions';
import { flags } from '@oclif/command';
import assert from 'assert';
import chalk from 'chalk';
@@ -26,15 +25,14 @@ import {
PublishPlatform,
buildBundlesAsync,
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
collectAssets,
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync, selectAsync } from '../../prompts';
import { formatUpdate } from '../../update/utils';
import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
import { getVcsClient } from '../../vcs';
import vcs from '../../vcs';
import { createUpdateBranchOnAppAsync } from './create';
import { listBranchesAsync } from './list';
import { viewUpdateBranchAsync } from './view';
@@ -62,8 +60,7 @@ async function getUpdateGroupAsync({
`,
{
group,
},
{ additionalTypenames: ['Update'] }
}
)
.toPromise()
);
@@ -165,20 +162,23 @@ export default class BranchPublish extends EasCommand {
// If a group was specified, that means we are republishing it.
republish = group ? true : republish;
const projectDir = await findProjectRootAsync();
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
isPublicConfig: true,
});
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
const runtimeVersions = getRuntimeVersionObject(exp, platformFlag, projectDir);
const projectId = await getProjectIdAsync(exp);
if (!branchName && autoFlag) {
branchName =
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substr(2, 4)}`;
(await vcs.getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`;
}
if (!branchName) {
@@ -290,7 +290,7 @@ export default class BranchPublish extends EasCommand {
const assetSpinner = ora('Uploading assets...').start();
try {
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
const assets = await collectAssetsAsync({ inputDir: inputDir!, platforms });
const assets = collectAssets({ inputDir: inputDir!, platforms });
await uploadAssetsAsync(assets);
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
assetSpinner.succeed('Uploaded assets!');
@@ -301,7 +301,7 @@ export default class BranchPublish extends EasCommand {
}
if (!message && autoFlag) {
message = (await getVcsClient().getLastCommitMessageAsync())?.trim();
message = (await vcs.getLastCommitMessageAsync())?.trim();
}
if (!message) {
@@ -315,7 +315,7 @@ export default class BranchPublish extends EasCommand {
message: `Please enter a publication message.`,
initial: republish
? `Republish "${oldMessage!}" - group: ${group}`
: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
: (await vcs.getLastCommitMessageAsync())?.trim(),
validate: value => (value ? true : validationMessage),
}));
}
@@ -392,26 +392,59 @@ export default class BranchPublish extends EasCommand {
}
}
async function getRuntimeVersionObjectAsync(
function getRuntimeVersionObject(
exp: ExpoConfig,
platformFlag: PlatformFlag,
platformFlag: string,
projectDir: string
): Promise<Record<string, string>> {
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
): Record<string, string> {
let { runtimeVersion: defaultRuntimeVersion, sdkVersion } = exp;
for (const platform of platforms) {
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
if (isPolicy) {
const isManaged = (await resolveWorkflowAsync(projectDir, platform)) === Workflow.MANAGED;
if (!isManaged) {
throw new Error('Runtime version policies are only supported in the managed workflow.');
}
// When a SDK version is supplied instead of a runtime version and we're in the managed workflow
// construct the runtimeVersion with special meaning indicating that the runtime is an
// Expo SDK preset runtime that can be launched in Expo Go.
const isManagedProject = getDefaultTarget(projectDir) === 'managed';
if (!defaultRuntimeVersion && sdkVersion && isManagedProject) {
Log.withTick('Generating runtime version from sdk version');
defaultRuntimeVersion = getRuntimeVersionForSDKVersion(sdkVersion);
}
const iOSRuntimeVersion = (exp as any).ios?.runtimeVersion; // TODO-JJ remove cast to any
const androidRuntimeVersion = (exp as any).android?.runtimeVersion; // TODO-JJ remove cast to any
let runtimeVersions: Record<string, string>;
switch (platformFlag) {
case 'ios': {
runtimeVersions = {
ios: iOSRuntimeVersion ?? defaultRuntimeVersion,
};
break;
}
case 'android': {
runtimeVersions = {
android: androidRuntimeVersion ?? defaultRuntimeVersion,
};
break;
}
case 'all': {
runtimeVersions = {
ios: iOSRuntimeVersion ?? defaultRuntimeVersion,
android: androidRuntimeVersion ?? defaultRuntimeVersion,
};
break;
}
default:
throw new Error('Platform flag must be "ios", "android", or "all"');
}
if (Object.values(runtimeVersions).some(runtime => !runtime)) {
throw new Error(
"Couldn't find a 'runtimeVersion' for every platform. Please specify it under the 'expo' key in 'app.json'"
);
}
if (Object.values(runtimeVersions).some(runtime => typeof runtime !== 'string')) {
throw new Error(
`Please ensure that all of the runtime versions defined in the app.json are strings.`
);
}
return Object.fromEntries(
platforms.map(platform => [platform, Updates.getRuntimeVersion(exp, platform)])
);
return runtimeVersions;
}
function formatUpdateTitle(
@@ -74,7 +74,10 @@ export default class BranchRename extends EasCommand {
flags: { json: jsonFlag, from: currentName, to: newName },
} = this.parse(BranchRename);
const projectDir = await findProjectRootAsync();
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
+6 -3
View File
@@ -58,8 +58,7 @@ export async function viewUpdateBranchAsync({
appId,
name,
limit: PAGE_LIMIT,
},
{ additionalTypenames: ['UpdateBranch', 'Update'] }
}
)
.toPromise()
);
@@ -91,7 +90,11 @@ export default class BranchView extends EasCommand {
flags: { json: jsonFlag },
} = this.parse(BranchView);
const projectDir = await findProjectRootAsync();
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
@@ -117,7 +117,7 @@ export default class BuildCancel extends EasCommand {
async runAsync(): Promise<void> {
const { BUILD_ID: buildIdFromArg } = this.parse(BuildCancel).args;
const projectDir = await findProjectRootAsync();
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectFullName = await getProjectFullNameAsync(exp);
@@ -31,7 +31,7 @@ export default class BuildConfigure extends EasCommand {
await configureAsync({
platform,
projectDir: await findProjectRootAsync(),
projectDir: (await findProjectRootAsync()) ?? process.cwd(),
});
Log.newLine();
+86 -135
View File
@@ -1,11 +1,13 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, EasJsonReader } from '@expo/eas-json';
import {
EasJsonReader,
hasMismatchedExtendsAsync,
isUsingDeprecatedFormatAsync,
migrateAsync,
} from '@expo/eas-json';
import { flags } from '@oclif/command';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import figures from 'figures';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { prepareAndroidBuildAsync } from '../../build/android/build';
import { BuildRequestSender, waitForBuildEndAsync } from '../../build/build';
@@ -14,17 +16,11 @@ 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,
BuildStatus,
SubmissionFragment,
} from '../../graphql/generated';
import { BuildFragment, BuildStatus, SubmissionFragment } from '../../graphql/generated';
import { toAppPlatform, toPlatform } from '../../graphql/types/AppPlatform';
import Log, { learnMore, link } from '../../log';
import Log from '../../log';
import {
RequestedPlatform,
appPlatformDisplayNames,
@@ -38,24 +34,21 @@ import {
} from '../../project/isEasEnabledForProject';
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
import { findProjectRootAsync } from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
import { createSubmissionContextAsync } from '../../submit/context';
import { confirmAsync } from '../../prompts';
import { createSubmissionContext } from '../../submissions/context';
import {
submitAsync,
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../../submit/submit';
import { printSubmissionDetailsUrls } from '../../submit/utils/urls';
import { easCliVersion } from '../../utils/easCli';
} from '../../submissions/submit';
import { printSubmissionDetailsUrls } from '../../submissions/utils/urls';
import { enableJsonOutput } from '../../utils/json';
import { ProfileData, getProfilesAsync } from '../../utils/profiles';
import { getVcsClient, setVcsClient } from '../../vcs';
import GitClient from '../../vcs/clients/git';
import vcs from '../../vcs';
interface RawBuildFlags {
platform?: string;
'skip-credentials-check': boolean;
'skip-project-configuration': boolean;
profile?: string;
profile: string;
'non-interactive': boolean;
local: boolean;
wait: boolean;
@@ -68,7 +61,7 @@ interface RawBuildFlags {
interface BuildFlags {
requestedPlatform: RequestedPlatform;
skipProjectConfiguration: boolean;
profile: string | null;
profile: string;
nonInteractive: boolean;
local: boolean;
wait: boolean;
@@ -99,8 +92,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({
@@ -143,69 +136,50 @@ export default class Build extends EasCommand {
const flags = await this.sanitizeFlagsAsync(rawFlags);
const { requestedPlatform } = flags;
const projectDir = await findProjectRootAsync();
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await getVcsClient().ensureRepoExistsAsync();
await vcs.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
const platforms = toPlatforms(requestedPlatform);
const easJsonReader = new EasJsonReader(projectDir);
const buildProfiles = await getProfilesAsync({
platforms,
profileName: flags.profile,
async readProfileAsync(platform, profileName) {
return await easJsonReader.readBuildProfileAsync(platform, profileName);
},
});
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
platforms,
projectDir,
profile: flags.profile,
nonInteractive: flags.nonInteractive,
buildProfiles,
});
const startedBuilds: {
build: BuildFragment;
buildProfile: ProfileData<BuildProfile>;
}[] = [];
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
for (const buildProfile of buildProfiles) {
const { build: maybeBuild, buildCtx } = await this.prepareAndStartBuildAsync({
const startedBuilds: BuildFragment[] = [];
for (const platform of platforms) {
const maybeBuild = 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;
}
if (flags.local) {
return;
}
Log.newLine();
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
printLogsUrls(startedBuilds);
Log.newLine();
const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) {
for (const startedBuild of startedBuilds) {
for (const build of startedBuilds) {
const submission = await this.prepareAndStartSubmissionAsync({
build: startedBuild.build,
credentialsCtx: nullthrows(
buildCtxByPlatform[startedBuild.build.platform]?.credentialsCtx
),
projectDir,
build,
flags,
moreBuilds: startedBuilds.length > 1,
projectDir,
buildProfile: startedBuild.buildProfile,
});
submissions.push(submission);
}
@@ -219,7 +193,7 @@ export default class Build extends EasCommand {
return;
}
const builds = await waitForBuildEndAsync(startedBuilds.map(({ build }) => build.id));
const builds = await waitForBuildEndAsync(startedBuilds.map(build => build.id));
printBuildResults(builds, flags.json);
const haveAllBuildsFailedOrCanceled = builds.every(
@@ -268,7 +242,7 @@ export default class Build extends EasCommand {
return {
requestedPlatform,
skipProjectConfiguration: flags['skip-project-configuration'],
profile: profile ?? null,
profile,
nonInteractive,
local: flags['local'],
wait: flags['wait'],
@@ -281,29 +255,32 @@ export default class Build extends EasCommand {
private async prepareAndStartBuildAsync({
projectDir,
platform,
flags,
moreBuilds,
buildProfile,
}: {
projectDir: string;
platform: Platform;
flags: BuildFlags;
moreBuilds: boolean;
buildProfile: ProfileData<BuildProfile>;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
}): Promise<BuildFragment | undefined> {
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,11 +297,15 @@ export default class Build extends EasCommand {
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
}
const build = await this.startBuildAsync(buildCtx);
return {
build,
buildCtx,
};
const { autoIncrement } = buildCtx.buildProfile as any; // TODO-JJ remove cast to any when android autoIncrementing is added
if (buildCtx.workflow === Workflow.GENERIC && autoIncrement) {
// Once we work out the kinks, this can be relaxed to support some GENERIC workflows.
// One of the known issues with extending autoIncrement to GENERIC projects is
// multiflavor android projects.
throw new Error('Autoincrementing is only supported for managed apps.');
}
return await this.startBuildAsync(buildCtx);
}
private async startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
@@ -338,32 +319,28 @@ export default class Build extends EasCommand {
}
private async prepareAndStartSubmissionAsync({
projectDir,
build,
credentialsCtx,
flags,
moreBuilds,
projectDir,
buildProfile,
}: {
projectDir: string;
build: BuildFragment;
credentialsCtx: CredentialsContext;
flags: BuildFlags;
moreBuilds: boolean;
projectDir: string;
buildProfile: ProfileData<BuildProfile>;
}): Promise<SubmissionFragment> {
const easJsonReader = new EasJsonReader(projectDir);
const platform = toPlatform(build.platform);
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, flags.profile);
const submitProfile = await easJsonReader.readSubmitProfileAsync(platform, flags.submitProfile);
const submissionCtx = await createSubmissionContextAsync({
const submissionCtx = createSubmissionContext({
platform,
projectDir,
projectId: build.project.id,
profile: submitProfile,
archiveFlags: { id: build.id },
nonInteractive: flags.nonInteractive,
env: buildProfile.profile.env,
credentialsCtx,
env: buildProfile.env,
});
if (moreBuilds) {
@@ -392,62 +369,36 @@ export async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
if (!(await fs.pathExists(easJsonPath))) {
return;
}
const easJsonReader = new EasJsonReader(projectDir);
const rawEasJson = await easJsonReader.readRawAsync();
if (rawEasJson?.cli) {
return;
}
if (await isUsingDeprecatedFormatAsync(projectDir)) {
const hasMismatchedExtendsKeys = await hasMismatchedExtendsAsync(projectDir);
if (nonInteractive) {
Log.error('We detected that your eas.json is using a deprecated format.');
Log.error(
'We will convert it automatically if run this command without --non-interactive flag. Alternatively, you can update eas.json manually according to https://docs.expo.dev/build/eas-json'
);
error('Unsupported eas.json format', { exit: 1 });
}
if (nonInteractive) {
Log.warn(
`${
figures.warning
} Action required: the default behavior of EAS CLI has changed and your eas.json must be updated to remove ambiguity around which Git integration workflow to use. Refer to ${link(
'https://expo.fyi/eas-vcs-workflow'
)} for more information.`
);
Log.warn(
'This warning will become an error in an upcoming EAS CLI release. For now, we will proceed with the old default behavior to avoid disruption of your builds.'
);
setVcsClient(new GitClient());
return;
}
Log.log(
`${chalk.bold(
'eas-cli@>=0.34.0 no longer requires that you commit changes to Git before starting a build.'
)} ${learnMore('https://expo.fyi/eas-vcs-workflow')}`
);
Log.log(
`If you want to continue using the Git integration, you can opt in with ${chalk.bold(
'cli.requireCommit'
)} in ${chalk.bold('eas.json')} or with the following prompt.`
);
Log.newLine();
const mode = await selectAsync('Select your preferred Git integration', [
{ title: 'Require changes to be committed in Git (old default)', value: 'requireCommit' },
{ title: 'Allow builds with dirty Git working tree (new default)', value: 'noCommit' },
]);
if (mode === 'requireCommit') {
setVcsClient(new GitClient());
await ensureRepoIsCleanAsync(nonInteractive);
}
rawEasJson.cli =
mode === 'requireCommit'
? { version: `>= ${easCliVersion}`, requireCommit: true }
: { version: `>= ${easCliVersion}` };
await fs.writeJSON(easJsonPath, rawEasJson, { spaces: 2 });
Log.withTick('Updated eas.json');
if (mode === 'requireCommit') {
await getVcsClient().trackFileAsync(easJsonPath);
await reviewAndCommitChangesAsync('Set cli.requireCommit to true in eas.json', {
nonInteractive,
const confirm = await confirmAsync({
message:
'We detected that your eas.json is using a deprecated format, do you want to migrate to the new format?',
});
if (confirm) {
await migrateAsync(projectDir);
if (hasMismatchedExtendsKeys) {
Log.warn(
'"extends" keyword can only be migrated automatically to the new format if both Android and iOS profiles extend base profiles with the same names for both platforms'
);
Log.warn(
'Migration was successful, but you need to manually adjust the extend rules for your profiles'
);
error('Fix eas.json manually', { exit: 1 });
}
} else {
Log.error(
"Aborting, update your eas.json according to https://docs.expo.dev/build/eas-json and run 'eas build' again"
);
error('Unsupported eas.json format', { exit: 1 });
}
}
}
+1 -1
View File
@@ -76,7 +76,7 @@ export default class BuildList extends EasCommand {
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution);
const projectDir = await findProjectRootAsync();
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectName = await getProjectFullNameAsync(exp);
+1 -1
View File
@@ -34,7 +34,7 @@ export default class BuildView extends EasCommand {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectName = await getProjectFullNameAsync(exp);
@@ -84,7 +84,10 @@ export default class ChannelCreate extends EasCommand {
flags: { json: jsonFlag },
} = this.parse(ChannelCreate);
const projectDir = await findProjectRootAsync();
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
@@ -49,8 +49,7 @@ async function getChannelByNameForAppAsync({
}
}
`,
{ appId, channelName },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch'] }
{ appId, channelName }
)
.toPromise()
);
@@ -122,7 +121,10 @@ export default class ChannelEdit extends EasCommand {
flags: { branch: branchName, json: jsonFlag },
} = this.parse(ChannelEdit);
const projectDir = await findProjectRootAsync();
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
@@ -58,8 +58,7 @@ async function getAllUpdateChannelForAppAsync({
}
}
`,
{ appId, offset: 0, limit: CHANNEL_LIMIT },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
{ appId, offset: 0, limit: CHANNEL_LIMIT }
)
.toPromise()
);
@@ -81,7 +80,10 @@ export default class ChannelList extends EasCommand {
flags: { json: jsonFlag },
} = this.parse(ChannelList);
const projectDir = await findProjectRootAsync();
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
@@ -346,7 +346,10 @@ export default class ChannelRollout extends EasCommand {
flags: { branch: branchName, percent },
} = this.parse(ChannelRollout);
const projectDir = await findProjectRootAsync();
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
@@ -133,8 +133,7 @@ export async function getUpdateChannelByNameForAppAsync({
}
}
`,
{ appId, channelName },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
{ appId, channelName }
)
.toPromise()
);
@@ -216,7 +215,10 @@ export default class ChannelView extends EasCommand {
flags: { json: jsonFlag },
} = this.parse(ChannelView);
const projectDir = await findProjectRootAsync();
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
+1 -1
View File
@@ -27,7 +27,7 @@ export default class Config extends EasCommand {
profile?: string;
};
const projectDir = await findProjectRootAsync();
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
await handleDeprecatedEasJsonAsync(projectDir, false);
const reader = new EasJsonReader(projectDir);
+2 -6
View File
@@ -1,16 +1,12 @@
import EasCommand from '../commandUtils/EasCommand';
import { CredentialsContext } from '../credentials/context';
import { createCredentialsContextAsync } 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> {
const ctx = new CredentialsContext({
projectDir: process.cwd(),
user: await ensureLoggedInAsync(),
});
const ctx = await createCredentialsContextAsync(process.cwd(), {});
await new SelectPlatform().runAsync(ctx);
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ export default class BuildList extends EasCommand {
async runAsync(): Promise<void> {
let appleTeamIdentifier = this.parse(BuildList).flags['apple-team-id'];
const projectDir = await findProjectRootAsync();
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = await getProjectAccountNameAsync(exp);
+1 -1
View File
@@ -29,7 +29,7 @@ If you are not sure what is the UDID of the device you are looking for, run:
throw new Error('Device UDID is missing');
}
const projectDir = await findProjectRootAsync();
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = await getProjectAccountNameAsync(exp);
+3 -2
View File
@@ -2,7 +2,8 @@ import envinfo from 'envinfo';
import EasCommand from '../commandUtils/EasCommand';
import Log from '../log';
import { easCliVersion } from '../utils/easCli';
const packageJSON = require('../../package.json');
export default class Diagnostics extends EasCommand {
static description = 'log environment info to the console';
@@ -29,7 +30,7 @@ export default class Diagnostics extends EasCommand {
npmGlobalPackages: ['eas-cli', 'expo-cli'],
},
{
title: `EAS CLI ${easCliVersion} environment info`,
title: `EAS CLI ${packageJSON.version} environment info`,
}
);
Log.log(info);
@@ -22,8 +22,7 @@ async function projectInfoByIdAsync(appId: string): Promise<AppInfoQuery> {
}
}
`,
{ appId },
{ additionalTypenames: ['App'] }
{ appId }
)
.toPromise()
);
@@ -35,7 +34,10 @@ export default class ProjectInfo extends EasCommand {
static description = 'information about the current project';
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const { app } = await projectInfoByIdAsync(projectId);
@@ -10,7 +10,10 @@ export default class ProjectInit extends EasCommand {
static aliases = ['init'];
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
if (exp.extra?.eas?.projectId) {
@@ -50,7 +50,7 @@ export default class EnvironmentSecretCreate extends EasCommand {
flags: { name, value: secretValue, scope, force },
} = this.parse(EnvironmentSecretCreate);
const projectDir = await findProjectRootAsync();
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = await getProjectAccountNameAsync(exp);
@@ -23,7 +23,7 @@ import { promptAsync, toggleConfirmAsync } from '../../prompts';
export default class EnvironmentSecretDelete extends EasCommand {
static description = `Delete an environment secret by ID.
Unsure where to find the secret's ID? Run ${chalk.bold('eas secret:list')}`;
Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
static flags = {
id: flags.string({
@@ -32,7 +32,7 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secret:list')}`;
};
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectAccountName = await getProjectAccountNameAsync(exp);
+1 -1
View File
@@ -20,7 +20,7 @@ export default class EnvironmentSecretList extends EasCommand {
static description = 'Lists environment secrets available for your current app';
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectAccountName = await getProjectAccountNameAsync(exp);
+17 -23
View File
@@ -7,7 +7,7 @@ import chalk from 'chalk';
import EasCommand from '../commandUtils/EasCommand';
import { SubmissionFragment } from '../graphql/generated';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log, { link } from '../log';
import Log, { learnMore } from '../log';
import {
RequestedPlatform,
appPlatformDisplayNames,
@@ -17,10 +17,9 @@ import {
} 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';
import { SubmitArchiveFlags, createSubmissionContext } from '../submissions/context';
import { submitAsync, waitToCompleteAsync } from '../submissions/submit';
import { printSubmissionDetailsUrls } from '../submissions/utils/urls';
interface RawFlags {
platform?: string;
@@ -45,7 +44,9 @@ interface Flags {
export default class Submit extends EasCommand {
static description = `submit build archive to app store
See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit/eas-json/')}`;
See how to configure submits with eas.json: ${learnMore('https://docs.expo.dev/submit/eas-json/', {
learnMoreMessage: '',
})}`;
static aliases = ['build:submit'];
static flags = {
@@ -55,7 +56,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',
@@ -92,7 +93,7 @@ See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit
const { flags: rawFlags } = this.parse(Submit);
const flags = await this.sanitizeFlagsAsync(rawFlags);
const projectDir = await findProjectRootAsync();
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
@@ -102,30 +103,23 @@ See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit
return;
}
const platforms = toPlatforms(flags.requestedPlatform);
const easJsonReader = new EasJsonReader(projectDir);
const submissionProfiles = await getProfilesAsync({
platforms,
profileName: flags.profile,
async readProfileAsync(platform, profileName) {
return await easJsonReader.readSubmitProfileAsync(platform, profileName);
},
});
const platforms = toPlatforms(flags.requestedPlatform);
const submissions: SubmissionFragment[] = [];
for (const submissionProfile of submissionProfiles) {
const ctx = await createSubmissionContextAsync({
platform: submissionProfile.platform,
for (const platform of platforms) {
const profile = await easJsonReader.readSubmitProfileAsync(platform, flags.profile);
const ctx = createSubmissionContext({
platform,
projectDir,
projectId,
profile: submissionProfile.profile,
profile,
archiveFlags: flags.archiveFlags,
nonInteractive: flags.nonInteractive,
});
if (submissionProfiles.length > 1) {
if (platforms.length > 1) {
Log.newLine();
const appPlatform = toAppPlatform(submissionProfile.platform);
const appPlatform = toAppPlatform(platform);
Log.log(
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
`${appPlatformDisplayNames[appPlatform]} submission`
+1 -2
View File
@@ -39,8 +39,7 @@ export async function viewUpdateAsync({
`,
{
groupId,
},
{ additionalTypenames: ['Update'] }
}
)
.toPromise()
);
@@ -30,7 +30,10 @@ export default class WebhookCreate extends EasCommand {
const { flags } = this.parse(WebhookCreate);
const webhookInputParams = await prepareInputParamsAsync(flags);
const projectDir = await findProjectRootAsync();
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
@@ -29,7 +29,10 @@ export default class WebhookDelete extends EasCommand {
args: { ID: webhookId },
} = this.parse(WebhookDelete);
const projectDir = await findProjectRootAsync();
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
@@ -29,7 +29,10 @@ export default class WebhookList extends EasCommand {
flags: { event },
} = this.parse(WebhookList);
const projectDir = await findProjectRootAsync();
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectFullName = await getProjectFullNameAsync(exp);
@@ -6,7 +6,6 @@ import {
AndroidKeystoreType,
AppFragment,
CommonAndroidAppCredentialsFragment,
GoogleServiceAccountKeyFragment,
} from '../../graphql/generated';
import * as AndroidGraphqlClient from '../android/api/GraphqlClient';
import { Keystore } from '../android/credentials';
@@ -69,16 +68,6 @@ export const testLegacyAndroidFcmFragment: AndroidFcmFragment = {
updatedAt: now,
};
export const testGoogleServiceAccountKeyFragment: GoogleServiceAccountKeyFragment = {
id: 'test-id',
projectIdentifier: 'sdf.sdf.sdf',
privateKeyIdentifier: 'test-private-key-identifier',
clientEmail: '[email protected]',
clientIdentifier: 'test-client-identifier',
createdAt: now,
updatedAt: now,
};
export const testJksAndroidKeystoreFragment: AndroidKeystoreFragment = {
id: 'test-id',
type: AndroidKeystoreType.Jks,
@@ -124,7 +113,6 @@ export const testAndroidAppCredentialsFragment: CommonAndroidAppCredentialsFragm
isLegacy: false,
app: testAppFragment,
androidFcm: testLegacyAndroidFcmFragment,
googleServiceAccountKeyForSubmissions: testGoogleServiceAccountKeyFragment,
androidAppBuildCredentialsList: [testLegacyAndroidBuildCredentialsFragment],
};
@@ -142,9 +130,6 @@ export function getNewAndroidApiMock(): { [key in keyof typeof AndroidGraphqlCli
getAndroidAppBuildCredentialsByNameAsync: jest.fn(),
createOrUpdateAndroidAppBuildCredentialsByNameAsync: jest.fn(),
createKeystoreAsync: jest.fn(),
createGoogleServiceAccountKeyAsync: jest.fn(),
deleteGoogleServiceAccountKeyAsync: jest.fn(),
getGoogleServiceAccountKeysForAccountAsync: jest.fn(),
createFcmAsync: jest.fn(),
deleteKeystoreAsync: jest.fn(),
deleteFcmAsync: jest.fn(),
@@ -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,12 +1,12 @@
import merge from 'ts-deepmerge';
import { CredentialsContext } from '../context';
import { Context } from '../context';
import { getNewAndroidApiMock } from './fixtures-android';
import { getAppstoreMock } from './fixtures-appstore';
import { testAppJson, testUsername } from './fixtures-constants';
import { getNewIosApiMock } from './fixtures-ios';
export function createCtxMock(mockOverride: Record<string, any> = {}): CredentialsContext {
export function createCtxMock(mockOverride: Record<string, any> = {}): Context {
const defaultMock = {
ios: getNewIosApiMock(),
android: getNewAndroidApiMock(),
@@ -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(() => []),
};
}
@@ -3,9 +3,9 @@ import { CredentialsSource } from '@expo/eas-json';
import nullthrows from 'nullthrows';
import { AndroidAppBuildCredentialsFragment } from '../../graphql/generated';
import { CredentialsContext } from '../context';
import { Context } 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';
@@ -20,7 +20,7 @@ interface Options {
export default class AndroidCredentialsProvider {
public readonly platform = Platform.ANDROID;
constructor(private ctx: CredentialsContext, private options: Options) {}
constructor(private ctx: Context, private options: Options) {}
public async getCredentialsAsync(
src: CredentialsSource.LOCAL | CredentialsSource.REMOTE
@@ -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);
}
@@ -3,14 +3,14 @@ import {
CommonAndroidAppCredentialsFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { Context } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
export class AssignFcm {
constructor(private app: AppLookupParams) {}
public async runAsync(
ctx: CredentialsContext,
ctx: Context,
fcm: AndroidFcmFragment
): Promise<CommonAndroidAppCredentialsFragment> {
const appCredentials =
@@ -1,29 +0,0 @@
import {
CommonAndroidAppCredentialsFragment,
GoogleServiceAccountKeyFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
export class AssignGoogleServiceAccountKey {
constructor(private app: AppLookupParams) {}
public async runAsync(
ctx: CredentialsContext,
googleServiceAccountKey: GoogleServiceAccountKeyFragment
): Promise<CommonAndroidAppCredentialsFragment> {
const appCredentials =
await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(this.app);
const updatedAppCredentials = await ctx.android.updateAndroidAppCredentialsAsync(
appCredentials,
{
googleServiceAccountKeyForSubmissionsId: googleServiceAccountKey.id,
}
);
Log.succeed(
`Google Service Account Key assigned to ${this.app.androidApplicationIdentifier} for submissions`
);
return updatedAppCredentials;
}
}
@@ -8,7 +8,7 @@ import { GradleBuildContext } from '../../../project/android/gradle';
import { getProjectAccountName, getProjectConfigDescription } from '../../../project/projectUtils';
import { promptAsync } from '../../../prompts';
import { findAccountByName } from '../../../user/Account';
import { CredentialsContext } from '../../context';
import { Context } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
/**
@@ -16,7 +16,7 @@ import { AppLookupParams } from '../api/GraphqlClient';
* EAS credentials set up yet
*/
export async function canCopyLegacyCredentialsAsync(
ctx: CredentialsContext,
ctx: Context,
app: AppLookupParams
): Promise<boolean> {
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(app);
@@ -30,7 +30,7 @@ export async function canCopyLegacyCredentialsAsync(
}
export async function promptUserAndCopyLegacyCredentialsAsync(
ctx: CredentialsContext,
ctx: Context,
app: AppLookupParams
): Promise<void> {
assert(
@@ -85,7 +85,7 @@ export async function promptUserAndCopyLegacyCredentialsAsync(
}
export async function getAppLookupParamsFromContextAsync(
ctx: CredentialsContext,
ctx: Context,
gradleContext?: GradleBuildContext
): Promise<AppLookupParams> {
ctx.ensureProjectContext();
@@ -113,7 +113,7 @@ export async function getAppLookupParamsFromContextAsync(
}
export async function createOrUpdateDefaultAndroidAppBuildCredentialsAsync(
ctx: CredentialsContext,
ctx: Context,
appLookupParams: AppLookupParams,
{
androidKeystoreId,
@@ -2,12 +2,12 @@ import { AndroidFcmFragment, AndroidFcmVersion } from '../../../graphql/generate
import Log from '../../../log';
import { promptAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { CredentialsContext } from '../../context';
import { Context } from '../../context';
export class CreateFcm {
constructor(private account: Account) {}
public async runAsync(ctx: CredentialsContext): Promise<AndroidFcmFragment> {
public async runAsync(ctx: Context): Promise<AndroidFcmFragment> {
if (ctx.nonInteractive) {
throw new Error(`FCM API Key cannot be uploaded in non-interactive mode.`);
}
@@ -1,54 +0,0 @@
import { GoogleServiceAccountKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { promptAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { CredentialsContext } from '../../context';
import { GoogleServiceAccountKey } from '../credentials';
import {
detectGoogleServiceAccountKeyPathAsync,
readAndValidateServiceAccountKey,
} from '../utils/googleServiceAccountKey';
export class CreateGoogleServiceAccountKey {
constructor(private account: Account) {}
public async runAsync(ctx: CredentialsContext): Promise<GoogleServiceAccountKeyFragment> {
if (ctx.nonInteractive) {
throw new Error(`New Google Service Account Key cannot be created in non-interactive mode.`);
}
const jsonKeyObject = await this.provideAsync(ctx);
const gsaKeyFragment = await ctx.android.createGoogleServiceAccountKeyAsync(
this.account,
jsonKeyObject
);
Log.succeed('Uploaded Google Service Account Key.');
return gsaKeyFragment;
}
private async provideAsync(ctx: CredentialsContext): Promise<GoogleServiceAccountKey> {
try {
const keyJsonPath = await this.provideKeyJsonPathAsync(ctx);
return readAndValidateServiceAccountKey(keyJsonPath);
} catch (e) {
Log.error(e);
return await this.provideAsync(ctx);
}
}
private async provideKeyJsonPathAsync(ctx: CredentialsContext): Promise<string> {
const detectedPath = await detectGoogleServiceAccountKeyPathAsync(ctx.projectDir);
if (detectedPath) {
return detectedPath;
}
const { keyJsonPath } = await promptAsync([
{
type: 'text',
name: 'keyJsonPath',
message: 'Path to Google Service Account Key JSON file:',
validate: (value: string) => value.length > 0 || "Path can't be empty",
},
]);
return keyJsonPath;
}
}
@@ -2,7 +2,7 @@ import { AndroidKeystoreFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { getProjectIdAsync } from '../../../project/projectUtils';
import { Account } from '../../../user/Account';
import { CredentialsContext } from '../../context';
import { Context } from '../../context';
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
import { KeystoreWithType, keystoreSchema } from '../credentials';
import { generateRandomKeystoreAsync } from '../utils/keystore';
@@ -11,7 +11,7 @@ import { getKeystoreWithType, validateKeystore } from '../utils/keystoreNew';
export class CreateKeystore {
constructor(private account: Account) {}
public async runAsync(ctx: CredentialsContext): Promise<AndroidKeystoreFragment> {
public async runAsync(ctx: Context): Promise<AndroidKeystoreFragment> {
if (ctx.nonInteractive) {
throw new Error(`New keystore cannot be created in non-interactive mode.`);
}
@@ -5,7 +5,7 @@ import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { maybeRenameExistingFileAsync } from '../../../utils/files';
import { CredentialsContext } from '../../context';
import { Context } from '../../context';
import { AppLookupParams, formatProjectFullName } from '../api/GraphqlClient';
import { displayAndroidKeystore } from '../utils/printCredentials';
@@ -19,7 +19,7 @@ export class DownloadKeystore {
constructor(private options: DownloadKeystoreOptions) {}
public async runAsync(
ctx: CredentialsContext,
ctx: Context,
buildCredentials: AndroidAppBuildCredentialsFragment
): Promise<void> {
const keystore = buildCredentials.androidKeystore;
@@ -65,7 +65,7 @@ export class BackupKeystore {
constructor(private app: AppLookupParams) {}
public async runAsync(
ctx: CredentialsContext,
ctx: Context,
buildCredentials: AndroidAppBuildCredentialsFragment
): Promise<void> {
if (!buildCredentials.androidKeystore) {
@@ -1,12 +1,12 @@
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { Context } from '../../context';
import { AppLookupParams, formatProjectFullName } from '../api/GraphqlClient';
export class RemoveFcm {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: CredentialsContext): Promise<void> {
async runAsync(ctx: Context): 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."
@@ -1,53 +0,0 @@
import { GoogleServiceAccountKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { CredentialsContext } from '../../context';
import { selectGoogleServiceAccountKeyAsync } from '../utils/googleServiceAccountKey';
export class SelectAndRemoveGoogleServiceAccountKey {
constructor(private account: Account) {}
async runAsync(ctx: CredentialsContext): Promise<void> {
if (ctx.nonInteractive) {
throw new Error(
`Cannot select and remove Google Service Account Keys in non-interactive mode.`
);
}
const gsaKeyFragments = await ctx.android.getGoogleServiceAccountKeysForAccountAsync(
this.account
);
if (gsaKeyFragments.length === 0) {
Log.warn("There aren't any Google Service Account Keys associated with your account.");
return;
}
const selected = await selectGoogleServiceAccountKeyAsync(gsaKeyFragments);
await new RemoveGoogleServiceAccountKey(selected).runAsync(ctx);
Log.succeed('Removed Google Service Account Key.');
Log.newLine();
}
}
export class RemoveGoogleServiceAccountKey {
constructor(private googleServiceAccountKey: GoogleServiceAccountKeyFragment) {}
public async runAsync(ctx: CredentialsContext): Promise<void> {
if (ctx.nonInteractive) {
throw new Error(`Cannot remove Google Service Account Keys in non-interactive mode`);
}
// TODO(quin): add an extra edge on GoogleServiceAccountKey to find the apps using it
const confirm = await confirmAsync({
message: `Deleting this Google Service Account Key may affect your projects that rely on it. Do you want to continue?`,
});
if (!confirm) {
Log.log('Aborting');
return;
}
Log.log('Removing Google Service Account Key.');
await ctx.android.deleteGoogleServiceAccountKeyAsync(this.googleServiceAccountKey);
}
}
@@ -3,7 +3,7 @@ import chalk from 'chalk';
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { Context } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
import { BackupKeystore } from './DownloadKeystore';
@@ -11,7 +11,7 @@ export class RemoveKeystore {
constructor(private app: AppLookupParams) {}
async runAsync(
ctx: CredentialsContext,
ctx: Context,
buildCredentials: AndroidAppBuildCredentialsFragment
): Promise<void> {
if (ctx.nonInteractive) {
@@ -1,79 +0,0 @@
import nullthrows from 'nullthrows';
import {
CommonAndroidAppCredentialsFragment,
GoogleServiceAccountKeyFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
import { promptAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import { AppLookupParams } from '../api/GraphqlClient';
import { AssignGoogleServiceAccountKey } from './AssignGoogleServiceAccountKey';
import { CreateGoogleServiceAccountKey } from './CreateGoogleServiceAccountKey';
import { UseExistingGoogleServiceAccountKey } from './UseExistingGoogleServiceAccountKey';
export class SetUpGoogleServiceAccountKey {
constructor(private app: AppLookupParams) {}
public async runAsync(ctx: CredentialsContext): Promise<CommonAndroidAppCredentialsFragment> {
const isKeySetup = await this.isGoogleServiceAccountKeySetupAsync(ctx);
if (isKeySetup) {
Log.succeed('Google Service Account Key already set up.');
return nullthrows(
await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(this.app),
'androidAppCredentials cannot be null if google service account key is already set up'
);
}
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'Google Service Account Keys cannot be set up in --non-interactive mode.'
);
}
const keysForAccount = await ctx.android.getGoogleServiceAccountKeysForAccountAsync(
this.app.account
);
let googleServiceAccountKey = null;
if (keysForAccount.length === 0) {
googleServiceAccountKey = await new CreateGoogleServiceAccountKey(this.app.account).runAsync(
ctx
);
} else {
googleServiceAccountKey = await this.createOrUseExistingKeyAsync(ctx);
}
return await new AssignGoogleServiceAccountKey(this.app).runAsync(ctx, googleServiceAccountKey);
}
private async isGoogleServiceAccountKeySetupAsync(ctx: CredentialsContext): Promise<boolean> {
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(
this.app
);
return !!appCredentials?.googleServiceAccountKeyForSubmissions;
}
private async createOrUseExistingKeyAsync(
ctx: CredentialsContext
): Promise<GoogleServiceAccountKeyFragment> {
const { action } = await promptAsync({
type: 'select',
name: 'action',
message: 'Select the Google Service Account Key to use for your project:',
choices: [
{
title: '[Choose an existing key]',
value: 'CHOOSE_EXISTING',
},
{ title: '[Upload a new service account key]', value: 'GENERATE' },
],
});
if (action === 'GENERATE') {
return await new CreateGoogleServiceAccountKey(this.app.account).runAsync(ctx);
}
return (
(await new UseExistingGoogleServiceAccountKey(this.app.account).runAsync(ctx)) ??
(await this.createOrUseExistingKeyAsync(ctx))
);
}
}
@@ -6,7 +6,7 @@ import {
} from '../../../graphql/generated';
import Log from '../../../log';
import { ora } from '../../../ora';
import { CredentialsContext } from '../../context';
import { Context } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import { AppLookupParams } from '../api/GraphqlClient';
import {
@@ -25,10 +25,10 @@ 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> {
async runAsync(ctx: Context): Promise<AndroidAppBuildCredentialsFragment> {
const { app, name: maybeName } = this.options;
if (!ctx.nonInteractive) {
@@ -62,7 +62,7 @@ export class SetUpBuildCredentials {
name,
keystore,
}: {
ctx: CredentialsContext;
ctx: Context;
app: AppLookupParams;
name?: string;
keystore: AndroidKeystoreFragment;
@@ -82,7 +82,7 @@ export class SetUpBuildCredentials {
app,
name,
}: {
ctx: CredentialsContext;
ctx: Context;
app: AppLookupParams;
name?: string;
}): Promise<AndroidAppBuildCredentialsFragment | null> {
@@ -108,7 +108,7 @@ export class SetUpBuildCredentials {
app,
name,
}: {
ctx: CredentialsContext;
ctx: Context;
app: AppLookupParams;
name: string;
}): Promise<AndroidAppBuildCredentialsFragment | null> {
@@ -4,7 +4,7 @@ import {
} from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { Context } from '../../context';
import { readAndroidCredentialsAsync } from '../../credentialsJson/read';
import {
SelectAndroidBuildCredentials,
@@ -14,10 +14,10 @@ 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> {
async runAsync(ctx: Context): Promise<AndroidAppBuildCredentialsFragment | null> {
if (ctx.nonInteractive) {
throw new Error(
'Setting up build credentials from credentials.json is only available in interactive mode'
@@ -1,11 +1,11 @@
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { Context } from '../../context';
import { updateAndroidCredentialsAsync } from '../../credentialsJson/update';
export class UpdateCredentialsJson {
async runAsync(
ctx: CredentialsContext,
ctx: Context,
buildCredentials: AndroidAppBuildCredentialsFragment
): Promise<void> {
Log.log('Updating Android credentials in credentials.json');
Loaded 100 of 348 files, more files were not shown because too many files have changed in this diff. Show more