mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 17:50:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca0a8f79cd | ||
|
|
67eac59585 | ||
|
|
8cf1f0dc22 | ||
|
|
51702e4bda | ||
|
|
426a3d1ef3 | ||
|
|
e8d71a52ec | ||
|
|
ccd0cb19a4 | ||
|
|
9f85178d71 | ||
|
|
085ee52305 | ||
|
|
22a71275b2 | ||
|
|
5a730d70d6 | ||
|
|
41f58e5e52 | ||
|
|
34af57f585 | ||
|
|
4674cbebc5 | ||
|
|
69add6f523 | ||
|
|
b70e874135 | ||
|
|
03fa29dd5a | ||
|
|
9e467b900e | ||
|
|
6e1c7dd91e | ||
|
|
9812406c18 | ||
|
|
cf11ae93af | ||
|
|
14756ad39b | ||
|
|
1486b481f6 | ||
|
|
84d82e9a59 | ||
|
|
c7ce4a8670 | ||
|
|
fe0a3b4c41 | ||
|
|
0bce278bd4 |
No files matched your search
+2
-1
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['universe/node'],
|
||||
plugins: ['async-protect'],
|
||||
plugins: ['async-protect', 'node'],
|
||||
rules: {
|
||||
'no-console': 'warn',
|
||||
'no-constant-condition': ['warn', { checkLoops: false }],
|
||||
@@ -41,6 +41,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'async-protect/async-suffix': 'error',
|
||||
'node/no-sync': 'error',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 192 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 153 KiB |
@@ -15,7 +15,7 @@ jobs:
|
||||
- id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
draft: true
|
||||
@@ -113,3 +113,69 @@ 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
|
||||
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 "[skip ci] 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: ''
|
||||
+47
-2
@@ -6,15 +6,60 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Enable auto-incrementing on Android. ([#645](https://github.com/expo/eas-cli/pull/645) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 🧹 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))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [0.28.1](https://github.com/expo/eas-cli/releases/tag/v0.28.1) - 2021-09-22
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
+3
-32
@@ -1,37 +1,8 @@
|
||||
## Releasing a new version
|
||||
|
||||
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.**
|
||||

|
||||
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.
|
||||

|
||||
1. If any of the changelog entries have been tagged with `[EAS BUILD API]`
|
||||
1. Run `yarn release` in the repository root folder.
|
||||
2. Wait, the `release` workflow on GitHub Actions is going to take care of the rest (including publishing packages to npm and updating the changelog).
|
||||
3. Check the changelog. If any of the entries have been tagged with `[EAS BUILD API]`
|
||||
- Open `package.json` and note the version of `@expo/eas-build-job` used in the eas-cli.
|
||||
- Find the `Publish` commit in [expo/eas-build](https://github.com/expo/eas-build/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
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.28.2"
|
||||
"version": "0.30.0"
|
||||
}
|
||||
+3
-1
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
"packages/*",
|
||||
"scripts"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "lerna run build",
|
||||
@@ -21,6 +22,7 @@
|
||||
"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",
|
||||
|
||||
+26
-26
@@ -73,7 +73,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/account/login.js)_
|
||||
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/account/logout.js)_
|
||||
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/account/view.js)_
|
||||
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/analytics.js)_
|
||||
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/build/index.js)_
|
||||
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/build/cancel.js)_
|
||||
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/build/configure.js)_
|
||||
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/build/list.js)_
|
||||
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/build/view.js)_
|
||||
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/config.js)_
|
||||
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/credentials.js)_
|
||||
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/device/create.js)_
|
||||
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/device/list.js)_
|
||||
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/device/view.js)_
|
||||
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/diagnostics.js)_
|
||||
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/project/info.js)_
|
||||
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/project/init.js)_
|
||||
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/secret/create.js)_
|
||||
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/secret/delete.js)_
|
||||
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/secret/list.js)_
|
||||
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/submit.js)_
|
||||
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/webhook/create.js)_
|
||||
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/webhook/delete.js)_
|
||||
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/webhook/list.js)_
|
||||
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/webhook/update.js)_
|
||||
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.30.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.28.2/packages/eas-cli/src/commands/webhook/view.js)_
|
||||
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/webhook/view.js)_
|
||||
<!-- commandsstop -->
|
||||
@@ -1,8 +1,10 @@
|
||||
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 });
|
||||
}
|
||||
|
||||
|
||||
Generated
+2067
-607
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.28.2",
|
||||
"version": "0.30.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -11,15 +11,15 @@
|
||||
"@expo/apple-utils": "0.0.0-alpha.25",
|
||||
"@expo/config": "5.0.9",
|
||||
"@expo/config-plugins": "3.1.0",
|
||||
"@expo/eas-build-job": "0.2.46",
|
||||
"@expo/eas-json": "^0.28.2",
|
||||
"@expo/eas-build-job": "0.2.48",
|
||||
"@expo/eas-json": "^0.30.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": "2.1.0",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.0.7",
|
||||
"@expo/rudder-sdk-node": "1.1.0",
|
||||
"@expo/sdk-runtime-versions": "1.0.0",
|
||||
"@expo/spawn-async": "1.5.0",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
const fs = require('fs');
|
||||
const { promises: fs } = require('fs');
|
||||
|
||||
// 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);
|
||||
(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);
|
||||
|
||||
// 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 {
|
||||
};
|
||||
}
|
||||
|
||||
function createTestProject(
|
||||
export function createTestProject(
|
||||
user: Actor,
|
||||
appJSONExtraData?: Record<string, object | string>
|
||||
): MockProject {
|
||||
@@ -58,5 +58,3 @@ function createTestProject(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export { createTestProject };
|
||||
@@ -65,7 +65,6 @@ export async function initAsync(): Promise<void> {
|
||||
flushInterval: 300,
|
||||
}
|
||||
);
|
||||
rudderstackClient.logger.silent = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +86,7 @@ export async function setUserDataAsync(userId: string, traits: Record<string, an
|
||||
|
||||
export async function flushAsync(): Promise<void> {
|
||||
if (rudderstackClient) {
|
||||
rudderstackClient.flush();
|
||||
await rudderstackClient.flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,13 +113,11 @@ function ensureUserIdentified(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rudderstackClient) {
|
||||
rudderstackClient.identify({
|
||||
userId: identifyData.userId,
|
||||
anonymousId: identifyData.deviceId,
|
||||
traits: identifyData.traits,
|
||||
});
|
||||
}
|
||||
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(() => {
|
||||
afterAll(async () => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.removeSync(os.tmpdir());
|
||||
await fs.remove(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.mkdirpSync(os.tmpdir());
|
||||
await fs.mkdirp(os.tmpdir());
|
||||
});
|
||||
|
||||
const testChannel = 'test-channel';
|
||||
|
||||
@@ -1,42 +1,204 @@
|
||||
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 { maybeResolveVersionsAsync } from '../version';
|
||||
import { getAppBuildGradleAsync, resolveConfigValue } from '../../../project/android/gradleUtils';
|
||||
import {
|
||||
BumpStrategy,
|
||||
bumpVersionAsync,
|
||||
bumpVersionInAppJsonAsync,
|
||||
maybeResolveVersionsAsync,
|
||||
} from '../version';
|
||||
|
||||
const fsReal = jest.requireActual('fs').promises as typeof fs;
|
||||
jest.mock('fs');
|
||||
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.removeSync(os.tmpdir());
|
||||
await fs.remove(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.mkdirpSync(os.tmpdir());
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe(maybeResolveVersionsAsync, () => {
|
||||
describe('generic project', () => {
|
||||
it('reads the version code from native code', async () => {
|
||||
it('reads the versions from native code', async () => {
|
||||
const exp = initGenericProject();
|
||||
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
|
||||
'/repo',
|
||||
exp,
|
||||
{} as BuildProfile<Platform.ANDROID>
|
||||
);
|
||||
expect(appVersion).toBe('2.0');
|
||||
expect(appVersion).toBe('3.0.0');
|
||||
expect(appBuildVersion).toBe('123');
|
||||
});
|
||||
});
|
||||
describe('managed project', () => {
|
||||
it('reads the version code from expo config', async () => {
|
||||
it('reads the versions from expo config', async () => {
|
||||
const exp = initManagedProject();
|
||||
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
|
||||
'/repo',
|
||||
@@ -49,22 +211,31 @@ describe(maybeResolveVersionsAsync, () => {
|
||||
});
|
||||
});
|
||||
|
||||
function initGenericProject(): ExpoConfig {
|
||||
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,
|
||||
},
|
||||
};
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: {
|
||||
version: '1.0.0',
|
||||
android: {
|
||||
versionCode: 1,
|
||||
},
|
||||
},
|
||||
expo: fakeExp,
|
||||
}),
|
||||
'./android/app/build.gradle': `android {
|
||||
defaultConfig {
|
||||
applicationId "com.expo.testapp"
|
||||
versionCode 123
|
||||
versionName "2.0"
|
||||
versionCode ${versionCode}
|
||||
versionName "${versionName}"
|
||||
}
|
||||
}`,
|
||||
'./android/app/src/main/AndroidManifest.xml': 'fake',
|
||||
@@ -72,32 +243,10 @@ function initGenericProject(): ExpoConfig {
|
||||
'/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',
|
||||
@@ -106,5 +255,26 @@ 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'
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,6 @@ 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 {
|
||||
@@ -65,9 +64,11 @@ 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 () => {
|
||||
if (ctx.workflow === Workflow.GENERIC) {
|
||||
await validateAndSyncProjectConfigurationAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
await validateAndSyncProjectConfigurationAsync({
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
buildProfile,
|
||||
});
|
||||
},
|
||||
getMetadataContext: () => ({
|
||||
gradleContext,
|
||||
@@ -110,22 +111,16 @@ async function ensureAndroidCredentialsAsync(
|
||||
ctx.exp,
|
||||
gradleContext
|
||||
);
|
||||
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 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 { credentialsSource } = ctx.buildProfile;
|
||||
logCredentialsSource(credentialsSource, Platform.ANDROID);
|
||||
return {
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
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) {
|
||||
@@ -30,16 +34,42 @@ export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void
|
||||
Log.withTick('Android project configured');
|
||||
}
|
||||
|
||||
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.'
|
||||
);
|
||||
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 });
|
||||
}
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,89 @@ 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,
|
||||
@@ -39,3 +115,39 @@ 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);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ 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';
|
||||
@@ -31,6 +32,7 @@ export interface BuildContext<T extends Platform> {
|
||||
buildProfile: BuildProfile<T>;
|
||||
buildProfileName: string;
|
||||
clearCache: boolean;
|
||||
credentialsCtx: CredentialsContext;
|
||||
exp: ExpoConfig;
|
||||
local: boolean;
|
||||
nonInteractive: boolean;
|
||||
@@ -69,9 +71,16 @@ 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,
|
||||
@@ -87,11 +96,13 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
...devClientProperties,
|
||||
};
|
||||
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
|
||||
|
||||
return {
|
||||
accountName,
|
||||
buildProfile,
|
||||
buildProfileName,
|
||||
clearCache,
|
||||
credentialsCtx,
|
||||
exp,
|
||||
local,
|
||||
nonInteractive,
|
||||
|
||||
@@ -19,7 +19,11 @@ export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig)
|
||||
projectDir,
|
||||
xcodeProject
|
||||
);
|
||||
await fs.writeFile(IOSConfig.Paths.getPBXProjectPath(projectDir), xcodeProject.writeSync());
|
||||
await fs.writeFile(
|
||||
IOSConfig.Paths.getPBXProjectPath(projectDir),
|
||||
// eslint-disable-next-line node/no-sync
|
||||
xcodeProject.writeSync()
|
||||
);
|
||||
}
|
||||
|
||||
let expoPlist = await readExpoPlistAsync(projectDir);
|
||||
|
||||
@@ -6,17 +6,17 @@ import { readChannelSafelyAsync } from '../UpdatesModule';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.removeSync(os.tmpdir());
|
||||
await fs.remove(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.mkdirpSync(os.tmpdir());
|
||||
await fs.mkdirp(os.tmpdir());
|
||||
});
|
||||
|
||||
const testChannel = 'test-channel';
|
||||
|
||||
@@ -10,29 +10,43 @@ import {
|
||||
bumpVersionAsync,
|
||||
bumpVersionInAppJsonAsync,
|
||||
evaluateTemplateString,
|
||||
getInfoPlistPath,
|
||||
readBuildNumberAsync,
|
||||
readShortVersionAsync,
|
||||
} from '../version';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.removeSync(os.tmpdir());
|
||||
await fs.remove(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.mkdirpSync(os.tmpdir());
|
||||
await fs.mkdirp(os.tmpdir());
|
||||
});
|
||||
|
||||
describe(evaluateTemplateString, () => {
|
||||
it('evaluates the template string', () => {
|
||||
it('evaluates the template string when value is a number', () => {
|
||||
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
|
||||
@@ -44,6 +58,7 @@ describe(bumpVersionAsync, () => {
|
||||
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
buildSettings: {},
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
@@ -58,13 +73,38 @@ describe(bumpVersionAsync, () => {
|
||||
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.SHORT_VERSION,
|
||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
buildSettings: {},
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
@@ -86,6 +126,7 @@ describe(bumpVersionAsync, () => {
|
||||
bumpStrategy: BumpStrategy.NOOP,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
buildSettings: {},
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
@@ -123,7 +164,7 @@ describe(bumpVersionInAppJsonAsync, () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.SHORT_VERSION,
|
||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
@@ -174,12 +215,12 @@ describe(readShortVersionAsync, () => {
|
||||
describe('generic project', () => {
|
||||
it('reads the short version from native code', async () => {
|
||||
const exp = initGenericProject();
|
||||
const shortVersion = await readShortVersionAsync('/repo', exp, {});
|
||||
expect(shortVersion).toBe('1.0.0');
|
||||
const appVersion = await readShortVersionAsync('/repo', exp, {});
|
||||
expect(appVersion).toBe('1.0.0');
|
||||
});
|
||||
it('evaluates interpolated build number', async () => {
|
||||
const exp = initGenericProject({
|
||||
shortVersion: '$(CURRENT_PROJECT_VERSION)',
|
||||
appVersion: '$(CURRENT_PROJECT_VERSION)',
|
||||
});
|
||||
const buildNumber = await readShortVersionAsync('/repo', exp, {
|
||||
CURRENT_PROJECT_VERSION: '1.0.0',
|
||||
@@ -191,32 +232,84 @@ describe(readShortVersionAsync, () => {
|
||||
describe('managed project', () => {
|
||||
it('reads the version from app config', async () => {
|
||||
const exp = initGenericProject();
|
||||
const shortVersion = await readShortVersionAsync('/repo', exp, {});
|
||||
expect(shortVersion).toBe('1.0.0');
|
||||
const appVersion = await readShortVersionAsync('/repo', exp, {});
|
||||
expect(appVersion).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({
|
||||
shortVersion = '1.0.0',
|
||||
appVersion = '1.0.0',
|
||||
version = '1',
|
||||
}: { shortVersion?: string; version?: string } = {}): ExpoConfig {
|
||||
infoPlistName = 'Info.plist',
|
||||
}: { appVersion?: string; version?: string; infoPlistName?: string } = {}): ExpoConfig {
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '1.0.0',
|
||||
ios: {
|
||||
buildNumber: '1',
|
||||
},
|
||||
};
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: {
|
||||
version: '1.0.0',
|
||||
ios: {
|
||||
buildNumber: '1',
|
||||
},
|
||||
},
|
||||
expo: fakeExp,
|
||||
}),
|
||||
'./ios/myproject/Info.plist': `<?xml version="1.0" encoding="UTF-8"?>
|
||||
[`./ios/myproject/${infoPlistName}`]: `<?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>${shortVersion}</string>
|
||||
<string>${appVersion}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${version}</string>
|
||||
</dict>
|
||||
@@ -225,6 +318,10 @@ function initGenericProject({
|
||||
'/repo'
|
||||
);
|
||||
|
||||
return fakeExp;
|
||||
}
|
||||
|
||||
function initManagedProject(): ExpoConfig {
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
@@ -233,31 +330,15 @@ function initGenericProject({
|
||||
buildNumber: '1',
|
||||
},
|
||||
};
|
||||
return fakeExp;
|
||||
}
|
||||
|
||||
function initManagedProject(): ExpoConfig {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: {
|
||||
version: '1.0.0',
|
||||
ios: {
|
||||
buildNumber: '1',
|
||||
},
|
||||
},
|
||||
expo: fakeExp,
|
||||
}),
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '1.0.0',
|
||||
ios: {
|
||||
buildNumber: '1',
|
||||
},
|
||||
};
|
||||
return fakeExp;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import { IosCredentials } from '../../credentials/ios/types';
|
||||
import { IosCredentials, Target } from '../../credentials/ios/types';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
|
||||
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
|
||||
@@ -16,6 +17,12 @@ 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> {
|
||||
@@ -43,6 +50,7 @@ export async function prepareIosBuildAsync(
|
||||
},
|
||||
xcodeBuildContext
|
||||
);
|
||||
const buildConfiguration = resolveBuildConfiguration(ctx, targets);
|
||||
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx,
|
||||
@@ -54,24 +62,11 @@ export async function prepareIosBuildAsync(
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
buildProfile,
|
||||
buildSettings: buildConfiguration.buildSettings,
|
||||
});
|
||||
},
|
||||
getMetadataContext: (): IosMetadataContext => {
|
||||
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,
|
||||
};
|
||||
return buildConfiguration;
|
||||
},
|
||||
prepareJobAsync: async (
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
@@ -94,3 +89,24 @@ 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,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { IosBuildProfile, VersionAutoIncrement } from '@expo/eas-json';
|
||||
import { IosBuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import Log from '../../log';
|
||||
import {
|
||||
@@ -31,10 +32,12 @@ 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;
|
||||
@@ -44,13 +47,13 @@ export async function validateAndSyncProjectConfigurationAsync({
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
|
||||
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, buildSettings });
|
||||
} else {
|
||||
await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
|
||||
}
|
||||
}
|
||||
|
||||
function resolveVersionBumpStrategy(autoIncrement: VersionAutoIncrement): BumpStrategy {
|
||||
function resolveVersionBumpStrategy(autoIncrement: IosVersionAutoIncrement): BumpStrategy {
|
||||
if (autoIncrement === true) {
|
||||
return BumpStrategy.BUILD_NUMBER;
|
||||
} else if (autoIncrement === false) {
|
||||
@@ -58,6 +61,6 @@ function resolveVersionBumpStrategy(autoIncrement: VersionAutoIncrement): BumpSt
|
||||
} else if (autoIncrement === 'buildNumber') {
|
||||
return BumpStrategy.BUILD_NUMBER;
|
||||
} else {
|
||||
return BumpStrategy.SHORT_VERSION;
|
||||
return BumpStrategy.APP_VERSION;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
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';
|
||||
@@ -17,13 +16,8 @@ export async function ensureIosCredentialsAsync(
|
||||
return;
|
||||
}
|
||||
|
||||
const credentialsCtx = await createCredentialsContextAsync(buildCtx.projectDir, {
|
||||
exp: buildCtx.exp,
|
||||
nonInteractive: buildCtx.nonInteractive,
|
||||
});
|
||||
|
||||
const provider = new IosCredentialsProvider(credentialsCtx, {
|
||||
app: getAppFromContext(credentialsCtx),
|
||||
const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, {
|
||||
app: getAppFromContext(buildCtx.credentialsCtx),
|
||||
targets,
|
||||
iosCapabilitiesOptions: {
|
||||
entitlements: await resolveEntitlementsJsonAsync(buildCtx.projectDir, buildCtx.workflow),
|
||||
|
||||
@@ -2,18 +2,18 @@ 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 nullthrows from 'nullthrows';
|
||||
import semver from 'semver';
|
||||
import path from 'path';
|
||||
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';
|
||||
|
||||
export enum BumpStrategy {
|
||||
SHORT_VERSION,
|
||||
APP_VERSION,
|
||||
BUILD_NUMBER,
|
||||
NOOP,
|
||||
}
|
||||
@@ -22,19 +22,21 @@ 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);
|
||||
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
|
||||
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
|
||||
Log.log('Updated versions in app.json');
|
||||
await writeVersionsToInfoPlistAsync({ projectDir, exp, infoPlist });
|
||||
await writeVersionsToInfoPlistAsync({ projectDir, exp, infoPlist, buildSettings });
|
||||
Log.log('Synchronized versions with Info.plist');
|
||||
}
|
||||
|
||||
@@ -52,29 +54,9 @@ export async function bumpVersionInAppJsonAsync({
|
||||
}
|
||||
ensureStaticConfigExists(projectDir);
|
||||
Log.addNewLineIfNone();
|
||||
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;
|
||||
});
|
||||
}
|
||||
if (bumpStrategy === BumpStrategy.APP_VERSION) {
|
||||
const appVersion = IOSConfig.Version.getVersion(exp);
|
||||
await bumpAppVersionAsync({ appVersion, projectDir, exp });
|
||||
} else {
|
||||
const buildNumber = IOSConfig.Version.getBuildNumber(exp);
|
||||
if (buildNumber.match(/^\d+(\.\d+)*$/)) {
|
||||
@@ -110,7 +92,7 @@ export async function readShortVersionAsync(
|
||||
): Promise<string | undefined> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const infoPlist = await readInfoPlistAsync(projectDir);
|
||||
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
|
||||
return (
|
||||
infoPlist.CFBundleShortVersionString &&
|
||||
evaluateTemplateString(infoPlist.CFBundleShortVersionString, buildSettings)
|
||||
@@ -127,7 +109,7 @@ export async function readBuildNumberAsync(
|
||||
): Promise<string | undefined> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const infoPlist = await readInfoPlistAsync(projectDir);
|
||||
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
|
||||
return (
|
||||
infoPlist.CFBundleVersion && evaluateTemplateString(infoPlist.CFBundleVersion, buildSettings)
|
||||
);
|
||||
@@ -155,30 +137,56 @@ 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 });
|
||||
await writeInfoPlistAsync({ projectDir, infoPlist: updatedInfoPlist, buildSettings });
|
||||
return updatedInfoPlist;
|
||||
}
|
||||
|
||||
async function readInfoPlistAsync(projectDir: string): Promise<IOSConfig.InfoPlist> {
|
||||
const infoPlistPath = IOSConfig.Paths.getInfoPlistPath(projectDir);
|
||||
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);
|
||||
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 = IOSConfig.Paths.getInfoPlistPath(projectDir);
|
||||
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
|
||||
await writePlistAsync(infoPlistPath, infoPlist);
|
||||
}
|
||||
|
||||
@@ -190,5 +198,12 @@ function ensureStaticConfigExists(projectDir: string): void {
|
||||
}
|
||||
|
||||
export function evaluateTemplateString(s: string, vars: Record<string, any>): string {
|
||||
return s.replace(/\$\((\w+)\)/g, (match, key) => (vars.hasOwnProperty(key) ? vars[key] : match));
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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,3 +1,4 @@
|
||||
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';
|
||||
@@ -31,11 +32,15 @@ afterAll(() => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
(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();
|
||||
asMock(getUserAsync)
|
||||
.mockReset()
|
||||
.mockImplementation(() => mockJester);
|
||||
asMock(ensureLoggedInAsync)
|
||||
.mockReset()
|
||||
.mockImplementation(() => mockJester);
|
||||
asMock(initAsync).mockReset();
|
||||
asMock(flushAsync).mockReset();
|
||||
asMock(logEvent).mockReset();
|
||||
});
|
||||
|
||||
describe(EasCommand.name, () => {
|
||||
@@ -54,7 +59,7 @@ describe(EasCommand.name, () => {
|
||||
}, 15_000);
|
||||
|
||||
it('ensures the user data is read from cache', async () => {
|
||||
(TestEasCommand.prototype.authValue as jest.Mock).mockImplementationOnce(() => false);
|
||||
asMock(TestEasCommand.prototype.authValue).mockImplementationOnce(() => false);
|
||||
|
||||
await TestEasCommand.run();
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
PublishPlatform,
|
||||
buildBundlesAsync,
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssets,
|
||||
collectAssetsAsync,
|
||||
uploadAssetsAsync,
|
||||
} from '../../project/publish';
|
||||
import { promptAsync, selectAsync } from '../../prompts';
|
||||
@@ -290,7 +290,7 @@ export default class BranchPublish extends EasCommand {
|
||||
const assetSpinner = ora('Uploading assets...').start();
|
||||
try {
|
||||
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
|
||||
const assets = collectAssets({ inputDir: inputDir!, platforms });
|
||||
const assets = await collectAssetsAsync({ inputDir: inputDir!, platforms });
|
||||
await uploadAssetsAsync(assets);
|
||||
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
|
||||
assetSpinner.succeed('Uploaded assets!');
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
@@ -18,7 +19,13 @@ import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from '../../build
|
||||
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
|
||||
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { BuildFragment, BuildStatus, SubmissionFragment } from '../../graphql/generated';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildStatus,
|
||||
SubmissionFragment,
|
||||
} from '../../graphql/generated';
|
||||
import { toAppPlatform, toPlatform } from '../../graphql/types/AppPlatform';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
@@ -35,7 +42,7 @@ import {
|
||||
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import { createSubmissionContext } from '../../submit/context';
|
||||
import { createSubmissionContextAsync } from '../../submit/context';
|
||||
import {
|
||||
submitAsync,
|
||||
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
|
||||
@@ -153,8 +160,9 @@ export default class Build extends EasCommand {
|
||||
});
|
||||
|
||||
const startedBuilds: BuildFragment[] = [];
|
||||
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
|
||||
for (const platform of platforms) {
|
||||
const maybeBuild = await this.prepareAndStartBuildAsync({
|
||||
const { build: maybeBuild, buildCtx } = await this.prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
platform,
|
||||
flags,
|
||||
@@ -163,6 +171,7 @@ export default class Build extends EasCommand {
|
||||
if (maybeBuild) {
|
||||
startedBuilds.push(maybeBuild);
|
||||
}
|
||||
buildCtxByPlatform[toAppPlatform(platform)] = buildCtx;
|
||||
}
|
||||
if (flags.local) {
|
||||
return;
|
||||
@@ -176,10 +185,11 @@ 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);
|
||||
}
|
||||
@@ -253,17 +263,17 @@ export default class Build extends EasCommand {
|
||||
};
|
||||
}
|
||||
|
||||
private async prepareAndStartBuildAsync({
|
||||
private async prepareAndStartBuildAsync<T extends Platform>({
|
||||
projectDir,
|
||||
platform,
|
||||
flags,
|
||||
moreBuilds,
|
||||
}: {
|
||||
projectDir: string;
|
||||
platform: Platform;
|
||||
platform: T;
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
}): Promise<BuildFragment | undefined> {
|
||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<T> }> {
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, flags.profile);
|
||||
|
||||
@@ -297,7 +307,11 @@ export default class Build extends EasCommand {
|
||||
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
|
||||
}
|
||||
|
||||
return await this.startBuildAsync(buildCtx);
|
||||
const build = await this.startBuildAsync(buildCtx);
|
||||
return {
|
||||
build,
|
||||
buildCtx,
|
||||
};
|
||||
}
|
||||
|
||||
private async startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
|
||||
@@ -311,21 +325,23 @@ 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 = createSubmissionContext({
|
||||
const submissionCtx = await createSubmissionContextAsync({
|
||||
platform,
|
||||
projectDir,
|
||||
projectId: build.project.id,
|
||||
@@ -333,6 +349,7 @@ export default class Build extends EasCommand {
|
||||
archiveFlags: { id: build.id },
|
||||
nonInteractive: flags.nonInteractive,
|
||||
env: buildProfile.env,
|
||||
credentialsCtx,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { createCredentialsContextAsync } from '../credentials/context';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
|
||||
export default class Credentials extends EasCommand {
|
||||
static description = 'manage your credentials';
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const ctx = await createCredentialsContextAsync(process.cwd(), {});
|
||||
const ctx = new CredentialsContext({
|
||||
projectDir: process.cwd(),
|
||||
user: await ensureLoggedInAsync(),
|
||||
});
|
||||
await new SelectPlatform().runAsync(ctx);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from '../platform';
|
||||
import { isEasEnabledForProjectAsync, warnEasUnavailable } from '../project/isEasEnabledForProject';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { SubmitArchiveFlags, createSubmissionContext } from '../submit/context';
|
||||
import { SubmitArchiveFlags, createSubmissionContextAsync } from '../submit/context';
|
||||
import { submitAsync, waitToCompleteAsync } from '../submit/submit';
|
||||
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
|
||||
|
||||
@@ -108,7 +108,7 @@ See how to configure submits with eas.json: ${learnMore('https://docs.expo.dev/s
|
||||
const submissions: SubmissionFragment[] = [];
|
||||
for (const platform of platforms) {
|
||||
const profile = await easJsonReader.readSubmitProfileAsync(platform, flags.profile);
|
||||
const ctx = createSubmissionContext({
|
||||
const ctx = await createSubmissionContextAsync({
|
||||
platform,
|
||||
projectDir,
|
||||
projectId,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
AndroidKeystoreType,
|
||||
AppFragment,
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
GoogleServiceAccountKeyFragment,
|
||||
} from '../../graphql/generated';
|
||||
import * as AndroidGraphqlClient from '../android/api/GraphqlClient';
|
||||
import { Keystore } from '../android/credentials';
|
||||
@@ -68,6 +69,16 @@ 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,
|
||||
@@ -113,6 +124,7 @@ export const testAndroidAppCredentialsFragment: CommonAndroidAppCredentialsFragm
|
||||
isLegacy: false,
|
||||
app: testAppFragment,
|
||||
androidFcm: testLegacyAndroidFcmFragment,
|
||||
googleServiceAccountKeyForSubmissions: testGoogleServiceAccountKeyFragment,
|
||||
androidAppBuildCredentialsList: [testLegacyAndroidBuildCredentialsFragment],
|
||||
};
|
||||
|
||||
@@ -130,6 +142,9 @@ 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 { Context } from '../context';
|
||||
import { CredentialsContext } 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> = {}): Context {
|
||||
export function createCtxMock(mockOverride: Record<string, any> = {}): CredentialsContext {
|
||||
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 { Context } from '../context';
|
||||
import { CredentialsContext } 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: Context, private options: Options) {}
|
||||
constructor(private ctx: CredentialsContext, 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 { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
|
||||
export class AssignFcm {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
public async runAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
fcm: AndroidFcmFragment
|
||||
): Promise<CommonAndroidAppCredentialsFragment> {
|
||||
const appCredentials =
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
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 { Context } from '../../context';
|
||||
import { CredentialsContext } 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: Context,
|
||||
ctx: CredentialsContext,
|
||||
app: AppLookupParams
|
||||
): Promise<boolean> {
|
||||
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(app);
|
||||
@@ -30,7 +30,7 @@ export async function canCopyLegacyCredentialsAsync(
|
||||
}
|
||||
|
||||
export async function promptUserAndCopyLegacyCredentialsAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
app: AppLookupParams
|
||||
): Promise<void> {
|
||||
assert(
|
||||
@@ -85,7 +85,7 @@ export async function promptUserAndCopyLegacyCredentialsAsync(
|
||||
}
|
||||
|
||||
export async function getAppLookupParamsFromContextAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
gradleContext?: GradleBuildContext
|
||||
): Promise<AppLookupParams> {
|
||||
ctx.ensureProjectContext();
|
||||
@@ -113,7 +113,7 @@ export async function getAppLookupParamsFromContextAsync(
|
||||
}
|
||||
|
||||
export async function createOrUpdateDefaultAndroidAppBuildCredentialsAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
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 { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
|
||||
export class CreateFcm {
|
||||
constructor(private account: Account) {}
|
||||
|
||||
public async runAsync(ctx: Context): Promise<AndroidFcmFragment> {
|
||||
public async runAsync(ctx: CredentialsContext): Promise<AndroidFcmFragment> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(`FCM API Key cannot be uploaded in non-interactive mode.`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
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 { Context } from '../../context';
|
||||
import { CredentialsContext } 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: Context): Promise<AndroidKeystoreFragment> {
|
||||
public async runAsync(ctx: CredentialsContext): 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 { Context } from '../../context';
|
||||
import { CredentialsContext } 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: Context,
|
||||
ctx: CredentialsContext,
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment
|
||||
): Promise<void> {
|
||||
const keystore = buildCredentials.androidKeystore;
|
||||
@@ -65,7 +65,7 @@ export class BackupKeystore {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
public async runAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment
|
||||
): Promise<void> {
|
||||
if (!buildCredentials.androidKeystore) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { AppLookupParams, formatProjectFullName } from '../api/GraphqlClient';
|
||||
|
||||
export class RemoveFcm {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
async runAsync(ctx: Context): Promise<void> {
|
||||
async runAsync(ctx: CredentialsContext): Promise<void> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(
|
||||
"Deleting an FCM Api Key is a destructive operation. Start the CLI without the '--non-interactive' flag to delete the credentials."
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
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 { Context } from '../../context';
|
||||
import { CredentialsContext } 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: Context,
|
||||
ctx: CredentialsContext,
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment
|
||||
): Promise<void> {
|
||||
if (ctx.nonInteractive) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { ora } from '../../../ora';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } 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: Context): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
async runAsync(ctx: CredentialsContext): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
const { app, name: maybeName } = this.options;
|
||||
|
||||
if (!ctx.nonInteractive) {
|
||||
@@ -62,7 +62,7 @@ export class SetupBuildCredentials {
|
||||
name,
|
||||
keystore,
|
||||
}: {
|
||||
ctx: Context;
|
||||
ctx: CredentialsContext;
|
||||
app: AppLookupParams;
|
||||
name?: string;
|
||||
keystore: AndroidKeystoreFragment;
|
||||
@@ -82,7 +82,7 @@ export class SetupBuildCredentials {
|
||||
app,
|
||||
name,
|
||||
}: {
|
||||
ctx: Context;
|
||||
ctx: CredentialsContext;
|
||||
app: AppLookupParams;
|
||||
name?: string;
|
||||
}): Promise<AndroidAppBuildCredentialsFragment | null> {
|
||||
@@ -108,7 +108,7 @@ export class SetupBuildCredentials {
|
||||
app,
|
||||
name,
|
||||
}: {
|
||||
ctx: Context;
|
||||
ctx: CredentialsContext;
|
||||
app: AppLookupParams;
|
||||
name: string;
|
||||
}): Promise<AndroidAppBuildCredentialsFragment | null> {
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import {
|
||||
} from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } 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: Context): Promise<AndroidAppBuildCredentialsFragment | null> {
|
||||
async runAsync(ctx: CredentialsContext): Promise<AndroidAppBuildCredentialsFragment | null> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(
|
||||
'Setting up build credentials from credentials.json is only available in interactive mode'
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
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> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new MissingCredentialsNonInteractiveError(
|
||||
'Google Service Account Keys cannot be set up in --non-interactive mode.'
|
||||
);
|
||||
}
|
||||
|
||||
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'
|
||||
);
|
||||
}
|
||||
|
||||
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 { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { updateAndroidCredentialsAsync } from '../../credentialsJson/update';
|
||||
|
||||
export class UpdateCredentialsJson {
|
||||
async runAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment
|
||||
): Promise<void> {
|
||||
Log.log('Updating Android credentials in credentials.json');
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
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 as any
|
||||
ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync
|
||||
).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateAndroidAppCredentialsAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateAndroidAppCredentialsAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('works in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
+8
-7
@@ -1,3 +1,4 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { confirmAsync, promptAsync } from '../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
|
||||
jest.mock('../../../../ora');
|
||||
jest.mock('../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
|
||||
describe('BuildCredentialsUtils', () => {
|
||||
describe(canCopyLegacyCredentialsAsync, () => {
|
||||
@@ -83,7 +84,7 @@ describe('BuildCredentialsUtils', () => {
|
||||
});
|
||||
describe(promptUserAndCopyLegacyCredentialsAsync, () => {
|
||||
it('copies all legacy credentials to EAS if the user is eligible', async () => {
|
||||
(promptAsync as jest.Mock).mockImplementation(() => ({ providedName: 'test-provided-name' }));
|
||||
asMock(promptAsync).mockImplementation(() => ({ providedName: 'test-provided-name' }));
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
android: {
|
||||
@@ -103,12 +104,12 @@ describe('BuildCredentialsUtils', () => {
|
||||
await promptUserAndCopyLegacyCredentialsAsync(ctx, appLookupParams);
|
||||
|
||||
expect(
|
||||
ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync as any
|
||||
ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync
|
||||
).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);
|
||||
expect(ctx.android.createFcmAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateAndroidAppCredentialsAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createAndroidAppBuildCredentialsAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
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');
|
||||
(promptAsync as jest.Mock).mockImplementation(() => ({ fcmApiKey: 'blah' }));
|
||||
asMock(promptAsync).mockImplementation(() => ({ fcmApiKey: 'blah' }));
|
||||
|
||||
describe(CreateFcm, () => {
|
||||
it('creates an fcm api key in Interactive Mode', async () => {
|
||||
@@ -14,7 +15,7 @@ describe(CreateFcm, () => {
|
||||
await createFcmAction.runAsync(ctx);
|
||||
|
||||
// expect fcm api key to be created on expo servers
|
||||
expect(ctx.android.createFcmAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createFcmAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: true });
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
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',
|
||||
}),
|
||||
});
|
||||
|
||||
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 as any).toHaveBeenCalledTimes(1);
|
||||
expect(fsWriteFileSpy as any).toHaveBeenCalledWith(
|
||||
expect(fsWriteFileSpy).toHaveBeenCalledTimes(1);
|
||||
expect(fsWriteFileSpy).toHaveBeenCalledWith(
|
||||
`@${appLookupParams.account.name}__${appLookupParams.projectName}.jks`,
|
||||
testAndroidBuildCredentialsFragment.androidKeystore?.keystore,
|
||||
'base64'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
@@ -8,7 +9,7 @@ import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { RemoveFcm } from '../RemoveFcm';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
|
||||
describe(RemoveFcm, () => {
|
||||
it('removes an FCM Api Key', async () => {
|
||||
@@ -24,7 +25,7 @@ describe(RemoveFcm, () => {
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
const removeFcmApiKeyAction = new RemoveFcm(appLookupParams);
|
||||
await removeFcmApiKeyAction.runAsync(ctx);
|
||||
expect(ctx.android.deleteFcmAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.deleteFcmAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: true });
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
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,3 +1,4 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
@@ -6,7 +7,7 @@ import { RemoveKeystore } from '../RemoveKeystore';
|
||||
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
|
||||
describe(RemoveKeystore, () => {
|
||||
it('removes a keystore', async () => {
|
||||
@@ -16,7 +17,7 @@ describe(RemoveKeystore, () => {
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
const removeKeystoreAction = new RemoveKeystore(appLookupParams);
|
||||
await removeKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment);
|
||||
expect(ctx.android.deleteKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.deleteKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: true });
|
||||
|
||||
+10
-9
@@ -1,5 +1,6 @@
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { AndroidKeystoreType } from '../../../../graphql/generated';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import {
|
||||
@@ -18,7 +19,7 @@ import { SetupBuildCredentialsFromCredentialsJson } from '../SetupBuildCredentia
|
||||
jest.mock('fs');
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
jest.mock('../../../manager/SelectAndroidBuildCredentials');
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -27,7 +28,7 @@ beforeEach(() => {
|
||||
|
||||
describe(SetupBuildCredentialsFromCredentialsJson, () => {
|
||||
it('sets up a new build configuration from credentials.json upon user request', async () => {
|
||||
(SelectAndroidBuildCredentials as any).mockImplementation(() => {
|
||||
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
|
||||
return {
|
||||
runAsync: () => {
|
||||
return {
|
||||
@@ -64,8 +65,8 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
|
||||
// expect keystore to be created with credentials.json content
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createKeystoreAsync as any).toBeCalledWith(appLookupParams.account, {
|
||||
expect(ctx.android.createKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createKeystoreAsync).toBeCalledWith(appLookupParams.account, {
|
||||
keystorePassword: testJksAndroidKeystoreFragment.keystorePassword,
|
||||
keyAlias: testJksAndroidKeystoreFragment.keyAlias,
|
||||
keyPassword: testJksAndroidKeystoreFragment.keyPassword,
|
||||
@@ -74,10 +75,10 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
|
||||
});
|
||||
|
||||
// expect new build credentials to be created
|
||||
expect(ctx.android.createAndroidAppBuildCredentialsAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createAndroidAppBuildCredentialsAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('uses an existing build configuration upon user request', async () => {
|
||||
(SelectAndroidBuildCredentials as any).mockImplementation(() => {
|
||||
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
|
||||
return {
|
||||
runAsync: () => {
|
||||
return {
|
||||
@@ -114,8 +115,8 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
|
||||
// expect keystore to be created with credentials.json content
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createKeystoreAsync as any).toBeCalledWith(appLookupParams.account, {
|
||||
expect(ctx.android.createKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createKeystoreAsync).toBeCalledWith(appLookupParams.account, {
|
||||
keystorePassword: testJksAndroidKeystoreFragment.keystorePassword,
|
||||
keyAlias: testJksAndroidKeystoreFragment.keyAlias,
|
||||
keyPassword: testJksAndroidKeystoreFragment.keyPassword,
|
||||
@@ -124,7 +125,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
|
||||
});
|
||||
|
||||
// expect existing build credentials to be updated
|
||||
expect(ctx.android.updateAndroidAppBuildCredentialsAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateAndroidAppBuildCredentialsAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
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',
|
||||
}),
|
||||
});
|
||||
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
|
||||
);
|
||||
});
|
||||
});
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
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,15 +5,18 @@ import {
|
||||
AndroidKeystoreFragment,
|
||||
AppFragment,
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
GoogleServiceAccountKeyFragment,
|
||||
} from '../../../graphql/generated';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { AppQuery } from '../../ios/api/graphql/queries/AppQuery';
|
||||
import { KeystoreWithType } from '../credentials';
|
||||
import { GoogleServiceAccountKey, 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;
|
||||
@@ -87,11 +90,27 @@ export async function updateAndroidAppCredentialsAsync(
|
||||
appCredentials: CommonAndroidAppCredentialsFragment,
|
||||
{
|
||||
androidFcmId,
|
||||
googleServiceAccountKeyForSubmissionsId,
|
||||
}: {
|
||||
androidFcmId: string;
|
||||
androidFcmId?: string;
|
||||
googleServiceAccountKeyForSubmissionsId?: string;
|
||||
}
|
||||
): Promise<CommonAndroidAppCredentialsFragment> {
|
||||
return await AndroidAppCredentialsMutation.setFcmKeyAsync(appCredentials.id, androidFcmId);
|
||||
let updatedAppCredentials = appCredentials;
|
||||
if (androidFcmId) {
|
||||
updatedAppCredentials = await AndroidAppCredentialsMutation.setFcmKeyAsync(
|
||||
appCredentials.id,
|
||||
androidFcmId
|
||||
);
|
||||
}
|
||||
if (googleServiceAccountKeyForSubmissionsId) {
|
||||
updatedAppCredentials =
|
||||
await AndroidAppCredentialsMutation.setGoogleServiceAccountKeyForSubmissionsAsync(
|
||||
appCredentials.id,
|
||||
googleServiceAccountKeyForSubmissionsId
|
||||
);
|
||||
}
|
||||
return updatedAppCredentials;
|
||||
}
|
||||
|
||||
export async function updateAndroidAppBuildCredentialsAsync(
|
||||
@@ -223,6 +242,30 @@ 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);
|
||||
|
||||
+1
-3
@@ -15,7 +15,7 @@ export type AndroidAppBuildCredentialsMetadataInput = Omit<
|
||||
AndroidAppBuildCredentialsInput,
|
||||
'keystoreId'
|
||||
>;
|
||||
const AndroidAppBuildCredentialsMutation = {
|
||||
export const AndroidAppBuildCredentialsMutation = {
|
||||
async createAndroidAppBuildCredentialsAsync(
|
||||
androidAppBuildCredentialsInput: AndroidAppBuildCredentialsInput,
|
||||
androidAppCredentialsId: string
|
||||
@@ -85,5 +85,3 @@ const AndroidAppBuildCredentialsMutation = {
|
||||
return data.androidAppBuildCredentials.setKeystore;
|
||||
},
|
||||
};
|
||||
|
||||
export { AndroidAppBuildCredentialsMutation };
|
||||
+39
-3
@@ -7,10 +7,11 @@ import {
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
CreateAndroidAppCredentialsMutation,
|
||||
SetFcmMutation,
|
||||
SetGoogleServiceAccountKeyForSubmissionsMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { CommonAndroidAppCredentialsFragmentNode } from '../../../../../graphql/types/credentials/AndroidAppCredentials';
|
||||
|
||||
const AndroidAppCredentialsMutation = {
|
||||
export const AndroidAppCredentialsMutation = {
|
||||
async createAndroidAppCredentialsAsync(
|
||||
androidAppCredentialsInput: {
|
||||
fcmId?: string;
|
||||
@@ -82,6 +83,41 @@ const AndroidAppCredentialsMutation = {
|
||||
assert(data.androidAppCredentials.setFcm, 'GraphQL: `setFcm` not defined in server response');
|
||||
return data.androidAppCredentials.setFcm;
|
||||
},
|
||||
async setGoogleServiceAccountKeyForSubmissionsAsync(
|
||||
androidAppCredentialsId: string,
|
||||
googleServiceAccountKeyId: string
|
||||
): Promise<CommonAndroidAppCredentialsFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<SetGoogleServiceAccountKeyForSubmissionsMutation>(
|
||||
gql`
|
||||
mutation SetGoogleServiceAccountKeyForSubmissionsMutation(
|
||||
$androidAppCredentialsId: ID!
|
||||
$googleServiceAccountKeyId: ID!
|
||||
) {
|
||||
androidAppCredentials {
|
||||
setGoogleServiceAccountKeyForSubmissions(
|
||||
id: $androidAppCredentialsId
|
||||
googleServiceAccountKeyId: $googleServiceAccountKeyId
|
||||
) {
|
||||
id
|
||||
...CommonAndroidAppCredentialsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(CommonAndroidAppCredentialsFragmentNode)}
|
||||
`,
|
||||
{
|
||||
androidAppCredentialsId,
|
||||
googleServiceAccountKeyId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(
|
||||
data.androidAppCredentials.setGoogleServiceAccountKeyForSubmissions,
|
||||
'GraphQL: `setGoogleServiceAccountKeyForSubmissions` not defined in server response'
|
||||
);
|
||||
return data.androidAppCredentials.setGoogleServiceAccountKeyForSubmissions;
|
||||
},
|
||||
};
|
||||
|
||||
export { AndroidAppCredentialsMutation };
|
||||
+1
-3
@@ -11,7 +11,7 @@ import {
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AndroidFcmFragmentNode } from '../../../../../graphql/types/credentials/AndroidFcm';
|
||||
|
||||
const AndroidFcmMutation = {
|
||||
export const AndroidFcmMutation = {
|
||||
async createAndroidFcmAsync(
|
||||
androidFcmInput: AndroidFcmInput,
|
||||
accountId: string
|
||||
@@ -67,5 +67,3 @@ const AndroidFcmMutation = {
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export { AndroidFcmMutation };
|
||||
+1
-3
@@ -11,7 +11,7 @@ import {
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AndroidKeystoreFragmentNode } from '../../../../../graphql/types/credentials/AndroidKeystore';
|
||||
|
||||
const AndroidKeystoreMutation = {
|
||||
export const AndroidKeystoreMutation = {
|
||||
async createAndroidKeystoreAsync(
|
||||
androidKeystoreInput: AndroidKeystoreInput,
|
||||
accountId: string
|
||||
@@ -73,5 +73,3 @@ const AndroidKeystoreMutation = {
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export { AndroidKeystoreMutation };
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import {
|
||||
CreateGoogleServiceAccountKeyMutation,
|
||||
DeleteGoogleServiceAccountKeyMutation,
|
||||
GoogleServiceAccountKeyFragment,
|
||||
GoogleServiceAccountKeyInput,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { GoogleServiceAccountKeyFragmentNode } from '../../../../../graphql/types/credentials/GoogleServiceAccountKey';
|
||||
|
||||
export const GoogleServiceAccountKeyMutation = {
|
||||
async createGoogleServiceAccountKeyAsync(
|
||||
googleServiceAccountKeyInput: GoogleServiceAccountKeyInput,
|
||||
accountId: string
|
||||
): Promise<GoogleServiceAccountKeyFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateGoogleServiceAccountKeyMutation>(
|
||||
gql`
|
||||
mutation CreateGoogleServiceAccountKeyMutation(
|
||||
$googleServiceAccountKeyInput: GoogleServiceAccountKeyInput!
|
||||
$accountId: ID!
|
||||
) {
|
||||
googleServiceAccountKey {
|
||||
createGoogleServiceAccountKey(
|
||||
googleServiceAccountKeyInput: $googleServiceAccountKeyInput
|
||||
accountId: $accountId
|
||||
) {
|
||||
id
|
||||
...GoogleServiceAccountKeyFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(GoogleServiceAccountKeyFragmentNode)}
|
||||
`,
|
||||
{
|
||||
googleServiceAccountKeyInput,
|
||||
accountId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(
|
||||
data.googleServiceAccountKey.createGoogleServiceAccountKey,
|
||||
'GraphQL: `createAndroidFcm` not defined in server response'
|
||||
);
|
||||
return data.googleServiceAccountKey.createGoogleServiceAccountKey;
|
||||
},
|
||||
async deleteGoogleServiceAccountKeyAsync(googleServiceAccountKeyId: string): Promise<void> {
|
||||
await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<DeleteGoogleServiceAccountKeyMutation>(
|
||||
gql`
|
||||
mutation DeleteGoogleServiceAccountKeyMutation($googleServiceAccountKeyId: ID!) {
|
||||
googleServiceAccountKey {
|
||||
deleteGoogleServiceAccountKey(id: $googleServiceAccountKeyId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
googleServiceAccountKeyId,
|
||||
},
|
||||
{
|
||||
additionalTypenames: ['GoogleServiceAccountKey', 'AndroidAppCredentials'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
},
|
||||
};
|
||||
+1
-3
@@ -9,7 +9,7 @@ import {
|
||||
} from '../../../../../graphql/generated';
|
||||
import { CommonAndroidAppCredentialsFragmentNode } from '../../../../../graphql/types/credentials/AndroidAppCredentials';
|
||||
|
||||
const AndroidAppCredentialsQuery = {
|
||||
export const AndroidAppCredentialsQuery = {
|
||||
async withCommonFieldsByApplicationIdentifierAsync(
|
||||
projectFullName: string,
|
||||
{
|
||||
@@ -66,5 +66,3 @@ const AndroidAppCredentialsQuery = {
|
||||
return data.app.byFullName.androidAppCredentials[0] ?? null;
|
||||
},
|
||||
};
|
||||
|
||||
export { AndroidAppCredentialsQuery };
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import {
|
||||
GoogleServiceAccountKeyByAccountQuery,
|
||||
GoogleServiceAccountKeyFragment,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { GoogleServiceAccountKeyFragmentNode } from '../../../../../graphql/types/credentials/GoogleServiceAccountKey';
|
||||
|
||||
export const GoogleServiceAccountKeyQuery = {
|
||||
async getAllForAccountAsync(accountName: string): Promise<GoogleServiceAccountKeyFragment[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<GoogleServiceAccountKeyByAccountQuery>(
|
||||
gql`
|
||||
query GoogleServiceAccountKeyByAccountQuery($accountName: String!) {
|
||||
account {
|
||||
byName(accountName: $accountName) {
|
||||
id
|
||||
googleServiceAccountKeys {
|
||||
id
|
||||
...GoogleServiceAccountKeyFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(GoogleServiceAccountKeyFragmentNode)}
|
||||
`,
|
||||
{
|
||||
accountName,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.account.byName.googleServiceAccountKeys;
|
||||
},
|
||||
};
|
||||
@@ -16,6 +16,11 @@ export type KeystoreWithType = Keystore & {
|
||||
type: AndroidKeystoreType;
|
||||
};
|
||||
|
||||
export type GoogleServiceAccountKey = {
|
||||
[key: string]: any;
|
||||
private_key: string;
|
||||
};
|
||||
|
||||
export type AndroidCredentials = {
|
||||
experienceName: string;
|
||||
keystore: Keystore | null;
|
||||
|
||||
+15
-1
@@ -1,3 +1,17 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`print credentials prints the AndroidAppCredentials fragment 1`] = `"Android Credentials Project: testApp Application Identifier: test.com.app Push Notifications (FCM): Key: abcd...efgh Updated 0 second ago Configuration: legacy (Default) Keystore: Type: JKS Key Alias: QHdrb3p5cmEvY3JlZGVudGlhbHMtdGVzdA== MD5 Fingerprint: TE:ST:-M:D5 SHA1 Fingerprint: TE:ST:-S:HA:1 SHA256 Fingerprint: TE:ST:-S:HA:25:6 Updated 0 second ago"`;
|
||||
exports[`print credentials prints the AndroidAppCredentials fragment 1`] = `
|
||||
"Android Credentials
|
||||
Project testApp
|
||||
Application Identifier test.com.appPush Notifications (FCM) Key abcd...efgh
|
||||
Updated 0 second agoGoogle Service Account Key For Submissions Project ID sdf.sdf.sdf
|
||||
Client Email [email protected]
|
||||
Client ID test-client-identifier
|
||||
Private Key ID test-private-key-identifier
|
||||
Updated 0 second agoConfiguration: legacy (Default) Keystore Type JKS
|
||||
Key Alias QHdrb3p5cmEvY3JlZGVudGlhbHMtdGVzdA==
|
||||
MD5 Fingerprint TE:ST:-M:D5
|
||||
SHA1 Fingerprint TE:ST:-S:HA:1
|
||||
SHA256 Fingerprint TE:ST:-S:HA:25:6
|
||||
Updated 0 second ago"
|
||||
`;
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { confirmAsync, promptAsync } from '../../../../prompts';
|
||||
import { detectGoogleServiceAccountKeyPathAsync } from '../googleServiceAccountKey';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('../../../../prompts');
|
||||
|
||||
beforeAll(() => {
|
||||
const mockDetectableServiceAccountJson = JSON.stringify({
|
||||
type: 'service_account',
|
||||
private_key: 'super secret',
|
||||
});
|
||||
|
||||
vol.fromJSON({
|
||||
'/project_dir/subdir/service-account.json': mockDetectableServiceAccountJson,
|
||||
'/project_dir/another-service-account.json': mockDetectableServiceAccountJson,
|
||||
'/other_dir/invalid_file.txt': 'this is not even a JSON',
|
||||
});
|
||||
});
|
||||
afterAll(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
asMock(promptAsync).mockClear();
|
||||
asMock(confirmAsync).mockClear();
|
||||
});
|
||||
|
||||
describe('Google Service Account Key path detection', () => {
|
||||
it('detects a single google-services file and prompts for confirmation', async () => {
|
||||
asMock(confirmAsync).mockResolvedValueOnce(true);
|
||||
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir/subdir');
|
||||
expect(confirmAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: 'Would you like to use this file?' })
|
||||
);
|
||||
expect(serviceAccountPath).toBe('/project_dir/subdir/service-account.json');
|
||||
});
|
||||
|
||||
it('returns null, when no valid files are found in the dir', async () => {
|
||||
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/other_dir'); // no valid files in that dir
|
||||
expect(promptAsync).not.toHaveBeenCalled();
|
||||
expect(serviceAccountPath).toBe(null);
|
||||
});
|
||||
|
||||
it('returns null, when user rejects to use detected file', async () => {
|
||||
asMock(confirmAsync).mockResolvedValueOnce(false);
|
||||
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir/subdir');
|
||||
expect(confirmAsync).toHaveBeenCalledTimes(1);
|
||||
expect(confirmAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: 'Would you like to use this file?' })
|
||||
);
|
||||
expect(serviceAccountPath).toBe(null);
|
||||
});
|
||||
|
||||
it('displays a chooser, when multiple files are found', async () => {
|
||||
asMock(promptAsync).mockResolvedValueOnce({
|
||||
selectedPath: '/project_dir/another-service-account.json',
|
||||
});
|
||||
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir'); // should find 2 files here
|
||||
|
||||
const expectedChoices = expect.arrayContaining([
|
||||
expect.objectContaining({ value: '/project_dir/another-service-account.json' }),
|
||||
expect.objectContaining({ value: '/project_dir/subdir/service-account.json' }),
|
||||
expect.objectContaining({ value: null }),
|
||||
]);
|
||||
expect(promptAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'select',
|
||||
choices: expectedChoices,
|
||||
})
|
||||
);
|
||||
expect(serviceAccountPath).toBe('/project_dir/another-service-account.json');
|
||||
});
|
||||
|
||||
it('returns null, when user selects the "None of above"', async () => {
|
||||
asMock(promptAsync).mockResolvedValueOnce({
|
||||
selectedPath: false,
|
||||
});
|
||||
|
||||
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir'); // should find 2 files here
|
||||
|
||||
expect(promptAsync).toHaveBeenCalledTimes(1);
|
||||
expect(promptAsync).toHaveBeenCalledWith(expect.objectContaining({ type: 'select' }));
|
||||
expect(serviceAccountPath).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,10 @@
|
||||
import mockdate from 'mockdate';
|
||||
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import Log from '../../../../log';
|
||||
import {
|
||||
testAndroidAppCredentialsFragment,
|
||||
testGoogleServiceAccountKeyFragment,
|
||||
testLegacyAndroidFcmFragment,
|
||||
} from '../../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
@@ -10,9 +12,10 @@ import { getAppLookupParamsFromContextAsync } from '../../actions/BuildCredentia
|
||||
import { displayAndroidAppCredentials } from '../printCredentials';
|
||||
|
||||
jest.mock('../../../../log');
|
||||
jest.mock('chalk', () => ({ bold: jest.fn(log => log) }));
|
||||
jest.mock('chalk', () => ({ bold: jest.fn(log => log), cyan: { bold: jest.fn(log => log) } }));
|
||||
|
||||
mockdate.set(testLegacyAndroidFcmFragment.updatedAt);
|
||||
mockdate.set(testGoogleServiceAccountKeyFragment.updatedAt);
|
||||
|
||||
describe('print credentials', () => {
|
||||
it('prints the AndroidAppCredentials fragment', async () => {
|
||||
@@ -22,9 +25,7 @@ describe('print credentials', () => {
|
||||
appLookupParams,
|
||||
appCredentials: testAndroidAppCredentialsFragment,
|
||||
});
|
||||
const loggedSoFar = (Log.log as jest.Mock).mock.calls.reduce(
|
||||
(acc, mockValue) => acc + mockValue
|
||||
);
|
||||
const loggedSoFar = asMock(Log.log).mock.calls.reduce((acc, mockValue) => acc + mockValue);
|
||||
expect(loggedSoFar).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
import JsonFile from '@expo/json-file';
|
||||
import chalk from 'chalk';
|
||||
import glob from 'fast-glob';
|
||||
import Joi from 'joi';
|
||||
import path from 'path';
|
||||
|
||||
import { GoogleServiceAccountKeyFragment } from '../../../graphql/generated';
|
||||
import Log, { learnMore } from '../../../log';
|
||||
import { confirmAsync, promptAsync } from '../../../prompts';
|
||||
import { fromNow } from '../../../utils/date';
|
||||
import { GoogleServiceAccountKey } from '../credentials';
|
||||
|
||||
export const MinimalGoogleServiceAccountKeySchema = Joi.object({
|
||||
type: Joi.string().required(),
|
||||
private_key: Joi.string().required(),
|
||||
});
|
||||
|
||||
function fileIsServiceAccountKey(keyJsonPath: string): boolean {
|
||||
try {
|
||||
readAndValidateServiceAccountKey(keyJsonPath);
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function readAndValidateServiceAccountKey(keyJsonPath: string): GoogleServiceAccountKey {
|
||||
try {
|
||||
const jsonKeyObject = JsonFile.read(keyJsonPath);
|
||||
const { error, value } = MinimalGoogleServiceAccountKeySchema.validate(jsonKeyObject, {
|
||||
abortEarly: false,
|
||||
allowUnknown: true,
|
||||
});
|
||||
if (error) {
|
||||
const maybeGoogleServicesJson = keyJsonPath.includes('google-services');
|
||||
if (maybeGoogleServicesJson) {
|
||||
Log.error(
|
||||
`Oops! Looks like you uploaded a google-services.json instead of your service account key. ${learnMore(
|
||||
'https://expo.fyi/creating-google-service-account'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
throw new Error(`Google Service Account JSON Key is not valid [${error.toString()}].`);
|
||||
}
|
||||
return value as unknown as GoogleServiceAccountKey;
|
||||
} catch (err: any) {
|
||||
if (err.code === 'EJSONPARSE') {
|
||||
err.message = `Found invalid JSON in Google Service Account JSON Key. ${err.message}`;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function selectGoogleServiceAccountKeyAsync(
|
||||
keys: GoogleServiceAccountKeyFragment[]
|
||||
): Promise<GoogleServiceAccountKeyFragment> {
|
||||
const sortedKeys = sortGoogleServiceAccountKeysByUpdatedAtDesc(keys);
|
||||
const { chosenKey } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'chosenKey',
|
||||
message: 'Select a Google Service Account Key:',
|
||||
choices: sortedKeys.map(key => ({
|
||||
title: formatGoogleServiceAccountKey(key),
|
||||
value: key,
|
||||
})),
|
||||
});
|
||||
return chosenKey;
|
||||
}
|
||||
|
||||
function sortGoogleServiceAccountKeysByUpdatedAtDesc(
|
||||
keys: GoogleServiceAccountKeyFragment[]
|
||||
): GoogleServiceAccountKeyFragment[] {
|
||||
return keys.sort(
|
||||
(keyA, keyB) => new Date(keyB.updatedAt).getTime() - new Date(keyA.updatedAt).getTime()
|
||||
);
|
||||
}
|
||||
|
||||
function formatGoogleServiceAccountKey({
|
||||
projectIdentifier,
|
||||
privateKeyIdentifier,
|
||||
clientEmail,
|
||||
clientIdentifier,
|
||||
updatedAt,
|
||||
}: GoogleServiceAccountKeyFragment): string {
|
||||
let line = `Client Email: ${clientEmail}, Project Id: ${projectIdentifier}`;
|
||||
line += chalk.gray(
|
||||
`\n Client Id: ${clientIdentifier}, Private Key Id: ${privateKeyIdentifier}`
|
||||
);
|
||||
line += chalk.gray(`\n Updated: ${fromNow(new Date(updatedAt))} ago,`);
|
||||
return line;
|
||||
}
|
||||
|
||||
export async function detectGoogleServiceAccountKeyPathAsync(
|
||||
projectDir: string
|
||||
): Promise<string | null> {
|
||||
const foundFilePaths = await glob('**/*.json', {
|
||||
cwd: projectDir,
|
||||
ignore: ['app.json', 'package*.json', 'tsconfig.json', 'node_modules'],
|
||||
});
|
||||
|
||||
const googleServiceFiles = foundFilePaths
|
||||
.map(file => path.join(projectDir, file))
|
||||
.filter(fileIsServiceAccountKey);
|
||||
|
||||
if (googleServiceFiles.length > 1) {
|
||||
const selectedPath = await displayPathChooserAsync(googleServiceFiles, projectDir);
|
||||
|
||||
if (selectedPath) {
|
||||
return selectedPath;
|
||||
}
|
||||
} else if (googleServiceFiles.length === 1) {
|
||||
const [detectedPath] = googleServiceFiles;
|
||||
|
||||
if (await confirmDetectedPathAsync(detectedPath)) {
|
||||
return detectedPath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function displayPathChooserAsync(
|
||||
paths: string[],
|
||||
projectDir: string
|
||||
): Promise<string | null> {
|
||||
const choices = paths.map<{ title: string; value: string | null }>(f => ({
|
||||
value: f,
|
||||
title: f.startsWith(projectDir) ? path.relative(projectDir, f) : f,
|
||||
}));
|
||||
|
||||
choices.push({
|
||||
title: 'None of the above',
|
||||
value: null,
|
||||
});
|
||||
|
||||
Log.log(
|
||||
'Multiple Google Service Account JSON keys have been found inside your project directory.'
|
||||
);
|
||||
const { selectedPath } = await promptAsync({
|
||||
name: 'selectedPath',
|
||||
type: 'select',
|
||||
message: 'Choose the key you want to use:',
|
||||
choices,
|
||||
});
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
return selectedPath;
|
||||
}
|
||||
|
||||
async function confirmDetectedPathAsync(path: string): Promise<boolean> {
|
||||
Log.log(`A Google Service Account JSON key has been found at\n ${chalk.underline(path)}`);
|
||||
const confirm = await confirmAsync({
|
||||
message: 'Would you like to use this file?',
|
||||
});
|
||||
Log.addNewLineIfNone();
|
||||
return confirm;
|
||||
}
|
||||
@@ -10,37 +10,76 @@ import {
|
||||
} from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { fromNow } from '../../../utils/date';
|
||||
import formatFields from '../../../utils/formatFields';
|
||||
import { sortBuildCredentials } from '../actions/BuildCredentialsUtils';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
|
||||
export function displayEmptyAndroidCredentials(appLookupParams: AppLookupParams): void {
|
||||
const { projectName, androidApplicationIdentifier } = appLookupParams;
|
||||
Log.log(chalk.bold(`Android Credentials`));
|
||||
Log.log(` Project: ${projectName}`);
|
||||
Log.log(` Application Identifier: ${androidApplicationIdentifier}`);
|
||||
Log.log(` No credentials set up yet!`);
|
||||
const fields = [
|
||||
{ label: 'Android Credentials', value: '' },
|
||||
{ label: 'Project', value: projectName },
|
||||
{ label: 'Application Identifier', value: androidApplicationIdentifier },
|
||||
];
|
||||
Log.log(formatFields(fields, { labelFormat: chalk.cyan.bold }));
|
||||
Log.log(formatFields([{ label: 'No credentials set up yet!', value: '' }]));
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
function displayAndroidFcmCredentials(appCredentials: CommonAndroidAppCredentialsFragment): void {
|
||||
const maybeFcm = appCredentials.androidFcm;
|
||||
Log.log(` Push Notifications (FCM):`);
|
||||
Log.log(
|
||||
formatFields([{ label: 'Push Notifications (FCM)', value: '' }], {
|
||||
labelFormat: chalk.cyan.bold,
|
||||
})
|
||||
);
|
||||
if (!maybeFcm) {
|
||||
Log.log(` None assigned yet`);
|
||||
Log.log(formatFields([{ label: '', value: 'None assigned yet' }]));
|
||||
Log.newLine();
|
||||
return;
|
||||
}
|
||||
const { snippet, version, updatedAt } = maybeFcm;
|
||||
const fields = [];
|
||||
if (version === AndroidFcmVersion.Legacy) {
|
||||
const { firstFourCharacters, lastFourCharacters } = snippet as FcmSnippetLegacy;
|
||||
Log.log(` Key: ${firstFourCharacters}...${lastFourCharacters}`);
|
||||
fields.push({ label: 'Key', value: `${firstFourCharacters}...${lastFourCharacters}` });
|
||||
} else if (version === AndroidFcmVersion.V1) {
|
||||
const { projectId, serviceAccountEmail, clientId, keyId } = snippet as FcmSnippetV1;
|
||||
Log.log(` Project Id: ${projectId}`);
|
||||
Log.log(` Service Account: ${serviceAccountEmail}`);
|
||||
Log.log(` Client Id: ${clientId}`);
|
||||
Log.log(` Key Id: ${keyId}`);
|
||||
fields.push({ label: 'Project ID', value: projectId });
|
||||
fields.push({ label: 'Client Email', value: serviceAccountEmail });
|
||||
fields.push({ label: 'Client ID', value: clientId ?? 'Unknown' });
|
||||
fields.push({ label: 'Private Key ID', value: keyId });
|
||||
}
|
||||
Log.log(` Updated ${fromNow(new Date(updatedAt))} ago`);
|
||||
fields.push({ label: 'Updated', value: `${fromNow(new Date(updatedAt))} ago` });
|
||||
Log.log(formatFields(fields, { labelFormat: chalk.cyan.bold }));
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
function displayGoogleServiceAccountKeyForSubmissions(
|
||||
appCredentials: CommonAndroidAppCredentialsFragment
|
||||
): void {
|
||||
const maybeGsaKey = appCredentials.googleServiceAccountKeyForSubmissions;
|
||||
Log.log(
|
||||
formatFields([{ label: 'Google Service Account Key For Submissions', value: '' }], {
|
||||
labelFormat: chalk.cyan.bold,
|
||||
})
|
||||
);
|
||||
if (!maybeGsaKey) {
|
||||
Log.log(formatFields([{ label: '', value: 'None assigned yet' }]));
|
||||
Log.newLine();
|
||||
return;
|
||||
}
|
||||
const { projectIdentifier, privateKeyIdentifier, clientEmail, clientIdentifier, updatedAt } =
|
||||
maybeGsaKey;
|
||||
|
||||
const fields = [
|
||||
{ label: 'Project ID', value: projectIdentifier },
|
||||
{ label: 'Client Email', value: clientEmail },
|
||||
{ label: 'Client ID', value: clientIdentifier },
|
||||
{ label: 'Private Key ID', value: privateKeyIdentifier },
|
||||
{ label: 'Updated', value: `${fromNow(new Date(updatedAt))} ago` },
|
||||
];
|
||||
Log.log(formatFields(fields, { labelFormat: chalk.cyan.bold }));
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
@@ -48,6 +87,7 @@ function displayEASAndroidAppCredentials(
|
||||
appCredentials: CommonAndroidAppCredentialsFragment
|
||||
): void {
|
||||
displayAndroidFcmCredentials(appCredentials);
|
||||
displayGoogleServiceAccountKeyForSubmissions(appCredentials);
|
||||
const sortedBuildCredentialsList = sortBuildCredentials(
|
||||
appCredentials.androidAppBuildCredentialsList
|
||||
);
|
||||
@@ -73,20 +113,25 @@ function formatFingerprint(fingerprint: string | null): string {
|
||||
return bytes.join(':');
|
||||
}
|
||||
function displayAndroidBuildCredentials(
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment,
|
||||
skipConfigurationDisplay?: boolean
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment
|
||||
): void {
|
||||
if (!skipConfigurationDisplay) {
|
||||
const { isDefault, name } = buildCredentials;
|
||||
Log.log(chalk.bold(` Configuration: ${name}${isDefault ? ' (Default)' : ''}`));
|
||||
}
|
||||
const { isDefault, name } = buildCredentials;
|
||||
Log.log(
|
||||
formatFields([{ label: `Configuration: ${name}${isDefault ? ' (Default)' : ''}`, value: '' }], {
|
||||
labelFormat: chalk.cyan.bold,
|
||||
})
|
||||
);
|
||||
|
||||
const maybeKeystore = buildCredentials.androidKeystore;
|
||||
Log.log(` Keystore:`);
|
||||
Log.log(
|
||||
formatFields([{ label: 'Keystore', value: '' }], {
|
||||
labelFormat: chalk.cyan.bold,
|
||||
})
|
||||
);
|
||||
if (maybeKeystore) {
|
||||
displayAndroidKeystore(maybeKeystore);
|
||||
} else {
|
||||
Log.log(` None assigned yet`);
|
||||
Log.log(formatFields([{ label: '', value: 'None assigned yet' }]));
|
||||
}
|
||||
Log.newLine();
|
||||
}
|
||||
@@ -100,13 +145,15 @@ export function displayAndroidKeystore(keystore: AndroidKeystoreFragment): void
|
||||
sha256CertificateFingerprint,
|
||||
updatedAt,
|
||||
} = keystore;
|
||||
Log.log(` Type: ${type}`);
|
||||
Log.log(` Key Alias: ${keyAlias}`);
|
||||
|
||||
Log.log(` MD5 Fingerprint: ${formatFingerprint(md5CertificateFingerprint ?? null)}`);
|
||||
Log.log(` SHA1 Fingerprint: ${formatFingerprint(sha1CertificateFingerprint ?? null)}`);
|
||||
Log.log(` SHA256 Fingerprint: ${formatFingerprint(sha256CertificateFingerprint ?? null)}`);
|
||||
Log.log(` Updated ${fromNow(new Date(updatedAt))} ago`);
|
||||
const fields = [
|
||||
{ label: 'Type', value: type },
|
||||
{ label: 'Key Alias', value: keyAlias },
|
||||
{ label: 'MD5 Fingerprint', value: formatFingerprint(md5CertificateFingerprint ?? null) },
|
||||
{ label: 'SHA1 Fingerprint', value: formatFingerprint(sha1CertificateFingerprint ?? null) },
|
||||
{ label: 'SHA256 Fingerprint', value: formatFingerprint(sha256CertificateFingerprint ?? null) },
|
||||
{ label: 'Updated', value: `${fromNow(new Date(updatedAt))} ago` },
|
||||
];
|
||||
Log.log(formatFields(fields, { labelFormat: chalk.cyan.bold }));
|
||||
}
|
||||
|
||||
export function displayAndroidAppCredentials({
|
||||
@@ -117,9 +164,12 @@ export function displayAndroidAppCredentials({
|
||||
appCredentials: CommonAndroidAppCredentialsFragment | null;
|
||||
}): void {
|
||||
const { projectName, androidApplicationIdentifier } = appLookupParams;
|
||||
Log.log(chalk.bold(`Android Credentials`));
|
||||
Log.log(` Project: ${projectName}`);
|
||||
Log.log(` Application Identifier: ${androidApplicationIdentifier}`);
|
||||
const fields = [
|
||||
{ label: 'Android Credentials', value: '' },
|
||||
{ label: 'Project', value: projectName },
|
||||
{ label: 'Application Identifier', value: androidApplicationIdentifier },
|
||||
];
|
||||
Log.log(formatFields(fields, { labelFormat: chalk.cyan.bold }));
|
||||
Log.newLine();
|
||||
|
||||
if (appCredentials) {
|
||||
|
||||
@@ -6,74 +6,41 @@ import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getProjectAccountName } from '../project/projectUtils';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { Actor, getActorDisplayName } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import pick from '../utils/expodash/pick';
|
||||
import * as AndroidGraphqlClient from './android/api/GraphqlClient';
|
||||
import * as IosGraphqlClient from './ios/api/GraphqlClient';
|
||||
import AppStoreApi from './ios/appstore/AppStoreApi';
|
||||
|
||||
export interface Action<T = void> {
|
||||
runAsync(ctx: Context): Promise<T>;
|
||||
}
|
||||
|
||||
interface AppleCtxOptions {
|
||||
appleId?: string;
|
||||
appleIdPassword?: string;
|
||||
teamId?: string;
|
||||
}
|
||||
|
||||
interface Options extends AppleCtxOptions {
|
||||
nonInteractive?: boolean;
|
||||
exp?: ExpoConfig;
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
readonly projectDir: string;
|
||||
readonly user: Actor;
|
||||
readonly nonInteractive: boolean;
|
||||
readonly android: typeof AndroidGraphqlClient;
|
||||
readonly ios: typeof IosGraphqlClient;
|
||||
readonly appStore: AppStoreApi;
|
||||
readonly hasProjectContext: boolean;
|
||||
readonly exp: ExpoConfig;
|
||||
|
||||
ensureProjectContext(): void;
|
||||
bestEffortAppStoreAuthenticateAsync(): Promise<void>;
|
||||
}
|
||||
|
||||
export async function createCredentialsContextAsync(
|
||||
projectDir: string,
|
||||
options: Options
|
||||
): Promise<Context> {
|
||||
const user = await ensureLoggedInAsync();
|
||||
|
||||
let expoConfig: ExpoConfig | undefined = options.exp;
|
||||
if (!expoConfig) {
|
||||
try {
|
||||
expoConfig = getExpoConfig(projectDir);
|
||||
} catch (error) {
|
||||
// ignore error, context might be created outside of expo project
|
||||
}
|
||||
}
|
||||
|
||||
return new CredentialsContext(projectDir, user, expoConfig, options);
|
||||
}
|
||||
|
||||
class CredentialsContext implements Context {
|
||||
export class CredentialsContext {
|
||||
public readonly android = AndroidGraphqlClient;
|
||||
public readonly appStore = new AppStoreApi();
|
||||
public readonly ios = IosGraphqlClient;
|
||||
public readonly appStore: AppStoreApi;
|
||||
public readonly nonInteractive: boolean;
|
||||
public readonly projectDir: string;
|
||||
public readonly user: Actor;
|
||||
|
||||
private shouldAskAuthenticateAppStore: boolean = true;
|
||||
private _exp?: ExpoConfig;
|
||||
|
||||
constructor(
|
||||
public readonly projectDir: string,
|
||||
public readonly user: Actor,
|
||||
private _exp: ExpoConfig | undefined,
|
||||
options: Options
|
||||
private options: {
|
||||
exp?: ExpoConfig;
|
||||
nonInteractive?: boolean;
|
||||
projectDir: string;
|
||||
user: Actor;
|
||||
}
|
||||
) {
|
||||
this.appStore = new AppStoreApi(pick(options, ['appleId', 'appleIdPassword', 'teamId']));
|
||||
this.projectDir = options.projectDir;
|
||||
this.user = options.user;
|
||||
this.nonInteractive = options.nonInteractive ?? false;
|
||||
|
||||
this._exp = options.exp;
|
||||
if (!this._exp) {
|
||||
try {
|
||||
this._exp = getExpoConfig(options.projectDir);
|
||||
} catch (error) {
|
||||
// ignore error, context might be created outside of expo project
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get hasProjectContext(): boolean {
|
||||
@@ -90,14 +57,15 @@ class CredentialsContext implements Context {
|
||||
return;
|
||||
}
|
||||
// trigger getConfig error
|
||||
getConfig(this.projectDir, { skipSDKVersionRequirement: true });
|
||||
getConfig(this.options.projectDir, { skipSDKVersionRequirement: true });
|
||||
}
|
||||
|
||||
public logOwnerAndProject(): void {
|
||||
const { user } = this.options;
|
||||
if (this.hasProjectContext) {
|
||||
const owner = getProjectAccountName(this.exp, this.user);
|
||||
const owner = getProjectAccountName(this.exp, user);
|
||||
// Figure out if User A is configuring credentials as admin for User B's project
|
||||
const isProxyUser = this.user.__typename === 'Robot' || owner !== this.user.username;
|
||||
const isProxyUser = user.__typename === 'Robot' || owner !== user.username;
|
||||
|
||||
Log.log(
|
||||
`Accessing credentials ${isProxyUser ? 'on behalf of' : 'for'} ${owner} in project ${
|
||||
@@ -105,7 +73,7 @@ class CredentialsContext implements Context {
|
||||
}`
|
||||
);
|
||||
} else {
|
||||
Log.log(`Accessing credentials for ${this.exp.owner ?? getActorDisplayName(this.user)}`);
|
||||
Log.log(`Accessing credentials for ${this.exp.owner ?? getActorDisplayName(user)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { findApplicationTarget, findTargetByName } from '../../project/ios/targe
|
||||
import zipObject from '../../utils/expodash/zipObject';
|
||||
import vcs from '../../vcs';
|
||||
import GitClient from '../../vcs/git';
|
||||
import { Context } from '../context';
|
||||
import { CredentialsContext } from '../context';
|
||||
import { App, Target, TargetCredentials } from '../ios/types';
|
||||
import { readRawAsync } from './read';
|
||||
import {
|
||||
@@ -23,7 +23,7 @@ import { getCredentialsJsonPath } from './utils';
|
||||
* is not validated
|
||||
*/
|
||||
export async function updateAndroidCredentialsAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment
|
||||
): Promise<void> {
|
||||
const rawCredentialsJson: any =
|
||||
@@ -70,7 +70,7 @@ export async function updateAndroidCredentialsAsync(
|
||||
* credentials.json will be updated partially
|
||||
*/
|
||||
export async function updateIosCredentialsAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
app: App,
|
||||
targets: Target[],
|
||||
distributionType: IosDistributionType
|
||||
@@ -205,7 +205,7 @@ function createTargetCredentialsPathsMap(
|
||||
}
|
||||
|
||||
async function getTargetBuildCredentialsAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
app: App,
|
||||
target: Target,
|
||||
iosDistributionType: IosDistributionType
|
||||
@@ -244,7 +244,7 @@ async function getTargetBuildCredentialsAsync(
|
||||
}
|
||||
|
||||
async function backupTargetCredentialsAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
{
|
||||
targetCredentials,
|
||||
targetCredentialsPaths,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CommonIosAppCredentialsFragment } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { findApplicationTarget } from '../../project/ios/target';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import { Context } from '../context';
|
||||
import { CredentialsContext } from '../context';
|
||||
import * as credentialsJsonReader from '../credentialsJson/read';
|
||||
import { ensureAllTargetsAreConfigured } from '../credentialsJson/utils';
|
||||
import { getAppFromContext } from './actions/BuildCredentialsUtils';
|
||||
@@ -26,7 +26,7 @@ interface Options {
|
||||
export default class IosCredentialsProvider {
|
||||
public readonly platform = Platform.IOS;
|
||||
|
||||
constructor(private ctx: Context, private options: Options) {}
|
||||
constructor(private ctx: CredentialsContext, private options: Options) {}
|
||||
|
||||
public async getCredentialsAsync(
|
||||
src: CredentialsSource.LOCAL | CredentialsSource.REMOTE
|
||||
@@ -68,7 +68,7 @@ export default class IosCredentialsProvider {
|
||||
}
|
||||
|
||||
private async getPushKeyAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
targets: Target[]
|
||||
): Promise<CommonIosAppCredentialsFragment | null> {
|
||||
if (ctx.nonInteractive) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { AppleTeamFragment } from '../../../graphql/generated';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
|
||||
export async function resolveAppleTeamIfAuthenticatedAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
app: AppLookupParams
|
||||
): Promise<AppleTeamFragment | null> {
|
||||
if (!ctx.appStore.authCtx) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ApplePushKeyFragment, CommonIosAppCredentialsFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
|
||||
|
||||
@@ -8,7 +8,7 @@ export class AssignPushKey {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
public async runAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
pushKey: ApplePushKeyFragment
|
||||
): Promise<CommonIosAppCredentialsFragment> {
|
||||
const appleTeam =
|
||||
|
||||
@@ -9,13 +9,13 @@ import {
|
||||
} from '../../../graphql/generated';
|
||||
import { getProjectAccountName } from '../../../project/projectUtils';
|
||||
import { findAccountByName } from '../../../user/Account';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { App, Target } from '../types';
|
||||
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
|
||||
|
||||
export async function getAllBuildCredentialsAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
app: AppLookupParams
|
||||
): Promise<IosAppBuildCredentialsFragment[]> {
|
||||
const appCredentials = await ctx.ios.getIosAppCredentialsWithBuildCredentialsAsync(app, {});
|
||||
@@ -26,7 +26,7 @@ export async function getAllBuildCredentialsAsync(
|
||||
}
|
||||
|
||||
export async function getBuildCredentialsAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
app: AppLookupParams,
|
||||
iosDistributionType: GraphQLIosDistributionType
|
||||
): Promise<IosAppBuildCredentialsFragment | null> {
|
||||
@@ -41,7 +41,7 @@ export async function getBuildCredentialsAsync(
|
||||
}
|
||||
|
||||
export async function getProvisioningProfileAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
app: AppLookupParams,
|
||||
iosDistributionType: GraphQLIosDistributionType
|
||||
): Promise<AppleProvisioningProfileFragment | null> {
|
||||
@@ -50,7 +50,7 @@ export async function getProvisioningProfileAsync(
|
||||
}
|
||||
|
||||
export async function getDistributionCertificateAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
app: AppLookupParams,
|
||||
iosDistributionType: GraphQLIosDistributionType
|
||||
): Promise<AppleDistributionCertificateFragment | null> {
|
||||
@@ -59,7 +59,7 @@ export async function getDistributionCertificateAsync(
|
||||
}
|
||||
|
||||
export async function assignBuildCredentialsAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
app: AppLookupParams,
|
||||
iosDistributionType: GraphQLIosDistributionType,
|
||||
distCert: AppleDistributionCertificateFragment,
|
||||
@@ -82,7 +82,7 @@ export async function assignBuildCredentialsAsync(
|
||||
});
|
||||
}
|
||||
|
||||
export function getAppFromContext(ctx: Context): App {
|
||||
export function getAppFromContext(ctx: CredentialsContext): App {
|
||||
ctx.ensureProjectContext();
|
||||
const projectName = ctx.exp.slug;
|
||||
const accountName = getProjectAccountName(ctx.exp, ctx.user);
|
||||
@@ -96,7 +96,10 @@ export function getAppFromContext(ctx: Context): App {
|
||||
};
|
||||
}
|
||||
|
||||
export function getAppLookupParamsFromContext(ctx: Context, target: Target): AppLookupParams {
|
||||
export function getAppLookupParamsFromContext(
|
||||
ctx: CredentialsContext,
|
||||
target: Target
|
||||
): AppLookupParams {
|
||||
const app = getAppFromContext(ctx);
|
||||
return { ...app, bundleIdentifier: target.bundleIdentifier };
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { ora } from '../../../ora';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../errors';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { AppleProvisioningProfileMutationResult } from '../api/graphql/mutations/AppleProvisioningProfileMutation';
|
||||
@@ -21,7 +21,9 @@ export class ConfigureProvisioningProfile {
|
||||
private originalProvisioningProfile: AppleProvisioningProfileFragment
|
||||
) {}
|
||||
|
||||
public async runAsync(ctx: Context): Promise<AppleProvisioningProfileMutationResult | null> {
|
||||
public async runAsync(
|
||||
ctx: CredentialsContext
|
||||
): Promise<AppleProvisioningProfileMutationResult | null> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new MissingCredentialsNonInteractiveError(
|
||||
'Configuring Provisioning Profiles is only supported in interactive mode.'
|
||||
@@ -62,7 +64,7 @@ export class ConfigureProvisioningProfile {
|
||||
}
|
||||
|
||||
private async configureAndUpdateAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
authCtx: AuthCtx,
|
||||
app: AppLookupParams,
|
||||
profileFromApple: ProvisioningProfileStoreInfo
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import Log from '../../../log';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { AppleDistributionCertificateMutationResult } from '../api/graphql/mutations/AppleDistributionCertificateMutation';
|
||||
import { provideOrGenerateDistributionCertificateAsync } from './DistributionCertificateUtils';
|
||||
|
||||
export class CreateDistributionCertificate {
|
||||
constructor(private account: Account) {}
|
||||
|
||||
public async runAsync(ctx: Context): Promise<AppleDistributionCertificateMutationResult> {
|
||||
public async runAsync(
|
||||
ctx: CredentialsContext
|
||||
): Promise<AppleDistributionCertificateMutationResult> {
|
||||
const distCert = await provideOrGenerateDistributionCertificateAsync(ctx, this.account.name);
|
||||
const result = await ctx.ios.createDistributionCertificateAsync(this.account, distCert);
|
||||
Log.succeed('Created distribution certificate');
|
||||
|
||||
@@ -3,7 +3,7 @@ import nullthrows from 'nullthrows';
|
||||
|
||||
import { AppleDistributionCertificateFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../errors';
|
||||
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
@@ -20,7 +20,7 @@ export class CreateProvisioningProfile {
|
||||
private distributionCertificate: AppleDistributionCertificateFragment
|
||||
) {}
|
||||
|
||||
async runAsync(ctx: Context): Promise<AppleProvisioningProfileMutationResult> {
|
||||
async runAsync(ctx: CredentialsContext): Promise<AppleProvisioningProfileMutationResult> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new MissingCredentialsNonInteractiveError(
|
||||
'Creating Provisioning Profiles is only supported in interactive mode.'
|
||||
@@ -46,7 +46,7 @@ export class CreateProvisioningProfile {
|
||||
}
|
||||
|
||||
private async provideOrGenerateAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
appleAuthCtx: AuthCtx
|
||||
): Promise<ProvisioningProfile> {
|
||||
const userProvided = await askForUserProvidedAsync(provisioningProfileSchema);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { ApplePushKeyFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { provideOrGeneratePushKeyAsync } from './PushKeyUtils';
|
||||
|
||||
export class CreatePushKey {
|
||||
constructor(private account: Account) {}
|
||||
|
||||
public async runAsync(ctx: Context): Promise<ApplePushKeyFragment> {
|
||||
public async runAsync(ctx: CredentialsContext): Promise<ApplePushKeyFragment> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(`A new push key cannot be created in non-interactive mode.`);
|
||||
}
|
||||
|
||||
@@ -7,15 +7,15 @@ import Log, { learnMore } from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { fromNow } from '../../../utils/date';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { AppleTooManyCertsError } from '../appstore/AppStoreApi';
|
||||
import {
|
||||
DistributionCertificate,
|
||||
DistributionCertificateStoreInfo,
|
||||
} from '../appstore/Credentials.types';
|
||||
import { filterRevokedDistributionCertsFromEasServers } from '../appstore/CredentialsUtils';
|
||||
import { AppleTooManyCertsError } from '../appstore/distributionCertificate';
|
||||
import { distributionCertificateSchema } from '../credentials';
|
||||
import { validateDistributionCertificateAsync } from '../validators/validateDistributionCertificate';
|
||||
import { formatAppleTeam } from './AppleTeamUtils';
|
||||
@@ -84,7 +84,7 @@ async function _selectDistributionCertificateAsync(
|
||||
* select a distribution certificate from an account (validity status shown on a best effort basis)
|
||||
* */
|
||||
export async function selectDistributionCertificateWithDependenciesAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
account: Account
|
||||
): Promise<AppleDistributionCertificateFragment | null> {
|
||||
const distCertsForAccount = await ctx.ios.getDistributionCertificatesForAccountAsync(account);
|
||||
@@ -110,7 +110,7 @@ export async function selectDistributionCertificateWithDependenciesAsync(
|
||||
* select a distribution certificate from a valid set (curated on a best effort basis)
|
||||
* */
|
||||
export async function selectValidDistributionCertificateAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
appLookupParams: AppLookupParams
|
||||
): Promise<AppleDistributionCertificateFragment | null> {
|
||||
const distCertsForAccount = await ctx.ios.getDistributionCertificatesForAccountAsync(
|
||||
@@ -133,7 +133,7 @@ export async function selectValidDistributionCertificateAsync(
|
||||
|
||||
// filter by valid certs on the developer portal
|
||||
const certInfoFromApple = await ctx.appStore.listDistributionCertificatesAsync();
|
||||
const validDistCerts = await filterRevokedDistributionCertsFromEasServers(
|
||||
const validDistCerts = filterRevokedDistributionCertsFromEasServers(
|
||||
distCertsForAppleTeam,
|
||||
certInfoFromApple
|
||||
);
|
||||
@@ -152,7 +152,7 @@ Please remember that Apple Distribution Certificates are not application specifi
|
||||
`;
|
||||
|
||||
export async function provideOrGenerateDistributionCertificateAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
accountName: string
|
||||
): Promise<DistributionCertificate> {
|
||||
if (!ctx.nonInteractive) {
|
||||
@@ -177,7 +177,9 @@ export async function provideOrGenerateDistributionCertificateAsync(
|
||||
return await generateDistributionCertificateAsync(ctx, accountName);
|
||||
}
|
||||
|
||||
async function promptForDistCertAsync(ctx: Context): Promise<DistributionCertificate | null> {
|
||||
async function promptForDistCertAsync(
|
||||
ctx: CredentialsContext
|
||||
): Promise<DistributionCertificate | null> {
|
||||
let initialValues: { teamId?: string } = {};
|
||||
if (ctx.appStore.authCtx) {
|
||||
initialValues = {
|
||||
@@ -198,7 +200,7 @@ async function promptForDistCertAsync(ctx: Context): Promise<DistributionCertifi
|
||||
}
|
||||
|
||||
async function generateDistributionCertificateAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
accountName: string
|
||||
): Promise<DistributionCertificate> {
|
||||
await ctx.appStore.ensureAuthenticatedAsync();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import {
|
||||
DistributionCertificate,
|
||||
ProvisioningProfile,
|
||||
@@ -19,7 +19,7 @@ export function formatProvisioningProfileFromApple(
|
||||
}
|
||||
|
||||
export async function generateProvisioningProfileAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
bundleIdentifier: string,
|
||||
distCert: DistributionCertificate
|
||||
): Promise<ProvisioningProfile> {
|
||||
|
||||
@@ -5,7 +5,7 @@ import Log, { learnMore } from '../../../log';
|
||||
import { confirmAsync, promptAsync } from '../../../prompts';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { fromNow } from '../../../utils/date';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
|
||||
import { PushKey, PushKeyStoreInfo } from '../appstore/Credentials.types';
|
||||
import { filterRevokedAndUntrackedPushKeysFromEasServersAsync } from '../appstore/CredentialsUtils';
|
||||
@@ -15,7 +15,7 @@ import { isPushKeyValidAndTrackedAsync } from '../validators/validatePushKey';
|
||||
import { formatAppleTeam } from './AppleTeamUtils';
|
||||
|
||||
export async function provideOrGeneratePushKeyAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
accountName: string
|
||||
): Promise<PushKey> {
|
||||
if (!ctx.nonInteractive) {
|
||||
@@ -43,7 +43,7 @@ export async function provideOrGeneratePushKeyAsync(
|
||||
return await generatePushKeyAsync(ctx, accountName);
|
||||
}
|
||||
|
||||
async function promptForPushKeyAsync(ctx: Context): Promise<PushKey | null> {
|
||||
async function promptForPushKeyAsync(ctx: CredentialsContext): Promise<PushKey | null> {
|
||||
let initialValues: { teamId?: string } = {};
|
||||
if (ctx.appStore.authCtx) {
|
||||
initialValues = {
|
||||
@@ -63,7 +63,10 @@ async function promptForPushKeyAsync(ctx: Context): Promise<PushKey | null> {
|
||||
return userProvided;
|
||||
}
|
||||
|
||||
async function generatePushKeyAsync(ctx: Context, accountName: string): Promise<PushKey> {
|
||||
async function generatePushKeyAsync(
|
||||
ctx: CredentialsContext,
|
||||
accountName: string
|
||||
): Promise<PushKey> {
|
||||
await ctx.appStore.ensureAuthenticatedAsync();
|
||||
try {
|
||||
return await ctx.appStore.createPushKeyAsync();
|
||||
@@ -116,7 +119,7 @@ function formatPushKeyFromApple(pushKey: PushKeyStoreInfo): string {
|
||||
* select a push key from an account (validity status shown on a best effort basis)
|
||||
* */
|
||||
export async function selectPushKeyAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
account: Account
|
||||
): Promise<ApplePushKeyFragment | null> {
|
||||
const pushKeysForAccount = await ctx.ios.getPushKeysForAccountAsync(account);
|
||||
@@ -133,7 +136,7 @@ export async function selectPushKeyAsync(
|
||||
}
|
||||
|
||||
export async function getValidAndTrackedPushKeysOnEasServersAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
pushKeysForAccount: ApplePushKeyFragment[]
|
||||
): Promise<ApplePushKeyFragment[]> {
|
||||
const pushInfoFromApple = await ctx.appStore.listPushKeysAsync();
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { selectDistributionCertificateWithDependenciesAsync } from './DistributionCertificateUtils';
|
||||
import { RemoveProvisioningProfiles } from './RemoveProvisioningProfile';
|
||||
@@ -13,7 +13,7 @@ import { RemoveProvisioningProfiles } from './RemoveProvisioningProfile';
|
||||
export class SelectAndRemoveDistributionCertificate {
|
||||
constructor(private account: Account) {}
|
||||
|
||||
async runAsync(ctx: Context): Promise<void> {
|
||||
async runAsync(ctx: CredentialsContext): Promise<void> {
|
||||
const selected = await selectDistributionCertificateWithDependenciesAsync(ctx, this.account);
|
||||
if (selected) {
|
||||
await new RemoveDistributionCertificate(this.account, selected).runAsync(ctx);
|
||||
@@ -29,7 +29,7 @@ export class RemoveDistributionCertificate {
|
||||
private distributionCertificate: AppleDistributionCertificateFragment
|
||||
) {}
|
||||
|
||||
public async runAsync(ctx: Context): Promise<void> {
|
||||
public async runAsync(ctx: CredentialsContext): Promise<void> {
|
||||
const apps = this.distributionCertificate.iosAppBuildCredentialsList.map(
|
||||
buildCredentials => buildCredentials.iosAppCredentials.app
|
||||
);
|
||||
@@ -71,7 +71,7 @@ export class RemoveDistributionCertificate {
|
||||
await this.removeInvalidProvisioningProfilesAsync(ctx);
|
||||
}
|
||||
|
||||
private async removeInvalidProvisioningProfilesAsync(ctx: Context): Promise<void> {
|
||||
private async removeInvalidProvisioningProfilesAsync(ctx: CredentialsContext): Promise<void> {
|
||||
const buildCredentialsList = this.distributionCertificate.iosAppBuildCredentialsList;
|
||||
const appsWithProfilesToRemove: AppLookupParams[] = [];
|
||||
const profilesToRemove: AppleProvisioningProfileIdentifiersFragment[] = [];
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from 'assert';
|
||||
|
||||
import { AppleProvisioningProfileIdentifiersFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
|
||||
export class RemoveProvisioningProfiles {
|
||||
@@ -16,7 +16,7 @@ export class RemoveProvisioningProfiles {
|
||||
);
|
||||
}
|
||||
|
||||
async runAsync(ctx: Context): Promise<void> {
|
||||
async runAsync(ctx: CredentialsContext): Promise<void> {
|
||||
if (this.provisioningProfiles.length === 0) {
|
||||
Log.log(`Skipping deletion of Provisioning Profiles`);
|
||||
return;
|
||||
|
||||
@@ -2,13 +2,13 @@ import { ApplePushKeyFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { selectPushKeyAsync } from './PushKeyUtils';
|
||||
|
||||
export class SelectAndRemovePushKey {
|
||||
constructor(private account: Account) {}
|
||||
|
||||
async runAsync(ctx: Context): Promise<void> {
|
||||
async runAsync(ctx: CredentialsContext): Promise<void> {
|
||||
const selected = await selectPushKeyAsync(ctx, this.account);
|
||||
if (selected) {
|
||||
await new RemovePushKey(this.account, selected).runAsync(ctx);
|
||||
@@ -21,7 +21,7 @@ export class SelectAndRemovePushKey {
|
||||
export class RemovePushKey {
|
||||
constructor(private account: Account, private pushKey: ApplePushKeyFragment) {}
|
||||
|
||||
public async runAsync(ctx: Context): Promise<void> {
|
||||
public async runAsync(ctx: CredentialsContext): Promise<void> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(`Cannot remove push keys in non-interactive mode`);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync, pressAnyKeyToContinueAsync } from '../../../prompts';
|
||||
import differenceBy from '../../../utils/expodash/differenceBy';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../errors';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { validateProvisioningProfileAsync } from '../validators/validateProvisioningProfile';
|
||||
@@ -26,7 +26,7 @@ import { SetupDistributionCertificate } from './SetupDistributionCertificate';
|
||||
export class SetupAdhocProvisioningProfile {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
async runAsync(ctx: Context): Promise<IosAppBuildCredentialsFragment> {
|
||||
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
|
||||
const distCert = await new SetupDistributionCertificate(
|
||||
this.app,
|
||||
IosDistributionType.AdHoc
|
||||
@@ -60,7 +60,7 @@ export class SetupAdhocProvisioningProfile {
|
||||
}
|
||||
|
||||
async runWithDistributionCertificateAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
distCert: AppleDistributionCertificateFragment
|
||||
): Promise<IosAppBuildCredentialsFragment> {
|
||||
const currentBuildCredentials = await getBuildCredentialsAsync(
|
||||
@@ -158,7 +158,7 @@ export class SetupAdhocProvisioningProfile {
|
||||
);
|
||||
}
|
||||
|
||||
private async areBuildCredentialsSetupAsync(ctx: Context): Promise<boolean> {
|
||||
private async areBuildCredentialsSetupAsync(ctx: CredentialsContext): Promise<boolean> {
|
||||
const buildCredentials = await getBuildCredentialsAsync(
|
||||
ctx,
|
||||
this.app,
|
||||
@@ -168,7 +168,7 @@ export class SetupAdhocProvisioningProfile {
|
||||
}
|
||||
|
||||
private async shouldUseExistingProfileAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
buildCredentials: IosAppBuildCredentialsFragment
|
||||
): Promise<boolean> {
|
||||
const provisioningProfile = nullthrows(buildCredentials.provisioningProfile);
|
||||
@@ -202,7 +202,7 @@ export class SetupAdhocProvisioningProfile {
|
||||
}
|
||||
|
||||
private async registerDevicesAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
appleTeam: AppleTeamFragment
|
||||
): Promise<AppleDeviceFragment[]> {
|
||||
const action = new DeviceCreateAction(this.app.account, appleTeam);
|
||||
|
||||
@@ -3,7 +3,7 @@ import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import Log from '../../../log';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
|
||||
import { App, IosAppBuildCredentialsMap, IosCredentials, Target } from '../types';
|
||||
import { displayProjectCredentials } from '../utils/printCredentials';
|
||||
@@ -20,7 +20,7 @@ interface Options {
|
||||
export class SetupBuildCredentials {
|
||||
constructor(private options: Options) {}
|
||||
|
||||
async runAsync(ctx: Context): Promise<IosCredentials> {
|
||||
async runAsync(ctx: CredentialsContext): Promise<IosCredentials> {
|
||||
const hasManyTargets = this.options.targets.length > 1;
|
||||
const iosAppBuildCredentialsMap: IosAppBuildCredentialsMap = {};
|
||||
if (hasManyTargets) {
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@ import chalk from 'chalk';
|
||||
import { IosDistributionType } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { findApplicationTarget } from '../../../project/ios/target';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { readIosCredentialsAsync } from '../../credentialsJson/read';
|
||||
import { IosCredentials } from '../../credentialsJson/types';
|
||||
import { ensureAllTargetsAreConfigured } from '../../credentialsJson/utils';
|
||||
@@ -17,7 +17,7 @@ export class SetupBuildCredentialsFromCredentialsJson {
|
||||
private distributionType: IosDistributionType
|
||||
) {}
|
||||
|
||||
async runAsync(ctx: Context): Promise<void> {
|
||||
async runAsync(ctx: CredentialsContext): Promise<void> {
|
||||
const credentialsJson = await this.readCredentialsJsonAsync(ctx);
|
||||
ensureAllTargetsAreConfigured(this.targets, credentialsJson);
|
||||
|
||||
@@ -45,7 +45,7 @@ export class SetupBuildCredentialsFromCredentialsJson {
|
||||
}
|
||||
}
|
||||
|
||||
private async readCredentialsJsonAsync(ctx: Context): Promise<IosCredentials> {
|
||||
private async readCredentialsJsonAsync(ctx: CredentialsContext): Promise<IosCredentials> {
|
||||
const applicationTarget = findApplicationTarget(this.targets);
|
||||
try {
|
||||
return await readIosCredentialsAsync(ctx.projectDir, applicationTarget);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync, promptAsync } from '../../../prompts';
|
||||
import sortBy from '../../../utils/expodash/sortBy';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../errors';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { AppleDistributionCertificateMutationResult } from '../api/graphql/mutations/AppleDistributionCertificateMutation';
|
||||
@@ -23,7 +23,7 @@ export class SetupDistributionCertificate {
|
||||
|
||||
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
|
||||
|
||||
public async runAsync(ctx: Context): Promise<AppleDistributionCertificateFragment> {
|
||||
public async runAsync(ctx: CredentialsContext): Promise<AppleDistributionCertificateFragment> {
|
||||
const appleTeam = await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app);
|
||||
|
||||
try {
|
||||
@@ -47,7 +47,7 @@ export class SetupDistributionCertificate {
|
||||
}
|
||||
|
||||
private async runNonInteractiveAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
currentCertificate: AppleDistributionCertificateFragment | null
|
||||
): Promise<AppleDistributionCertificateFragment> {
|
||||
// TODO: implement validation
|
||||
@@ -60,7 +60,7 @@ export class SetupDistributionCertificate {
|
||||
}
|
||||
|
||||
private async runInteractiveAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
currentCertificate: AppleDistributionCertificateFragment | null
|
||||
): Promise<AppleDistributionCertificateFragment> {
|
||||
if (await this.isCurrentCertificateValidAsync(ctx, currentCertificate)) {
|
||||
@@ -75,7 +75,7 @@ export class SetupDistributionCertificate {
|
||||
}
|
||||
|
||||
private async isCurrentCertificateValidAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
currentCertificate: AppleDistributionCertificateFragment | null
|
||||
): Promise<boolean> {
|
||||
if (!currentCertificate) {
|
||||
@@ -108,7 +108,7 @@ export class SetupDistributionCertificate {
|
||||
}
|
||||
|
||||
private async createOrReuseDistCertAsync(
|
||||
ctx: Context
|
||||
ctx: CredentialsContext
|
||||
): Promise<AppleDistributionCertificateFragment> {
|
||||
const validDistCerts = await this.getValidDistCertsAsync(ctx);
|
||||
const autoselectedDistCert = validDistCerts[0];
|
||||
@@ -147,12 +147,12 @@ export class SetupDistributionCertificate {
|
||||
}
|
||||
|
||||
private async createNewDistCertAsync(
|
||||
ctx: Context
|
||||
ctx: CredentialsContext
|
||||
): Promise<AppleDistributionCertificateMutationResult> {
|
||||
return new CreateDistributionCertificate(this.app.account).runAsync(ctx);
|
||||
}
|
||||
|
||||
async reuseDistCertAsync(ctx: Context): Promise<AppleDistributionCertificate> {
|
||||
async reuseDistCertAsync(ctx: CredentialsContext): Promise<AppleDistributionCertificate> {
|
||||
const validDistCerts = await this.getValidDistCertsAsync(ctx);
|
||||
const { distCert } = await promptAsync({
|
||||
type: 'select',
|
||||
@@ -167,7 +167,7 @@ export class SetupDistributionCertificate {
|
||||
}
|
||||
|
||||
private async getValidDistCertsAsync(
|
||||
ctx: Context
|
||||
ctx: CredentialsContext
|
||||
): Promise<AppleDistributionCertificateFragment[]> {
|
||||
if (this.validDistCerts) {
|
||||
return this.validDistCerts;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IosAppBuildCredentialsFragment, IosDistributionType } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { getAllBuildCredentialsAsync } from './BuildCredentialsUtils';
|
||||
import { SetupAdhocProvisioningProfile } from './SetupAdhocProvisioningProfile';
|
||||
@@ -17,7 +17,7 @@ import { SetupProvisioningProfile } from './SetupProvisioningProfile';
|
||||
export class SetupInternalProvisioningProfile {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
async runAsync(ctx: Context): Promise<IosAppBuildCredentialsFragment> {
|
||||
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
|
||||
const buildCredentials = await getAllBuildCredentialsAsync(ctx, this.app);
|
||||
|
||||
const adhocBuildCredentialsExist =
|
||||
@@ -80,13 +80,13 @@ export class SetupInternalProvisioningProfile {
|
||||
}
|
||||
|
||||
private async setupAdhocProvisioningProfileAsync(
|
||||
ctx: Context
|
||||
ctx: CredentialsContext
|
||||
): Promise<IosAppBuildCredentialsFragment> {
|
||||
return await new SetupAdhocProvisioningProfile(this.app).runAsync(ctx);
|
||||
}
|
||||
|
||||
private async setupUniversalProvisioningProfileAsync(
|
||||
ctx: Context
|
||||
ctx: CredentialsContext
|
||||
): Promise<IosAppBuildCredentialsFragment> {
|
||||
return await new SetupProvisioningProfile(this.app, IosDistributionType.Enterprise).runAsync(
|
||||
ctx
|
||||
@@ -94,7 +94,7 @@ export class SetupInternalProvisioningProfile {
|
||||
}
|
||||
|
||||
private async askForDistributionTypeAndSetupAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
message: string
|
||||
): Promise<IosAppBuildCredentialsFragment> {
|
||||
const { distributionType } = await promptAsync({
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
IosDistributionType,
|
||||
} from '../../../graphql/generated';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import { Context } from '../../context';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../errors';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { ProvisioningProfileStoreInfo } from '../appstore/Credentials.types';
|
||||
@@ -28,13 +28,13 @@ import { SetupDistributionCertificate } from './SetupDistributionCertificate';
|
||||
export class SetupProvisioningProfile {
|
||||
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
|
||||
|
||||
async areBuildCredentialsSetupAsync(ctx: Context): Promise<boolean> {
|
||||
async areBuildCredentialsSetupAsync(ctx: CredentialsContext): Promise<boolean> {
|
||||
const buildCredentials = await getBuildCredentialsAsync(ctx, this.app, this.distributionType);
|
||||
return await validateProvisioningProfileAsync(ctx, this.app, buildCredentials);
|
||||
}
|
||||
|
||||
async assignNewAndDeleteOldProfileAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
distCert: AppleDistributionCertificateFragment,
|
||||
currentProfile: AppleProvisioningProfileFragment
|
||||
): Promise<IosAppBuildCredentialsFragment> {
|
||||
@@ -45,7 +45,7 @@ export class SetupProvisioningProfile {
|
||||
}
|
||||
|
||||
async createAndAssignProfileAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
distCert: AppleDistributionCertificateFragment
|
||||
): Promise<IosAppBuildCredentialsFragment> {
|
||||
const provisioningProfile = await new CreateProvisioningProfile(this.app, distCert).runAsync(
|
||||
@@ -61,7 +61,7 @@ export class SetupProvisioningProfile {
|
||||
}
|
||||
|
||||
async configureAndAssignProfileAsync(
|
||||
ctx: Context,
|
||||
ctx: CredentialsContext,
|
||||
distCert: AppleDistributionCertificateFragment,
|
||||
originalProvisioningProfile: AppleProvisioningProfileFragment
|
||||
): Promise<IosAppBuildCredentialsFragment | null> {
|
||||
@@ -83,7 +83,7 @@ export class SetupProvisioningProfile {
|
||||
);
|
||||
}
|
||||
|
||||
async runAsync(ctx: Context): Promise<IosAppBuildCredentialsFragment> {
|
||||
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
|
||||
const distCert = await new SetupDistributionCertificate(
|
||||
this.app,
|
||||
this.distributionType
|
||||
|
||||
Loaded 100 of 199 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user