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
278 changed files with 2219 additions and 5146 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" | xargs )
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | xargs )
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 -93
View File
@@ -6,108 +6,17 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
### 🎉 New features
### 🐛 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
- 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))
- Only allow autoIncrementing on managed projects. ([#630](https://github.com/expo/eas-cli/pull/630) by [@jkhales](https://github.com/jkhales))
### 🎉 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
+32 -3
View File
@@ -1,8 +1,37 @@
## 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/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.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.31.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"
"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"
}
}
+26 -26
View File
@@ -73,7 +73,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/account/login.js)_
_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.31.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.31.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.31.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`
@@ -145,7 +145,7 @@ OPTIONS
--[no-]wait Wait for build(s) to complete
```
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.31.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]`
@@ -156,7 +156,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/build/cancel.js)_
_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`
@@ -170,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.31.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`
@@ -203,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.31.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]`
@@ -217,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.31.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`
@@ -232,7 +232,7 @@ OPTIONS
--profile=profile
```
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.31.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`
@@ -243,7 +243,7 @@ USAGE
$ eas credentials
```
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.31.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`
@@ -254,7 +254,7 @@ USAGE
$ eas device:create
```
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/device/create.js)_
_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`
@@ -268,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.31.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]`
@@ -279,7 +279,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/device/view.js)_
_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`
@@ -290,7 +290,7 @@ USAGE
$ eas diagnostics
```
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.31.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]`
@@ -318,7 +318,7 @@ USAGE
$ eas project:info
```
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/project/info.js)_
_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`
@@ -332,7 +332,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/project/init.js)_
_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`
@@ -349,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.31.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`
@@ -366,7 +366,7 @@ DESCRIPTION
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.31.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`
@@ -377,7 +377,7 @@ USAGE
$ eas secret:list
```
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/secret/list.js)_
_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`
@@ -410,7 +410,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.31.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`
@@ -429,7 +429,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/webhook/create.js)_
_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]`
@@ -443,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.31.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`
@@ -457,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.31.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`
@@ -477,7 +477,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/webhook/update.js)_
_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`
@@ -491,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.31.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(),
-256
View File
@@ -1889,30 +1889,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "googleServiceAccountKeys",
"description": "Android credentials for account",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GoogleServiceAccountKey",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environmentSecrets",
"description": "Environment secrets for an account",
@@ -6120,18 +6096,6 @@
"name": "BuildMetrics",
"description": "",
"fields": [
{
"name": "buildWaitTime",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "buildQueueTime",
"description": "",
@@ -13014,22 +12978,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "googleServiceAccountKey",
"description": "Mutations that modify a Google Service Account Key",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GoogleServiceAccountKeyMutation",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "appleAppIdentifier",
"description": "Mutations that modify an Identifier for an iOS App",
@@ -14574,51 +14522,6 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "setGoogleServiceAccountKeyForSubmissions",
"description": "Set the Google Service Account Key to be used for submitting an Android app",
"args": [
{
"name": "id",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "googleServiceAccountKeyId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "OBJECT",
"name": "AndroidAppCredentials",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -14643,18 +14546,6 @@
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "googleServiceAccountKeyForSubmissionsId",
"description": "",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
@@ -15027,153 +14918,6 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GoogleServiceAccountKeyMutation",
"description": "",
"fields": [
{
"name": "createGoogleServiceAccountKey",
"description": "Create a Google Service Account Key",
"args": [
{
"name": "googleServiceAccountKeyInput",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "GoogleServiceAccountKeyInput",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "accountId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GoogleServiceAccountKey",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deleteGoogleServiceAccountKey",
"description": "Delete a Google Service Account Key",
"args": [
{
"name": "id",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DeleteGoogleServiceAccountKeyResult",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "GoogleServiceAccountKeyInput",
"description": "",
"fields": null,
"inputFields": [
{
"name": "jsonKey",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "JSONObject",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeleteGoogleServiceAccountKeyResult",
"description": "",
"fields": [
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AppleAppIdentifierMutation",
+9 -12
View File
@@ -1,8 +1,7 @@
{
"name": "eas-cli",
"type": "module",
"description": "EAS command line tool",
"version": "0.31.1",
"version": "0.28.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -10,17 +9,17 @@
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.25",
"@expo/config": "6.0.2",
"@expo/config-plugins": "4.0.2",
"@expo/eas-build-job": "0.2.52",
"@expo/eas-json": "^0.31.0",
"@expo/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.2",
"@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",
@@ -49,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",
@@ -62,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",
@@ -114,6 +111,7 @@
"cli"
],
"license": "MIT",
"main": "build/index.js",
"oclif": {
"bin": "eas",
"commands": "./build/commands",
@@ -177,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 build node_modules"
"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 };
+9 -6
View File
@@ -65,6 +65,7 @@ export async function initAsync(): Promise<void> {
flushInterval: 300,
}
);
rudderstackClient.logger.silent = true;
}
}
@@ -86,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();
}
}
@@ -113,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;
}
@@ -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());
});
// generic 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 = initGenericProject({ 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 = initGenericProject({ 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 = initGenericProject();
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('generic project', () => {
it('reads the versions from native code', async () => {
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 initGenericProject({
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 initGenericProject({
'/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 {
+10 -40
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,
warnIfAndroidPackageDefinedInAppConfigForGenericProject,
} from '../../project/android/applicationId';
import { resolveWorkflowAsync } from '../../project/workflow';
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) {
@@ -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);
}
}
@@ -1,7 +1,6 @@
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';
@@ -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 vcs.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,
@@ -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);
}
+30 -36
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';
@@ -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');
+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,
@@ -19,11 +19,7 @@ export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig)
projectDir,
xcodeProject
);
await fs.writeFile(
IOSConfig.Paths.getPBXProjectPath(projectDir),
// eslint-disable-next-line node/no-sync
xcodeProject.writeSync()
);
await fs.writeFile(IOSConfig.Paths.getPBXProjectPath(projectDir), xcodeProject.writeSync());
}
let expoPlist = await readExpoPlistAsync(projectDir);
@@ -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'
);
});
});
// generic workflow
describe(bumpVersionAsync, () => {
it('bumps expo.ios.buildNumber and CFBundleVersion when strategy = BumpStrategy.BUILD_NUMBER', async () => {
const fakeExp = initGenericProject();
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 = initGenericProject({ 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 = initGenericProject();
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 = initGenericProject();
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,
});
@@ -215,12 +104,12 @@ describe(readShortVersionAsync, () => {
describe('generic project', () => {
it('reads the short version from native code', async () => {
const exp = initGenericProject();
const appVersion = await readShortVersionAsync('/repo', exp, {});
expect(appVersion).toBe('1.0.0');
const shortVersion = await readShortVersionAsync('/repo', exp, {});
expect(shortVersion).toBe('1.0.0');
});
it('evaluates interpolated build number', async () => {
const exp = initGenericProject({
appVersion: '$(CURRENT_PROJECT_VERSION)',
shortVersion: '$(CURRENT_PROJECT_VERSION)',
});
const buildNumber = await readShortVersionAsync('/repo', exp, {
CURRENT_PROJECT_VERSION: '1.0.0',
@@ -232,84 +121,32 @@ describe(readShortVersionAsync, () => {
describe('managed project', () => {
it('reads the version from app config', async () => {
const exp = initGenericProject();
const appVersion = await readShortVersionAsync('/repo', exp, {});
expect(appVersion).toBe('1.0.0');
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 initGenericProject({
appVersion = '1.0.0',
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 initGenericProject({
'/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,
};
}
+4 -8
View File
@@ -1,7 +1,6 @@
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 {
@@ -12,7 +11,7 @@ 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) {
@@ -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),
+14 -4
View File
@@ -1,6 +1,6 @@
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';
@@ -24,8 +24,7 @@ export async function prepareJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData
): Promise<Job> {
const projectRootDirectory =
slash(path.relative(await vcs.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) {
@@ -42,7 +41,7 @@ export async function prepareJobAsync(
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,
@@ -74,6 +73,17 @@ export async function prepareJobAsync(
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));
}
@@ -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)}`
);
});
}
+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;
});
}
@@ -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();
@@ -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);
@@ -98,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);
+4 -1
View File
@@ -67,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 });
@@ -25,7 +25,7 @@ import {
PublishPlatform,
buildBundlesAsync,
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
collectAssets,
uploadAssetsAsync,
} from '../../project/publish';
import { promptAsync, selectAsync } from '../../prompts';
@@ -162,7 +162,11 @@ 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,
@@ -286,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!');
@@ -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);
+5 -1
View File
@@ -90,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();
+22 -31
View File
@@ -8,7 +8,6 @@ import {
import { flags } from '@oclif/command';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { prepareAndroidBuildAsync } from '../../build/android/build';
import { BuildRequestSender, waitForBuildEndAsync } from '../../build/build';
@@ -19,13 +18,7 @@ import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from '../../build
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
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 from '../../log';
import {
@@ -42,12 +35,12 @@ import {
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
import { findProjectRootAsync } from '../../project/projectUtils';
import { confirmAsync } from '../../prompts';
import { createSubmissionContextAsync } from '../../submit/context';
import { createSubmissionContext } from '../../submissions/context';
import {
submitAsync,
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../../submit/submit';
import { printSubmissionDetailsUrls } from '../../submit/utils/urls';
} from '../../submissions/submit';
import { printSubmissionDetailsUrls } from '../../submissions/utils/urls';
import { enableJsonOutput } from '../../utils/json';
import vcs from '../../vcs';
@@ -143,7 +136,7 @@ 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 vcs.ensureRepoExistsAsync();
@@ -160,9 +153,8 @@ export default class Build extends EasCommand {
});
const startedBuilds: BuildFragment[] = [];
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
for (const platform of platforms) {
const { build: maybeBuild, buildCtx } = await this.prepareAndStartBuildAsync({
const maybeBuild = await this.prepareAndStartBuildAsync({
projectDir,
platform,
flags,
@@ -171,7 +163,6 @@ export default class Build extends EasCommand {
if (maybeBuild) {
startedBuilds.push(maybeBuild);
}
buildCtxByPlatform[toAppPlatform(platform)] = buildCtx;
}
if (flags.local) {
return;
@@ -185,11 +176,10 @@ export default class Build extends EasCommand {
if (flags.autoSubmit) {
for (const build of startedBuilds) {
const submission = await this.prepareAndStartSubmissionAsync({
projectDir,
build,
credentialsCtx: nullthrows(buildCtxByPlatform[build.platform]?.credentialsCtx),
flags,
moreBuilds: startedBuilds.length > 1,
projectDir,
});
submissions.push(submission);
}
@@ -263,17 +253,17 @@ export default class Build extends EasCommand {
};
}
private async prepareAndStartBuildAsync<T extends Platform>({
private async prepareAndStartBuildAsync({
projectDir,
platform,
flags,
moreBuilds,
}: {
projectDir: string;
platform: T;
platform: Platform;
flags: BuildFlags;
moreBuilds: boolean;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<T> }> {
}): Promise<BuildFragment | undefined> {
const easJsonReader = new EasJsonReader(projectDir);
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, flags.profile);
@@ -307,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> {
@@ -325,23 +319,21 @@ export default class Build extends EasCommand {
}
private async prepareAndStartSubmissionAsync({
projectDir,
build,
credentialsCtx,
flags,
moreBuilds,
projectDir,
}: {
projectDir: string;
build: BuildFragment;
credentialsCtx: CredentialsContext;
flags: BuildFlags;
moreBuilds: boolean;
projectDir: string;
}): 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,
@@ -349,7 +341,6 @@ export default class Build extends EasCommand {
archiveFlags: { id: build.id },
nonInteractive: flags.nonInteractive,
env: buildProfile.env,
credentialsCtx,
});
if (moreBuilds) {
+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);
@@ -121,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);
@@ -80,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);
@@ -215,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);
@@ -34,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);
@@ -32,7 +32,7 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets: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);
+9 -7
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,9 +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 { SubmitArchiveFlags, createSubmissionContext } from '../submissions/context';
import { submitAsync, waitToCompleteAsync } from '../submissions/submit';
import { printSubmissionDetailsUrls } from '../submissions/utils/urls';
interface RawFlags {
platform?: string;
@@ -44,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 = {
@@ -91,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);
@@ -106,7 +108,7 @@ See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit
const submissions: SubmissionFragment[] = [];
for (const platform of platforms) {
const profile = await easJsonReader.readSubmitProfileAsync(platform, flags.profile);
const ctx = await createSubmissionContextAsync({
const ctx = createSubmissionContext({
platform,
projectDir,
projectId,
@@ -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(),
@@ -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(),
@@ -3,7 +3,7 @@ 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 { AppLookupParams } from './api/GraphqlClient';
@@ -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
@@ -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) {
@@ -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 {
@@ -28,7 +28,7 @@ interface Options {
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,
@@ -17,7 +17,7 @@ import { BackupKeystore } from './DownloadKeystore';
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,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))
);
}
}
@@ -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');
@@ -1,25 +0,0 @@
import { GoogleServiceAccountKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { Account } from '../../../user/Account';
import { CredentialsContext } from '../../context';
import { selectGoogleServiceAccountKeyAsync } from '../utils/googleServiceAccountKey';
export class UseExistingGoogleServiceAccountKey {
constructor(private account: Account) {}
public async runAsync(ctx: CredentialsContext): Promise<GoogleServiceAccountKeyFragment | null> {
if (ctx.nonInteractive) {
throw new Error(
`Existing Google Service Account Key cannot be chosen in non-interactive mode.`
);
}
const gsaKeyFragments = await ctx.android.getGoogleServiceAccountKeysForAccountAsync(
this.account
);
if (gsaKeyFragments.length === 0) {
Log.error("There aren't any Google Service Account Keys associated with your account.");
return null;
}
return await selectGoogleServiceAccountKeyAsync(gsaKeyFragments);
}
}
@@ -14,9 +14,9 @@ describe(AssignFcm, () => {
// expect app credentials to be fetched/created, then updated
expect(
ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync
ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync as any
).toHaveBeenCalledTimes(1);
expect(ctx.android.updateAndroidAppCredentialsAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.updateAndroidAppCredentialsAsync as any).toHaveBeenCalledTimes(1);
});
it('works in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -1,33 +0,0 @@
import { testGoogleServiceAccountKeyFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { AssignGoogleServiceAccountKey } from '../AssignGoogleServiceAccountKey';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
describe(AssignGoogleServiceAccountKey, () => {
it('assigns a Google Service Account key in Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: false,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const assignGoogleServiceAccountKeyAction = new AssignGoogleServiceAccountKey(appLookupParams);
await assignGoogleServiceAccountKeyAction.runAsync(ctx, testGoogleServiceAccountKeyFragment);
// expect app credentials to be fetched/created, then updated
expect(
ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync
).toHaveBeenCalledTimes(1);
expect(ctx.android.updateAndroidAppCredentialsAsync).toHaveBeenCalledTimes(1);
});
it('works in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const assignGoogleServiceAccountKeyAction = new AssignGoogleServiceAccountKey(appLookupParams);
// dont fail if users are running in non-interactive mode
await expect(
assignGoogleServiceAccountKeyAction.runAsync(ctx, testGoogleServiceAccountKeyFragment)
).resolves.not.toThrowError();
});
});
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync, promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -15,9 +14,8 @@ import {
promptUserAndCopyLegacyCredentialsAsync,
} from '../BuildCredentialsUtils';
jest.mock('../../../../ora');
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
(confirmAsync as jest.Mock).mockImplementation(() => true);
describe('BuildCredentialsUtils', () => {
describe(canCopyLegacyCredentialsAsync, () => {
@@ -84,7 +82,7 @@ describe('BuildCredentialsUtils', () => {
});
describe(promptUserAndCopyLegacyCredentialsAsync, () => {
it('copies all legacy credentials to EAS if the user is eligible', async () => {
asMock(promptAsync).mockImplementation(() => ({ providedName: 'test-provided-name' }));
(promptAsync as jest.Mock).mockImplementation(() => ({ providedName: 'test-provided-name' }));
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -104,12 +102,12 @@ describe('BuildCredentialsUtils', () => {
await promptUserAndCopyLegacyCredentialsAsync(ctx, appLookupParams);
expect(
ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync
ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync as any
).toHaveBeenCalledTimes(1);
expect(ctx.android.createFcmAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.updateAndroidAppCredentialsAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.createAndroidAppBuildCredentialsAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.createFcmAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.updateAndroidAppCredentialsAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createAndroidAppBuildCredentialsAsync as any).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -1,11 +1,10 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateFcm } from '../CreateFcm';
jest.mock('../../../../prompts');
asMock(promptAsync).mockImplementation(() => ({ fcmApiKey: 'blah' }));
(promptAsync as jest.Mock).mockImplementation(() => ({ fcmApiKey: 'blah' }));
describe(CreateFcm, () => {
it('creates an fcm api key in Interactive Mode', async () => {
@@ -15,7 +14,7 @@ describe(CreateFcm, () => {
await createFcmAction.runAsync(ctx);
// expect fcm api key to be created on expo servers
expect(ctx.android.createFcmAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.createFcmAsync as any).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -1,45 +0,0 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateGoogleServiceAccountKey } from '../CreateGoogleServiceAccountKey';
jest.mock('../../../../prompts');
jest.mock('fs');
asMock(promptAsync).mockImplementation(() => ({
keyJsonPath: '/google-service-account-key.json',
}));
beforeEach(() => {
vol.reset();
});
describe(CreateGoogleServiceAccountKey, () => {
it('creates a Google Service Account Key in Interactive Mode', async () => {
vol.fromJSON({
'/google-service-account-key.json': JSON.stringify({
type: 'service_account',
private_key: 'super secret',
client_email: '[email protected]',
}),
});
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createGsaKeyAction = new CreateGoogleServiceAccountKey(appLookupParams.account);
await createGsaKeyAction.runAsync(ctx);
// expect fcm api key to be created on expo servers
expect(ctx.android.createGoogleServiceAccountKeyAsync).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createGsaKeyAction = new CreateGoogleServiceAccountKey(appLookupParams.account);
// fail if users are running in non-interactive mode
await expect(createGsaKeyAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -16,8 +16,8 @@ describe(DownloadKeystore, () => {
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const downloadKeystoreAction = new DownloadKeystore({ app: appLookupParams });
await downloadKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment);
expect(fsWriteFileSpy).toHaveBeenCalledTimes(1);
expect(fsWriteFileSpy).toHaveBeenCalledWith(
expect(fsWriteFileSpy as any).toHaveBeenCalledTimes(1);
expect(fsWriteFileSpy as any).toHaveBeenCalledWith(
`@${appLookupParams.account.name}__${appLookupParams.projectName}.jks`,
testAndroidBuildCredentialsFragment.androidKeystore?.keystore,
'base64'
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -9,7 +8,7 @@ import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { RemoveFcm } from '../RemoveFcm';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
(confirmAsync as jest.Mock).mockImplementation(() => true);
describe(RemoveFcm, () => {
it('removes an FCM Api Key', async () => {
@@ -25,7 +24,7 @@ describe(RemoveFcm, () => {
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const removeFcmApiKeyAction = new RemoveFcm(appLookupParams);
await removeFcmApiKeyAction.runAsync(ctx);
expect(ctx.android.deleteFcmAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.deleteFcmAsync as any).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -1,34 +0,0 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
testGoogleServiceAccountKeyFragment,
} from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { RemoveGoogleServiceAccountKey } from '../RemoveGoogleServiceAccountKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(RemoveGoogleServiceAccountKey, () => {
it('removes an Google Service Account Key', async () => {
const ctx = createCtxMock({
nonInteractive: false,
android: {
...getNewAndroidApiMock(),
},
});
const removeGoogleServiceAccountKeyAction = new RemoveGoogleServiceAccountKey(
testGoogleServiceAccountKeyFragment
);
await removeGoogleServiceAccountKeyAction.runAsync(ctx);
expect(ctx.android.deleteGoogleServiceAccountKeyAsync).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const removeGoogleServiceAccountKeyAction = new RemoveGoogleServiceAccountKey(
testGoogleServiceAccountKeyFragment
);
await expect(removeGoogleServiceAccountKeyAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -7,7 +6,7 @@ import { RemoveKeystore } from '../RemoveKeystore';
jest.mock('fs-extra');
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
(confirmAsync as jest.Mock).mockImplementation(() => true);
describe(RemoveKeystore, () => {
it('removes a keystore', async () => {
@@ -17,7 +16,7 @@ describe(RemoveKeystore, () => {
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const removeKeystoreAction = new RemoveKeystore(appLookupParams);
await removeKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment);
expect(ctx.android.deleteKeystoreAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.deleteKeystoreAsync as any).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -11,7 +11,6 @@ import { generateRandomKeystoreAsync } from '../../utils/keystore';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetupBuildCredentials } from '../SetupBuildCredentials';
jest.mock('../../../../ora');
jest.mock('../../../../project/ensureProjectExists');
jest.mock('../../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
jest.mock('../../../../prompts', () => ({ confirmAsync: jest.fn(() => true) }));
@@ -1,6 +1,5 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { AndroidKeystoreType } from '../../../../graphql/generated';
import { confirmAsync } from '../../../../prompts';
import {
@@ -19,7 +18,7 @@ import { SetupBuildCredentialsFromCredentialsJson } from '../SetupBuildCredentia
jest.mock('fs');
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
(confirmAsync as jest.Mock).mockImplementation(() => true);
jest.mock('../../../manager/SelectAndroidBuildCredentials');
beforeEach(() => {
@@ -28,7 +27,7 @@ beforeEach(() => {
describe(SetupBuildCredentialsFromCredentialsJson, () => {
it('sets up a new build configuration from credentials.json upon user request', async () => {
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
(SelectAndroidBuildCredentials as any).mockImplementation(() => {
return {
runAsync: () => {
return {
@@ -65,8 +64,8 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
await setupBuildCredentialsAction.runAsync(ctx);
// expect keystore to be created with credentials.json content
expect(ctx.android.createKeystoreAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync).toBeCalledWith(appLookupParams.account, {
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync as any).toBeCalledWith(appLookupParams.account, {
keystorePassword: testJksAndroidKeystoreFragment.keystorePassword,
keyAlias: testJksAndroidKeystoreFragment.keyAlias,
keyPassword: testJksAndroidKeystoreFragment.keyPassword,
@@ -75,10 +74,10 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
});
// expect new build credentials to be created
expect(ctx.android.createAndroidAppBuildCredentialsAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.createAndroidAppBuildCredentialsAsync as any).toHaveBeenCalledTimes(1);
});
it('uses an existing build configuration upon user request', async () => {
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
(SelectAndroidBuildCredentials as any).mockImplementation(() => {
return {
runAsync: () => {
return {
@@ -115,8 +114,8 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
await setupBuildCredentialsAction.runAsync(ctx);
// expect keystore to be created with credentials.json content
expect(ctx.android.createKeystoreAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync).toBeCalledWith(appLookupParams.account, {
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync as any).toBeCalledWith(appLookupParams.account, {
keystorePassword: testJksAndroidKeystoreFragment.keystorePassword,
keyAlias: testJksAndroidKeystoreFragment.keyAlias,
keyPassword: testJksAndroidKeystoreFragment.keyPassword,
@@ -125,7 +124,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
});
// expect existing build credentials to be updated
expect(ctx.android.updateAndroidAppBuildCredentialsAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.updateAndroidAppBuildCredentialsAsync as any).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -1,76 +0,0 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
testAndroidAppCredentialsFragment,
testGoogleServiceAccountKeyFragment,
} from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetupGoogleServiceAccountKey } from '../SetupGoogleServiceAccountKey';
jest.mock('../../../../prompts');
jest.mock('fs');
asMock(promptAsync).mockImplementation(() => ({
keyJsonPath: '/google-service-account-key.json',
}));
beforeEach(() => {
vol.reset();
});
describe(SetupGoogleServiceAccountKey, () => {
it('skips setup when there is a Google Service Account Key already assigned to the project', async () => {
const ctx = createCtxMock({
nonInteractive: false,
android: {
...getNewAndroidApiMock(),
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(
() => testAndroidAppCredentialsFragment
),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
await setupGoogleServiceAccountKeyAction.runAsync(ctx);
expect(ctx.android.createGoogleServiceAccountKeyAsync).not.toHaveBeenCalled();
expect(ctx.android.updateAndroidAppCredentialsAsync).not.toHaveBeenCalled();
});
it('sets up a Google Service Account Key when there is none already setup', async () => {
vol.fromJSON({
'/google-service-account-key.json': JSON.stringify({
type: 'service_account',
private_key: 'super secret',
client_email: '[email protected]',
}),
});
const ctx = createCtxMock({
nonInteractive: false,
android: {
...getNewAndroidApiMock(),
createGoogleServiceAccountKeyAsync: jest.fn(() => testGoogleServiceAccountKeyFragment),
getGoogleServiceAccountKeysForAccountAsync: jest.fn(() => []),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
await setupGoogleServiceAccountKeyAction.runAsync(ctx);
expect(ctx.android.createGoogleServiceAccountKeyAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.updateAndroidAppCredentialsAsync).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
await expect(setupGoogleServiceAccountKeyAction.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
);
});
});
@@ -1,59 +0,0 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
testGoogleServiceAccountKeyFragment,
} from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { UseExistingGoogleServiceAccountKey } from '../UseExistingGoogleServiceAccountKey';
jest.mock('../../../../prompts');
asMock(promptAsync).mockImplementation(() => ({ chosenKey: testGoogleServiceAccountKeyFragment }));
describe(UseExistingGoogleServiceAccountKey, () => {
it('uses an existing Google Service Account Key in Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: false,
android: {
...getNewAndroidApiMock(),
getGoogleServiceAccountKeysForAccountAsync: jest.fn(() => [
testGoogleServiceAccountKeyFragment,
]),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const useExistingGoogleServiceAccountKeyAction = new UseExistingGoogleServiceAccountKey(
appLookupParams.account
);
const selectedKey = await useExistingGoogleServiceAccountKeyAction.runAsync(ctx);
expect(ctx.android.getGoogleServiceAccountKeysForAccountAsync).toHaveBeenCalledTimes(1);
expect(selectedKey).toMatchObject(testGoogleServiceAccountKeyFragment);
});
it("returns null if the account doesn't have any Google Service Account Keys", async () => {
const ctx = createCtxMock({
nonInteractive: false,
android: {
...getNewAndroidApiMock(),
getGoogleServiceAccountKeysForAccountAsync: jest.fn(() => []),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const useExistingGoogleServiceAccountKeyAction = new UseExistingGoogleServiceAccountKey(
appLookupParams.account
);
const selectedKey = await useExistingGoogleServiceAccountKeyAction.runAsync(ctx);
expect(ctx.android.getGoogleServiceAccountKeysForAccountAsync).toHaveBeenCalledTimes(1);
expect(selectedKey).toBe(null);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const useExistingGoogleServiceAccountKeyAction = new UseExistingGoogleServiceAccountKey(
appLookupParams.account
);
// fail if users are running in non-interactive mode
await expect(useExistingGoogleServiceAccountKeyAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -5,18 +5,15 @@ import {
AndroidKeystoreFragment,
AppFragment,
CommonAndroidAppCredentialsFragment,
GoogleServiceAccountKeyFragment,
} from '../../../graphql/generated';
import { Account } from '../../../user/Account';
import { AppQuery } from '../../ios/api/graphql/queries/AppQuery';
import { GoogleServiceAccountKey, KeystoreWithType } from '../credentials';
import { KeystoreWithType } from '../credentials';
import { AndroidAppBuildCredentialsMutation } from './graphql/mutations/AndroidAppBuildCredentialsMutation';
import { AndroidAppCredentialsMutation } from './graphql/mutations/AndroidAppCredentialsMutation';
import { AndroidFcmMutation } from './graphql/mutations/AndroidFcmMutation';
import { AndroidKeystoreMutation } from './graphql/mutations/AndroidKeystoreMutation';
import { GoogleServiceAccountKeyMutation } from './graphql/mutations/GoogleServiceAccountKeyMutation';
import { AndroidAppCredentialsQuery } from './graphql/queries/AndroidAppCredentialsQuery';
import { GoogleServiceAccountKeyQuery } from './graphql/queries/GoogleServiceAccountKeyQuery';
export interface AppLookupParams {
account: Account;
@@ -90,27 +87,11 @@ export async function updateAndroidAppCredentialsAsync(
appCredentials: CommonAndroidAppCredentialsFragment,
{
androidFcmId,
googleServiceAccountKeyForSubmissionsId,
}: {
androidFcmId?: string;
googleServiceAccountKeyForSubmissionsId?: string;
androidFcmId: string;
}
): Promise<CommonAndroidAppCredentialsFragment> {
let updatedAppCredentials = appCredentials;
if (androidFcmId) {
updatedAppCredentials = await AndroidAppCredentialsMutation.setFcmKeyAsync(
appCredentials.id,
androidFcmId
);
}
if (googleServiceAccountKeyForSubmissionsId) {
updatedAppCredentials =
await AndroidAppCredentialsMutation.setGoogleServiceAccountKeyForSubmissionsAsync(
appCredentials.id,
googleServiceAccountKeyForSubmissionsId
);
}
return updatedAppCredentials;
return await AndroidAppCredentialsMutation.setFcmKeyAsync(appCredentials.id, androidFcmId);
}
export async function updateAndroidAppBuildCredentialsAsync(
@@ -242,30 +223,6 @@ export async function deleteFcmAsync(fcm: AndroidFcmFragment): Promise<void> {
return await AndroidFcmMutation.deleteAndroidFcmAsync(fcm.id);
}
export async function createGoogleServiceAccountKeyAsync(
account: Account,
jsonKey: GoogleServiceAccountKey
): Promise<GoogleServiceAccountKeyFragment> {
return await GoogleServiceAccountKeyMutation.createGoogleServiceAccountKeyAsync(
{ jsonKey },
account.id
);
}
export async function deleteGoogleServiceAccountKeyAsync(
googleServiceAccountKey: GoogleServiceAccountKeyFragment
): Promise<void> {
return await GoogleServiceAccountKeyMutation.deleteGoogleServiceAccountKeyAsync(
googleServiceAccountKey.id
);
}
export async function getGoogleServiceAccountKeysForAccountAsync(
account: Account
): Promise<GoogleServiceAccountKeyFragment[]> {
return await GoogleServiceAccountKeyQuery.getAllForAccountAsync(account.name);
}
async function getAppAsync(appLookupParams: AppLookupParams): Promise<AppFragment> {
const projectFullName = formatProjectFullName(appLookupParams);
return await AppQuery.byFullNameAsync(projectFullName);
Loaded 100 of 278 files, more files were not shown because too many files have changed in this diff. Show more