mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
81
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0aad0bec1d | ||
|
|
18e55dafd4 | ||
|
|
8e013ab793 | ||
|
|
b88e79f0f3 | ||
|
|
35b505120d | ||
|
|
6807d3080b | ||
|
|
0d1cfb3286 | ||
|
|
0b9a5621fe | ||
|
|
9bd15af02a | ||
|
|
aab3bf1330 | ||
|
|
9e95218523 | ||
|
|
bb98e6de03 | ||
|
|
2604228233 | ||
|
|
a7035d3469 | ||
|
|
f6dce0e17c | ||
|
|
ccfa6f9fc3 | ||
|
|
0ee8e2f967 | ||
|
|
94dc4c6e93 | ||
|
|
4144e387d6 | ||
|
|
de4e05e1e0 | ||
|
|
36118b700f | ||
|
|
2fb947b47a | ||
|
|
0f1156e951 | ||
|
|
e9f8be92b7 | ||
|
|
6360ea9f8e | ||
|
|
ad2d8e80f0 | ||
|
|
bc0a5fdaa6 | ||
|
|
d6cfc887c3 | ||
|
|
ce41c436aa | ||
|
|
ec096eaaa2 | ||
|
|
9c32dd5308 | ||
|
|
69d7ee9547 | ||
|
|
e2e4cb67ca | ||
|
|
4a08ad4997 | ||
|
|
107a1c12bd | ||
|
|
8fe5268a73 | ||
|
|
faba1ad8e3 | ||
|
|
ea4885c710 | ||
|
|
4d85002d2b | ||
|
|
535b17f592 | ||
|
|
e556b45d4d | ||
|
|
48130dcbdc | ||
|
|
1419ca6325 | ||
|
|
8513dc0645 | ||
|
|
70e8f8370b | ||
|
|
135c90a8b8 | ||
|
|
fa588c2d24 | ||
|
|
b4ce7a91c2 | ||
|
|
8ca9d66c32 | ||
|
|
a86fabde59 | ||
|
|
40a896675f | ||
|
|
cf7848e3c5 | ||
|
|
3d4394a9bc | ||
|
|
05f4c08d09 | ||
|
|
a4107a86f9 | ||
|
|
a5c695dd88 | ||
|
|
7576d9195d | ||
|
|
838c1508be | ||
|
|
7941683435 | ||
|
|
91f2311a50 | ||
|
|
45d10d9b83 | ||
|
|
f5d5093922 | ||
|
|
fa4687b0f8 | ||
|
|
e78c475118 | ||
|
|
def91af01f | ||
|
|
5eb0fb75ac | ||
|
|
de8c69f766 | ||
|
|
5879ac424d | ||
|
|
e30f62ec12 | ||
|
|
dc4e36e8a8 | ||
|
|
12ded8c9a9 | ||
|
|
7378c6fd0e | ||
|
|
77a8b08834 | ||
|
|
79db9eb482 | ||
|
|
59f6d36114 | ||
|
|
c36c26f5a9 | ||
|
|
d6692f20b5 | ||
|
|
6e37f596df | ||
|
|
d326891ef8 | ||
|
|
46a34327d9 | ||
|
|
97242af30f |
No files matched your search
@@ -34,6 +34,7 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
'no-underscore-dangle': ['error', { allow: ['__typename'] }],
|
||||
'@typescript-eslint/explicit-function-return-type': [
|
||||
'warn',
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Checklist
|
||||
|
||||
- [ ] I've added an entry to [CHANGELOG.md](https://github.com/expo/eas-cli/blob/main/CHANGELOG.md#main) if necessary.
|
||||
- [ ] I've added an entry to [CHANGELOG.md](https://github.com/expo/eas-cli/blob/main/CHANGELOG.md#main) if necessary. You can comment this pull request with `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]` and CHANGELOG.md will be updated automatically.
|
||||
- [ ] I've tagged the changelog entry with `[EAS BUILD API]` if it's a part of a breaking change to EAS Build API (only possible when updating `@expo/eas-build-job` package).
|
||||
|
||||
# Why
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
name: Changelog
|
||||
|
||||
jobs:
|
||||
pr-commented:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.issue.pull_request && github.event.issue.user.login == github.event.comment.user.login && startsWith(github.event.comment.body, '/changelog-entry ') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Checkout Pull Request
|
||||
run: hub pr checkout ${{ github.event.issue.number }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Install dependencies
|
||||
working-directory: ./scripts
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
- name: Add changelog entry
|
||||
working-directory: ./scripts
|
||||
run: |
|
||||
PARAMS=${PR_COMMENT_BODY#/changelog-entry }
|
||||
if [[ ! "$PARAMS" =~ " " ]]; then
|
||||
echo "Invalid /changelog-entry command"
|
||||
exit 1
|
||||
fi
|
||||
CATEGORY=$( cut -d ' ' -f 1 <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
|
||||
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
|
||||
yarn changelog-entry "$CATEGORY" "$MESSAGE"
|
||||
env:
|
||||
GITHUB_PR_AUTHOR: ${{ github.event.issue.user.login }}
|
||||
GITHUB_PR_NUMBER: ${{ github.event.issue.number }}
|
||||
PR_COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
- name: Commit and push
|
||||
run: |
|
||||
# use the last commit author name and email
|
||||
git config --global user.name "$(git --no-pager log --format=format:'%an' -n 1)"
|
||||
git config --global user.email "$(git --no-pager log --format=format:'%ae' -n 1)"
|
||||
|
||||
git add CHANGELOG.md
|
||||
git commit -m "update CHANGELOG.md"
|
||||
git push
|
||||
@@ -142,6 +142,7 @@ jobs:
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
working-directory: ./scripts
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
- name: Resolve version
|
||||
run: echo "EAS_CLI_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
@@ -159,7 +160,7 @@ jobs:
|
||||
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 commit -m "update CHANGELOG.md"
|
||||
git push
|
||||
- name: Prepare changelog for Slack
|
||||
run: |
|
||||
|
||||
+123
-1
@@ -6,12 +6,134 @@ 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
|
||||
|
||||
- Add ASC Api Key generation workflow. ([#718](https://github.com/expo/eas-cli/pull/718) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add support for removal of App Store Connect Api Keys. ([#721](https://github.com/expo/eas-cli/pull/721) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Allow users to assign an ASC Api Key to their project. ([#719](https://github.com/expo/eas-cli/pull/719) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add setup support for ASC Api Keys. ([#733](https://github.com/expo/eas-cli/pull/733) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Show initiating user display name when selecting a build to submit. ([#730](https://github.com/expo/eas-cli/pull/730) by [@barthap](https://github.com/barthap))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Clean up credentials prompt method. ([#728](https://github.com/expo/eas-cli/pull/728) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Bump @expo/apple-utils to 0.0.0-alpha.26. ([#723](https://github.com/expo/eas-cli/pull/723] by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.34.1](https://github.com/expo/eas-cli/releases/tag/0.34.1) - 2021-11-02
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Don't show commit prompt in no-commit workflow after installing `expo-dev-client`. ([#722](https://github.com/expo/eas-cli/pull/722) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add App Store Connect Api Key graphql type and print support. ([#717](https://github.com/expo/eas-cli/pull/717) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.34.0](https://github.com/expo/eas-cli/releases/tag/0.34.0) - 2021-11-01
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- `[EAS BUILD API]` Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701) and [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal))
|
||||
- Don't print logs to `stderr`. ([#708](https://github.com/expo/eas-cli/pull/708) by [@dsokal](https://github.com/dsokal))
|
||||
- Remove automatic migration for legacy `eas.json` format introduced in [v0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2). ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add required `cli` field in root of `eas.json` ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Better error message when eas.json is invalid. ([#707](https://github.com/expo/eas-cli/pull/707) by [@dsokal](https://github.com/dsokal))
|
||||
- Fix credentials workflow for new users: add additionalTypenames for credentials. ([#703](https://github.com/expo/eas-cli/pull/703) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add additionalTypenames for other queries. ([#704](https://github.com/expo/eas-cli/pull/704) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Fix errors after pulling down most recent graphql schema changes. ([#713](https://github.com/expo/eas-cli/pull/713) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Refactor credentials manager. ([#712](https://github.com/expo/eas-cli/pull/712) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Remove unneeded parameters. ([#710](https://github.com/expo/eas-cli/pull/710) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Enforce additionalTypenames for graphql queries. ([#709](https://github.com/expo/eas-cli/pull/709) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.33.1](https://github.com/expo/eas-cli/releases/tag/0.33.1) - 2021-10-22
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Compute runtime version using config-plugins. ([#697](https://github.com/expo/eas-cli/pull/697) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [eas-cli] Skip validating updates scripts integration, which no longer exist in SDK 43+ ([#706](https://github.com/expo/eas-cli/pull/706) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.33.0](https://github.com/expo/eas-cli/releases/tag/0.33.0) - 2021-10-20
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Make "production" the default profile for building and submitting. ([#677](https://github.com/expo/eas-cli/pull/677)) by [@jonsamp](https://github.com/jonsamp)
|
||||
- Previously, the default profile when running `eas build` or `eas submit` was "release". We're changing it to a more recognizable name that is consistent with our docs, which is now "production". You can always specify a profile manually with the `--profile` flag. For this major version, if you do not have a profile named "production", and still have a profile named "release", we will fall back to the "release" profile as the default, however you'll see a warning as we're going to remove that behavior in the next major release of EAS CLI.
|
||||
- To upgrade, update **eas.json** to have a "production" profile in both the `build` and `submit` objects. If you already have a project set up, this will replace the existing "release" profile. After the change, eas.json should have the following profiles:
|
||||
```json
|
||||
{
|
||||
"build": {
|
||||
"production": { ... }
|
||||
},
|
||||
"submit": {
|
||||
"production": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Skip the second prompt for Apple ID if the user is already signed in with Apple. ([#691](https://github.com/expo/eas-cli/pull/691) by [@dsokal](https://github.com/dsokal))
|
||||
- Unify reading `app.json`. ([#692](https://github.com/expo/eas-cli/pull/692) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.32.0](https://github.com/expo/eas-cli/releases/tag/0.32.0) - 2021-10-15
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add App Store Connect Api Key support to iOS submissions. ([#678](https://github.com/expo/eas-cli/pull/678) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Create/list/revoke App Store Connect Api keys via Apple apis. ([#687](https://github.com/expo/eas-cli/pull/687) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add ability to select a build from a list in `eas submit` interactive mode. ([#688](https://github.com/expo/eas-cli/pull/688) by [@barthap](https://github.com/barthap))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix printing App Store Connect URL after submission. ([#683](https://github.com/expo/eas-cli/pull/683) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add App Store Connect Api Key fields to `eas.json`. ([#684](https://github.com/expo/eas-cli/pull/684) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Enable no-underscore-dangle eslint rule. ([#686](https://github.com/expo/eas-cli/pull/686) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.31.1](https://github.com/expo/eas-cli/releases/tag/0.31.1) - 2021-10-08
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Google Service Account Keys: Fix non-interactive bug in credentials service workflow ([#682](https://github.com/expo/eas-cli/pull/682) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.31.0](https://github.com/expo/eas-cli/releases/tag/0.31.0) - 2021-10-08
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Don't resolve the iOS builder image on the client side. EAS Build will use the appropriate iOS image for a given Expo SDK version unless the `image` is defined. This only applies to managed projects. ([#675](https://github.com/expo/eas-cli/pull/675) by [@wkozyra95](https://github.com/wkozyra95) + [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Integrate credentials service with Android submissions. ([#664](https://github.com/expo/eas-cli/pull/664) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add option to review ad-hoc devices when reusing provisioning profile. ([#673](https://github.com/expo/eas-cli/pull/673) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.30.1](https://github.com/expo/eas-cli/releases/tag/0.30.1) - 2021-10-06
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `--json` flag when running EAS CLI on GitHub actions. ([#669](https://github.com/expo/eas-cli/pull/669) by [@dsokal](https://github.com/dsokal))
|
||||
- Fix `"ios: mods.ios.infoPlist: Failed to find Info.plist linked to Xcode project."` warning when running `eas build` in a managed project.([#670](https://github.com/expo/eas-cli/pull/670) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Fix building monorepo projects on Windows. ([#671](https://github.com/expo/eas-cli/pull/671) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.30.0](https://github.com/expo/eas-cli/releases/tag/v0.30.0) - 2021-10-05
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Enable auto-incrementing on Android. ([#645](https://github.com/expo/eas-cli/pull/645) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Ports detection for Google Service Account Keys into the credentials service. ([#660](https://github.com/expo/eas-cli/pull/660) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
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 `Publish` commit in [expo/eas-build](https://github.com/expo/eas-build/commits) for that version. In most cases, it will be the latest `Publish` commit.
|
||||
- Find the `eas-cli-local-build-plugin` version published in that commit.
|
||||

|
||||
- Run `npm dist-tags add eas-cli-local-build-plugin@VERSION latest` with the version from the previous step.
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.30.0"
|
||||
"version": "0.34.1"
|
||||
}
|
||||
+5
-7
@@ -1,9 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"scripts"
|
||||
],
|
||||
"workspaces": ["packages/*"],
|
||||
"scripts": {
|
||||
"build": "lerna run build",
|
||||
"build:tarballs:linux": "yarn build && yarn workspace eas-cli oclif-dev pack --targets linux-x64",
|
||||
@@ -13,7 +10,8 @@
|
||||
"eas": "packages/eas-cli/bin/run",
|
||||
"lint": "eslint . --ext .ts --ext .js",
|
||||
"release": "lerna version",
|
||||
"test": "jest"
|
||||
"test": "jest",
|
||||
"clean": "lerna run clean && rm -rf node_modules coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/oclif-dev-cli": "1.24.0-expo",
|
||||
@@ -31,7 +29,7 @@
|
||||
"typescript": "4.4.2"
|
||||
},
|
||||
"volta": {
|
||||
"node": "16.8.0",
|
||||
"yarn": "1.22.11"
|
||||
"node": "16.10.0",
|
||||
"yarn": "1.22.15"
|
||||
}
|
||||
}
|
||||
+32
-30
@@ -73,7 +73,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_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)_
|
||||
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/login.js)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -87,7 +87,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_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)_
|
||||
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/logout.js)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -101,7 +101,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_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)_
|
||||
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/view.js)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -112,7 +112,7 @@ USAGE
|
||||
$ eas analytics [STATUS]
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/analytics.js)_
|
||||
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/analytics.js)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -138,14 +138,15 @@ OPTIONS
|
||||
|
||||
--non-interactive Run command in non-interactive mode
|
||||
|
||||
--profile=PROFILE_NAME [default: release] Name of the build profile from eas.json
|
||||
--profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined
|
||||
in eas.json.
|
||||
|
||||
--skip-project-configuration Skip project configuration
|
||||
|
||||
--[no-]wait Wait for build(s) to complete
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/build/index.js)_
|
||||
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/index.js)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -156,7 +157,7 @@ USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
```
|
||||
|
||||
_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)_
|
||||
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/cancel.js)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -170,7 +171,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.30.0/packages/eas-cli/src/commands/build/configure.js)_
|
||||
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/configure.js)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -203,7 +204,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.30.0/packages/eas-cli/src/commands/build/list.js)_
|
||||
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/list.js)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -217,7 +218,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.30.0/packages/eas-cli/src/commands/build/view.js)_
|
||||
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/view.js)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -232,7 +233,7 @@ OPTIONS
|
||||
--profile=profile
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/config.js)_
|
||||
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/config.js)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -243,7 +244,7 @@ USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/credentials.js)_
|
||||
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/credentials.js)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -254,7 +255,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_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)_
|
||||
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/create.js)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -268,7 +269,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_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)_
|
||||
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/list.js)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -279,7 +280,7 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_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)_
|
||||
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/view.js)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -290,7 +291,7 @@ USAGE
|
||||
$ eas diagnostics
|
||||
```
|
||||
|
||||
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/diagnostics.js)_
|
||||
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/diagnostics.js)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -318,7 +319,7 @@ USAGE
|
||||
$ eas project:info
|
||||
```
|
||||
|
||||
_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)_
|
||||
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/project/info.js)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -332,7 +333,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_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)_
|
||||
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/project/init.js)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
@@ -349,7 +350,7 @@ OPTIONS
|
||||
--value=value Value of the secret
|
||||
```
|
||||
|
||||
_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)_
|
||||
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/create.js)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -363,10 +364,10 @@ OPTIONS
|
||||
--id=id ID of the secret to delete
|
||||
|
||||
DESCRIPTION
|
||||
Unsure where to find the secret's ID? Run eas secrets:list
|
||||
Unsure where to find the secret's ID? Run eas secret:list
|
||||
```
|
||||
|
||||
_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)_
|
||||
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/delete.js)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -377,7 +378,7 @@ USAGE
|
||||
$ eas secret:list
|
||||
```
|
||||
|
||||
_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)_
|
||||
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/list.js)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -394,7 +395,7 @@ OPTIONS
|
||||
--non-interactive Run command in non-interactive mode
|
||||
--path=path Path to the .apk/.aab/.ipa file
|
||||
|
||||
--profile=profile Name of the submit profile from eas.json. Defaults to "release" if defined in
|
||||
--profile=profile Name of the submit profile from eas.json. Defaults to "production" if defined in
|
||||
eas.json.
|
||||
|
||||
--url=url App archive url
|
||||
@@ -404,13 +405,14 @@ OPTIONS
|
||||
--[no-]wait Wait for submission to complete
|
||||
|
||||
DESCRIPTION
|
||||
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
|
||||
See how to configure submits with eas.json:
|
||||
https://docs.expo.dev/submit/eas-json/
|
||||
|
||||
ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/submit.js)_
|
||||
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/submit.js)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -429,7 +431,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_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)_
|
||||
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/create.js)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -443,7 +445,7 @@ ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
```
|
||||
|
||||
_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)_
|
||||
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/delete.js)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -457,7 +459,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.30.0/packages/eas-cli/src/commands/webhook/list.js)_
|
||||
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/list.js)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -477,7 +479,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_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)_
|
||||
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/update.js)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -491,5 +493,5 @@ ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
```
|
||||
|
||||
_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)_
|
||||
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/view.js)_
|
||||
<!-- commandsstop -->
|
||||
Generated
+1083
-1149
File diff suppressed because it is too large.
Load diff
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.30.0",
|
||||
"version": "0.34.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "0.0.0-alpha.25",
|
||||
"@expo/config": "5.0.9",
|
||||
"@expo/config-plugins": "3.1.0",
|
||||
"@expo/eas-build-job": "0.2.48",
|
||||
"@expo/eas-json": "^0.30.0",
|
||||
"@expo/apple-utils": "0.0.0-alpha.26",
|
||||
"@expo/config": "6.0.3",
|
||||
"@expo/config-plugins": "4.0.3",
|
||||
"@expo/eas-build-job": "0.2.57",
|
||||
"@expo/eas-json": "^0.34.1",
|
||||
"@expo/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/prebuild-config": "3.0.3",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.0",
|
||||
"@expo/sdk-runtime-versions": "1.0.0",
|
||||
@@ -61,6 +61,7 @@
|
||||
"qrcode-terminal": "0.12.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.3.5",
|
||||
"slash": "3.0.0",
|
||||
"strip-ansi": "6.0.0",
|
||||
"tar": "6.1.11",
|
||||
"terminal-link": "2.1.1",
|
||||
@@ -112,7 +113,6 @@
|
||||
"cli"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "build/index.js",
|
||||
"oclif": {
|
||||
"bin": "eas",
|
||||
"commands": "./build/commands",
|
||||
@@ -176,6 +176,7 @@
|
||||
"typecheck": "tsc",
|
||||
"test": "jest",
|
||||
"version": "yarn oclif-dev readme && node patch-readme && git add README.md",
|
||||
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml"
|
||||
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
|
||||
"clean": "rm -rf dist build node_modules yarn-error.log"
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,7 @@ import { URL } from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import UserSettings from './user/UserSettings';
|
||||
|
||||
/**
|
||||
* We use require() to exclude package.json from TypeScript's analysis since it lives outside
|
||||
* the src directory and would change the directory structure of the emitted files
|
||||
* under the build directory
|
||||
*/
|
||||
const packageJSON = require('../package.json');
|
||||
import { easCliVersion } from './utils/easCli';
|
||||
|
||||
const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
|
||||
darwin: 'Mac',
|
||||
@@ -97,7 +91,7 @@ export function logEvent(name: string, properties: Record<string, any> = {}): vo
|
||||
ensureUserIdentified();
|
||||
|
||||
const { userId, deviceId } = identifyData ?? {};
|
||||
const commonEventProperties = { source_version: packageJSON?.version, source: 'eas cli' };
|
||||
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
|
||||
|
||||
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
|
||||
rudderstackClient.track({
|
||||
@@ -126,7 +120,7 @@ function getRudderStackContext(): Record<string, any> {
|
||||
return {
|
||||
os: { name: platform, version: os.release() },
|
||||
device: { type: platform, model: platform },
|
||||
app: { name: 'eas cli', version: packageJSON?.version ?? undefined },
|
||||
app: { name: 'eas cli', version: easCliVersion ?? undefined },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig, AndroidManifest } from '@expo/config-plugins';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import Log from '../../log';
|
||||
import { getProjectAccountName } from '../../project/projectUtils';
|
||||
@@ -10,17 +9,6 @@ import { ensureValidVersions } from '../utils/updates';
|
||||
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
|
||||
|
||||
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
|
||||
const updatedBuildGradleContents =
|
||||
AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
|
||||
projectDir,
|
||||
buildGradleContents
|
||||
);
|
||||
await fs.writeFile(buildGradlePath, updatedBuildGradleContents);
|
||||
}
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
@@ -39,7 +27,7 @@ export async function syncUpdatesConfigurationAsync(
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
try {
|
||||
await ensureUpdatesConfiguredAsync(projectDir, exp);
|
||||
await ensureUpdatesConfiguredAsync(projectDir);
|
||||
} catch (error) {
|
||||
Log.error(
|
||||
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
|
||||
@@ -62,15 +50,9 @@ export async function syncUpdatesConfigurationAsync(
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
|
||||
|
||||
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
|
||||
const gradleScriptApply = AndroidConfig.Updates.formatApplyLineForBuildGradle(projectDir);
|
||||
throw new Error(`Missing ${gradleScriptApply} in ${buildGradlePath}`);
|
||||
}
|
||||
|
||||
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
|
||||
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in AndroidManifest.xml
|
||||
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
|
||||
if (!AndroidConfig.Updates.isMainApplicationMetaDataSet(androidManifest)) {
|
||||
|
||||
@@ -31,7 +31,7 @@ beforeEach(async () => {
|
||||
await fs.mkdirp(os.tmpdir());
|
||||
});
|
||||
|
||||
// generic workflow
|
||||
// bare workflow
|
||||
describe(bumpVersionAsync, () => {
|
||||
it('throws an informative error when multiple flavors are detected in the android project', async () => {
|
||||
await initProjectWithGradleFileAsync(
|
||||
@@ -69,7 +69,7 @@ describe(bumpVersionAsync, () => {
|
||||
});
|
||||
it('bumps expo.android.versionCode and buildGradle versionCode when strategy = BumpStrategy.VERSION_CODE', async () => {
|
||||
const nativeVersionCode = 100; // this should be overwritten by bumpVersionAsync
|
||||
const fakeExp = initGenericProject({ versionCode: nativeVersionCode });
|
||||
const fakeExp = initBareWorkflowProject({ versionCode: nativeVersionCode });
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.VERSION_CODE,
|
||||
@@ -91,7 +91,7 @@ describe(bumpVersionAsync, () => {
|
||||
|
||||
it('bumps expo.version and gradle versionCode when strategy = BumpStrategy.SHORT_VERSION', async () => {
|
||||
const nativeVersionName = '1.0.0'; // this should be overwritten by bumpVersionAsync
|
||||
const fakeExp = initGenericProject({ versionName: nativeVersionName });
|
||||
const fakeExp = initBareWorkflowProject({ versionName: nativeVersionName });
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||
@@ -112,7 +112,7 @@ describe(bumpVersionAsync, () => {
|
||||
});
|
||||
|
||||
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
|
||||
const fakeExp = initGenericProject();
|
||||
const fakeExp = initBareWorkflowProject();
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.NOOP,
|
||||
@@ -185,9 +185,9 @@ describe(bumpVersionInAppJsonAsync, () => {
|
||||
});
|
||||
|
||||
describe(maybeResolveVersionsAsync, () => {
|
||||
describe('generic project', () => {
|
||||
describe('bare project', () => {
|
||||
it('reads the versions from native code', async () => {
|
||||
const exp = initGenericProject();
|
||||
const exp = initBareWorkflowProject();
|
||||
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
|
||||
'/repo',
|
||||
exp,
|
||||
@@ -211,7 +211,7 @@ describe(maybeResolveVersionsAsync, () => {
|
||||
});
|
||||
});
|
||||
|
||||
function initGenericProject({
|
||||
function initBareWorkflowProject({
|
||||
versionCode = 123,
|
||||
versionName = '3.0.0',
|
||||
}: {
|
||||
|
||||
@@ -6,10 +6,10 @@ import { AndroidBuildProfile, AndroidVersionAutoIncrement } from '@expo/eas-json
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
warnIfAndroidPackageDefinedInAppConfigForGenericProject,
|
||||
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject,
|
||||
} from '../../project/android/applicationId';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import vcs from '../../vcs';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { ConfigureContext } from '../context';
|
||||
import { isExpoUpdatesInstalled } from '../utils/updates';
|
||||
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
|
||||
@@ -21,12 +21,12 @@ export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void
|
||||
return;
|
||||
}
|
||||
|
||||
warnIfAndroidPackageDefinedInAppConfigForGenericProject(ctx.projectDir, ctx.exp);
|
||||
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
|
||||
|
||||
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
|
||||
|
||||
const easGradlePath = AndroidConfig.EasBuild.getEasBuildGradlePath(ctx.projectDir);
|
||||
await vcs.trackFileAsync(easGradlePath);
|
||||
await getVcsClient().trackFileAsync(easGradlePath);
|
||||
|
||||
if (isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
|
||||
|
||||
@@ -17,15 +17,14 @@ export function transformJob(job: Android.Job): AndroidJobInput {
|
||||
artifactPath: job.artifactPath,
|
||||
username: job.username,
|
||||
buildType: job.buildType && transformBuildType(job.buildType),
|
||||
developmentClient: job.developmentClient,
|
||||
};
|
||||
}
|
||||
|
||||
function transformBuildType(buildType: Android.BuildType): AndroidBuildType {
|
||||
if (buildType === Android.BuildType.APK) {
|
||||
return AndroidBuildType.Apk;
|
||||
} else if (buildType === Android.BuildType.APP_BUNDLE) {
|
||||
return AndroidBuildType.AppBundle;
|
||||
} else {
|
||||
return AndroidBuildType.DevelopmentClient;
|
||||
return AndroidBuildType.AppBundle;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile } from '@expo/eas-json';
|
||||
import path from 'path';
|
||||
import slash from 'slash';
|
||||
|
||||
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import vcs from '../../vcs';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { BuildContext } from '../context';
|
||||
|
||||
interface JobData {
|
||||
@@ -25,7 +26,8 @@ export async function prepareJobAsync(
|
||||
): Promise<Job> {
|
||||
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
|
||||
const buildProfile: AndroidBuildProfile = ctx.buildProfile;
|
||||
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
|
||||
const projectRootDirectory =
|
||||
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
|
||||
const { credentials } = jobData;
|
||||
const buildCredentials = credentials
|
||||
? {
|
||||
@@ -51,7 +53,7 @@ export async function prepareJobAsync(
|
||||
projectRootDirectory,
|
||||
projectArchive: jobData.projectArchive,
|
||||
builderEnvironment: {
|
||||
image: buildProfile.image ?? 'default',
|
||||
image: buildProfile.image,
|
||||
node: buildProfile.node,
|
||||
yarn: buildProfile.yarn,
|
||||
ndk: buildProfile.ndk,
|
||||
@@ -68,12 +70,11 @@ export async function prepareJobAsync(
|
||||
},
|
||||
releaseChannel: ctx.buildProfile.releaseChannel,
|
||||
updates: { channel: ctx.buildProfile.channel },
|
||||
|
||||
developmentClient: buildProfile.developmentClient,
|
||||
gradleCommand: buildProfile.gradleCommand,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
|
||||
buildType,
|
||||
username,
|
||||
buildType: buildProfile.developmentClient ? Android.BuildType.DEVELOPMENT_CLIENT : buildType,
|
||||
};
|
||||
|
||||
return sanitizeJob(job);
|
||||
|
||||
@@ -13,7 +13,7 @@ import { uploadAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
import { sleepAsync } from '../utils/promise';
|
||||
import vcs from '../vcs';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { BuildContext } from './context';
|
||||
import { runLocalBuildAsync } from './local';
|
||||
import { MetadataContext, collectMetadataAsync } from './metadata';
|
||||
@@ -68,7 +68,7 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
});
|
||||
}
|
||||
|
||||
if (await vcs.hasUncommittedChangesAsync()) {
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
Log.addNewLineIfNone();
|
||||
await reviewAndCommitChangesAsync(
|
||||
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
|
||||
|
||||
@@ -10,7 +10,8 @@ import { RequestedPlatform } from '../platform';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { confirmAsync, promptAsync } from '../prompts';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import vcs from '../vcs';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { configureAndroidAsync } from './android/configure';
|
||||
import { ConfigureContext } from './context';
|
||||
import { configureIosAsync } from './ios/configure';
|
||||
@@ -37,7 +38,7 @@ export async function ensureProjectConfiguredAsync(
|
||||
projectDir,
|
||||
platform: requestedPlatform,
|
||||
});
|
||||
if (await vcs.hasUncommittedChangesAsync()) {
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
error(
|
||||
'Build process requires clean working tree, please commit all your changes and run `eas build` again',
|
||||
{ exit: 1 }
|
||||
@@ -57,7 +58,7 @@ export async function configureAsync(options: {
|
||||
platform: RequestedPlatform;
|
||||
projectDir: string;
|
||||
}): Promise<void> {
|
||||
await vcs.ensureRepoExistsAsync();
|
||||
await getVcsClient().ensureRepoExistsAsync();
|
||||
await maybeBailOnRepoStatusAsync();
|
||||
|
||||
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
|
||||
@@ -86,31 +87,39 @@ export async function configureAsync(options: {
|
||||
await configureIosAsync(ctx);
|
||||
}
|
||||
|
||||
if (await vcs.hasUncommittedChangesAsync()) {
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
Log.newLine();
|
||||
await reviewAndCommitChangesAsync(configureCommitMessage[options.platform]);
|
||||
} else {
|
||||
} else if (!(await getVcsClient().hasUncommittedChangesAsync())) {
|
||||
Log.newLine();
|
||||
Log.withTick('No changes were necessary, the project is already configured correctly.');
|
||||
}
|
||||
}
|
||||
|
||||
const EAS_JSON_MANAGED_DEFAULT: EasJson = {
|
||||
cli: {
|
||||
version: `>= ${easCliVersion}`,
|
||||
},
|
||||
build: {
|
||||
release: {},
|
||||
development: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
},
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
},
|
||||
production: {},
|
||||
},
|
||||
submit: {
|
||||
release: {},
|
||||
production: {},
|
||||
},
|
||||
};
|
||||
|
||||
const EAS_JSON_GENERIC_DEFAULT: EasJson = {
|
||||
const EAS_JSON_BARE_DEFAULT: EasJson = {
|
||||
cli: {
|
||||
version: `>= ${easCliVersion}`,
|
||||
},
|
||||
build: {
|
||||
release: {},
|
||||
development: {
|
||||
distribution: 'internal',
|
||||
android: {
|
||||
@@ -120,9 +129,13 @@ const EAS_JSON_GENERIC_DEFAULT: EasJson = {
|
||||
buildConfiguration: 'Debug',
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
},
|
||||
production: {},
|
||||
},
|
||||
submit: {
|
||||
release: {},
|
||||
production: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -139,11 +152,11 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
|
||||
|
||||
const easJson =
|
||||
ctx.hasAndroidNativeProject && ctx.hasIosNativeProject
|
||||
? EAS_JSON_GENERIC_DEFAULT
|
||||
? EAS_JSON_BARE_DEFAULT
|
||||
: EAS_JSON_MANAGED_DEFAULT;
|
||||
|
||||
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
|
||||
await vcs.trackFileAsync(easJsonPath);
|
||||
await getVcsClient().trackFileAsync(easJsonPath);
|
||||
Log.withTick('Generated eas.json');
|
||||
}
|
||||
|
||||
@@ -177,7 +190,7 @@ async function reviewAndCommitChangesAsync(
|
||||
await commitPromptAsync({ initialCommitMessage });
|
||||
Log.withTick('Committed changes');
|
||||
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
|
||||
await vcs.showDiffAsync();
|
||||
await getVcsClient().showDiffAsync();
|
||||
await reviewAndCommitChangesAsync(initialCommitMessage, false);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,16 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import Log from '../../log';
|
||||
import { getProjectAccountName } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import vcs from '../../vcs';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { ensureValidVersions } from '../utils/updates';
|
||||
import { readPlistAsync, writePlistAsync } from './plist';
|
||||
|
||||
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
let xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
|
||||
|
||||
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, xcodeProject)) {
|
||||
xcodeProject = IOSConfig.Updates.ensureBundleReactNativePhaseContainsConfigurationScript(
|
||||
projectDir,
|
||||
xcodeProject
|
||||
);
|
||||
await fs.writeFile(
|
||||
IOSConfig.Paths.getPBXProjectPath(projectDir),
|
||||
// eslint-disable-next-line node/no-sync
|
||||
xcodeProject.writeSync()
|
||||
);
|
||||
}
|
||||
|
||||
let expoPlist = await readExpoPlistAsync(projectDir);
|
||||
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
|
||||
@@ -41,7 +27,7 @@ export async function syncUpdatesConfigurationAsync(
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
try {
|
||||
await ensureUpdatesConfiguredAsync(projectDir, exp);
|
||||
await ensureUpdatesConfiguredAsync(projectDir);
|
||||
} catch (error) {
|
||||
Log.error(
|
||||
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
|
||||
@@ -62,15 +48,9 @@ export async function syncUpdatesConfigurationAsync(
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
const xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
|
||||
|
||||
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, xcodeProject)) {
|
||||
const script = 'expo-updates/scripts/create-manifest-ios.sh';
|
||||
const buildPhase = '"Bundle React Native code and images"';
|
||||
throw new Error(`Path to ${script} is missing in a ${buildPhase} build phase.`);
|
||||
}
|
||||
|
||||
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
|
||||
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in Expo.plist
|
||||
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
|
||||
const expoPlist = await readExpoPlistAsync(projectDir);
|
||||
if (!IOSConfig.Updates.isPlistConfigurationSet(expoPlist)) {
|
||||
throw new Error('Missing values in Expo.plist');
|
||||
@@ -88,7 +68,7 @@ async function writeExpoPlistAsync(
|
||||
): Promise<void> {
|
||||
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
|
||||
await writePlistAsync(expoPlistPath, expoPlist);
|
||||
await vcs.trackFileAsync(expoPlistPath);
|
||||
await getVcsClient().trackFileAsync(expoPlistPath);
|
||||
}
|
||||
|
||||
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
|
||||
@@ -49,10 +49,10 @@ describe(evaluateTemplateString, () => {
|
||||
});
|
||||
});
|
||||
|
||||
// generic workflow
|
||||
// bare workflow
|
||||
describe(bumpVersionAsync, () => {
|
||||
it('bumps expo.ios.buildNumber and CFBundleVersion when strategy = BumpStrategy.BUILD_NUMBER', async () => {
|
||||
const fakeExp = initGenericProject();
|
||||
const fakeExp = initBareWorkflowProject();
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
||||
@@ -74,7 +74,7 @@ describe(bumpVersionAsync, () => {
|
||||
});
|
||||
|
||||
it('bumps expo.ios.buildNumber and CFBundleVersion for non default Info.plist location', async () => {
|
||||
const fakeExp = initGenericProject({ infoPlistName: 'Info2.plist' });
|
||||
const fakeExp = initBareWorkflowProject({ infoPlistName: 'Info2.plist' });
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
||||
@@ -98,7 +98,7 @@ describe(bumpVersionAsync, () => {
|
||||
});
|
||||
|
||||
it('bumps expo.version and CFBundleShortVersionString when strategy = BumpStrategy.SHORT_VERSION', async () => {
|
||||
const fakeExp = initGenericProject();
|
||||
const fakeExp = initBareWorkflowProject();
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||
@@ -120,7 +120,7 @@ describe(bumpVersionAsync, () => {
|
||||
});
|
||||
|
||||
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
|
||||
const fakeExp = initGenericProject();
|
||||
const fakeExp = initBareWorkflowProject();
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.NOOP,
|
||||
@@ -194,9 +194,9 @@ describe(bumpVersionInAppJsonAsync, () => {
|
||||
});
|
||||
|
||||
describe(readBuildNumberAsync, () => {
|
||||
describe('generic project', () => {
|
||||
describe('bare project', () => {
|
||||
it('reads the build number from native code', async () => {
|
||||
const exp = initGenericProject();
|
||||
const exp = initBareWorkflowProject();
|
||||
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
|
||||
expect(buildNumber).toBe('1');
|
||||
});
|
||||
@@ -212,14 +212,14 @@ describe(readBuildNumberAsync, () => {
|
||||
});
|
||||
|
||||
describe(readShortVersionAsync, () => {
|
||||
describe('generic project', () => {
|
||||
describe('bare project', () => {
|
||||
it('reads the short version from native code', async () => {
|
||||
const exp = initGenericProject();
|
||||
const exp = initBareWorkflowProject();
|
||||
const appVersion = await readShortVersionAsync('/repo', exp, {});
|
||||
expect(appVersion).toBe('1.0.0');
|
||||
});
|
||||
it('evaluates interpolated build number', async () => {
|
||||
const exp = initGenericProject({
|
||||
const exp = initBareWorkflowProject({
|
||||
appVersion: '$(CURRENT_PROJECT_VERSION)',
|
||||
});
|
||||
const buildNumber = await readShortVersionAsync('/repo', exp, {
|
||||
@@ -231,7 +231,7 @@ describe(readShortVersionAsync, () => {
|
||||
|
||||
describe('managed project', () => {
|
||||
it('reads the version from app config', async () => {
|
||||
const exp = initGenericProject();
|
||||
const exp = initBareWorkflowProject();
|
||||
const appVersion = await readShortVersionAsync('/repo', exp, {});
|
||||
expect(appVersion).toBe('1.0.0');
|
||||
});
|
||||
@@ -286,7 +286,7 @@ describe(getInfoPlistPath, () => {
|
||||
});
|
||||
});
|
||||
|
||||
function initGenericProject({
|
||||
function initBareWorkflowProject({
|
||||
appVersion = '1.0.0',
|
||||
version = '1',
|
||||
infoPlistName = 'Info.plist',
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { XCBuildConfiguration } from 'xcode';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
|
||||
warnIfBundleIdentifierDefinedInAppConfigForGenericProject,
|
||||
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject,
|
||||
} from '../../project/ios/bundleIdentifier';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { ConfigureContext } from '../context';
|
||||
@@ -20,7 +20,7 @@ export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
warnIfBundleIdentifierDefinedInAppConfigForGenericProject(ctx.projectDir, ctx.exp);
|
||||
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
|
||||
|
||||
if (isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { Env, Ios } from '@expo/eas-build-job';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import {
|
||||
DistributionType,
|
||||
IosBuildType,
|
||||
IosJobInput,
|
||||
IosJobSecretsInput,
|
||||
} from '../../graphql/generated';
|
||||
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
|
||||
import { transformProjectArchive, transformWorkflow } from '../graphql';
|
||||
|
||||
export function transformJob(job: Ios.Job): IosJobInput {
|
||||
@@ -16,30 +11,18 @@ export function transformJob(job: Ios.Job): IosJobInput {
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
updates: job.updates,
|
||||
distribution: job.distribution && transformDistributionType(job.distribution),
|
||||
secrets: transformIosSecrets(job.secrets),
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
|
||||
scheme: job.scheme,
|
||||
buildConfiguration: job.buildConfiguration,
|
||||
artifactPath: job.artifactPath,
|
||||
|
||||
buildType: job.buildType && transformBuildType(job.buildType),
|
||||
username: job.username,
|
||||
developmentClient: job.developmentClient,
|
||||
simulator: job.simulator,
|
||||
};
|
||||
}
|
||||
|
||||
function transformDistributionType(distributionType: Ios.DistributionType): DistributionType {
|
||||
if (distributionType === 'store') {
|
||||
return DistributionType.Store;
|
||||
} else if (distributionType === 'internal') {
|
||||
return DistributionType.Internal;
|
||||
} else {
|
||||
return DistributionType.Simulator;
|
||||
}
|
||||
}
|
||||
|
||||
function transformIosSecrets(secrets: {
|
||||
buildCredentials?: Ios.BuildCredentials;
|
||||
env?: Env;
|
||||
@@ -61,11 +44,3 @@ function transformIosSecrets(secrets: {
|
||||
environmentSecrets: secrets.env,
|
||||
};
|
||||
}
|
||||
|
||||
function transformBuildType(buildType: Ios.BuildType): IosBuildType {
|
||||
if (buildType === Ios.BuildType.DEVELOPMENT_CLIENT) {
|
||||
return IosBuildType.DevelopmentClient;
|
||||
} else {
|
||||
return IosBuildType.Release;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
import slash from 'slash';
|
||||
|
||||
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import vcs from '../../vcs';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { BuildContext } from '../context';
|
||||
|
||||
interface JobData {
|
||||
@@ -24,7 +24,8 @@ export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData
|
||||
): Promise<Job> {
|
||||
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
|
||||
const projectRootDirectory =
|
||||
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
|
||||
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
|
||||
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
|
||||
if (jobData.credentials) {
|
||||
@@ -39,9 +40,8 @@ export async function prepareJobAsync(
|
||||
platform: Platform.IOS,
|
||||
projectArchive: jobData.projectArchive,
|
||||
projectRootDirectory,
|
||||
distribution: ctx.buildProfile.simulator ? 'simulator' : ctx.buildProfile.distribution,
|
||||
builderEnvironment: {
|
||||
image: resolveImage(ctx),
|
||||
image: ctx.buildProfile.image,
|
||||
node: ctx.buildProfile.node,
|
||||
yarn: ctx.buildProfile.yarn,
|
||||
bundler: ctx.buildProfile.bundler,
|
||||
@@ -60,30 +60,16 @@ export async function prepareJobAsync(
|
||||
},
|
||||
releaseChannel: ctx.buildProfile.releaseChannel,
|
||||
updates: { channel: ctx.buildProfile.channel },
|
||||
|
||||
developmentClient: ctx.buildProfile.developmentClient,
|
||||
simulator: ctx.buildProfile.simulator,
|
||||
scheme: jobData.buildScheme,
|
||||
buildConfiguration: ctx.buildProfile.buildConfiguration,
|
||||
artifactPath: ctx.buildProfile.artifactPath,
|
||||
|
||||
buildType: ctx.buildProfile.developmentClient
|
||||
? Ios.BuildType.DEVELOPMENT_CLIENT
|
||||
: Ios.BuildType.RELEASE,
|
||||
username,
|
||||
};
|
||||
return sanitizeJob(job);
|
||||
}
|
||||
|
||||
function resolveImage(ctx: BuildContext<Platform.IOS>): Ios.BuilderEnvironment['image'] {
|
||||
// see https://linear.app/expo/issue/ENG-1396/make-default-image-dependent-on-sdk-version
|
||||
if (!ctx.buildProfile.image && ctx.exp.sdkVersion) {
|
||||
const majorSdkVersion = semver.major(ctx.exp.sdkVersion);
|
||||
if (majorSdkVersion <= 41) {
|
||||
return 'macos-catalina-10.15-xcode-12.4';
|
||||
}
|
||||
}
|
||||
return ctx.buildProfile.image ?? 'default';
|
||||
}
|
||||
|
||||
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
|
||||
return {
|
||||
provisioningProfileBase64: targetCredentials.provisioningProfile,
|
||||
|
||||
@@ -7,7 +7,8 @@ import { GradleBuildContext } from '../project/android/gradle';
|
||||
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
|
||||
import { getUsername } from '../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import vcs from '../vcs';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import {
|
||||
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
|
||||
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
|
||||
@@ -21,13 +22,6 @@ import {
|
||||
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
|
||||
import { isExpoUpdatesInstalled } from './utils/updates';
|
||||
|
||||
/**
|
||||
* We use require() to exclude package.json from TypeScript's analysis since it lives outside
|
||||
* the src directory and would change the directory structure of the emitted files
|
||||
* under the build directory
|
||||
*/
|
||||
const packageJSON = require('../../package.json');
|
||||
|
||||
export type MetadataContext<T extends Platform> = T extends Platform.ANDROID
|
||||
? AndroidMetadataContext
|
||||
: IosMetadataContext;
|
||||
@@ -53,7 +47,7 @@ export async function collectMetadataAsync<T extends Platform>(
|
||||
const metadata = {
|
||||
trackingContext: ctx.trackingCtx,
|
||||
...(await maybeResolveVersionsAsync(ctx, platformContext)),
|
||||
cliVersion: packageJSON.version,
|
||||
cliVersion: easCliVersion,
|
||||
workflow: ctx.workflow,
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
sdkVersion: ctx.exp.sdkVersion,
|
||||
@@ -63,7 +57,8 @@ export async function collectMetadataAsync<T extends Platform>(
|
||||
appName: ctx.exp.name,
|
||||
appIdentifier: await resolveAppIdentifierAsync(ctx, platformContext),
|
||||
buildProfile: ctx.buildProfileName,
|
||||
gitCommitHash: await vcs.getCommitHashAsync(),
|
||||
gitCommitHash: await getVcsClient().getCommitHashAsync(),
|
||||
isGitWorkingTreeDirty: await getVcsClient().hasUncommittedChangesAsync(),
|
||||
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
|
||||
...(ctx.platform === Platform.IOS && {
|
||||
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as ExpoConfig from '@expo/config';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import assert from 'assert';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
export async function updateAppJsonConfigAsync(
|
||||
{
|
||||
@@ -15,10 +15,19 @@ export async function updateAppJsonConfigAsync(
|
||||
const paths = ExpoConfig.getConfigFilePaths(projectDir);
|
||||
assert(paths.staticConfigPath, "can't update dynamic config");
|
||||
|
||||
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
|
||||
const rawStaticConfig = readAppJson(paths.staticConfigPath);
|
||||
rawStaticConfig.expo = rawStaticConfig.expo ?? {};
|
||||
modifyConfig(rawStaticConfig.expo);
|
||||
await fs.writeJson(paths.staticConfigPath, rawStaticConfig, { spaces: 2 });
|
||||
await JsonFile.writeAsync(paths.staticConfigPath, rawStaticConfig, { json5: false });
|
||||
|
||||
modifyConfig(exp);
|
||||
}
|
||||
|
||||
// TODO: remove this once @expo/config exports getStaticConfig
|
||||
export function readAppJson(appJsonPath: string): any {
|
||||
const config = JsonFile.read(appJsonPath, { json5: true });
|
||||
if (config) {
|
||||
return config as any;
|
||||
}
|
||||
throw new Error(`Failed to read app.json`);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import { error } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
import resolveFrom from 'resolve-from';
|
||||
@@ -9,40 +9,38 @@ import Log, { learnMore } from '../../log';
|
||||
import { appPlatformDisplayNames } from '../../platform';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import { expoCommandAsync } from '../../utils/expoCommand';
|
||||
import zipObject from '../../utils/expodash/zipObject';
|
||||
import { expoCommandAsync } from '../../utils/expoCli';
|
||||
import { ProfileData } from '../../utils/profiles';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { reviewAndCommitChangesAsync } from './repository';
|
||||
|
||||
export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
projectDir,
|
||||
platforms,
|
||||
profile,
|
||||
nonInteractive = false,
|
||||
buildProfiles = [],
|
||||
}: {
|
||||
projectDir: string;
|
||||
platforms: Platform[];
|
||||
profile: string;
|
||||
nonInteractive?: boolean;
|
||||
buildProfiles?: ProfileData<BuildProfile>[];
|
||||
}): Promise<void> {
|
||||
if (await isExpoDevClientInstalledAsync(projectDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const devClientPerPlatformList = await Promise.all(
|
||||
platforms.map(async platform => {
|
||||
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, profile);
|
||||
return buildProfile.developmentClient ?? false;
|
||||
})
|
||||
const buildProfilesWithDevelopmentClientRequired = buildProfiles.filter(
|
||||
buildProfile => buildProfile.profile.developmentClient
|
||||
);
|
||||
|
||||
const isDevClientRequired = devClientPerPlatformList.some(i => i);
|
||||
if (!isDevClientRequired) {
|
||||
const isDevelopmentClientRequired = buildProfilesWithDevelopmentClientRequired.some(Boolean);
|
||||
|
||||
if (!isDevelopmentClientRequired) {
|
||||
return;
|
||||
}
|
||||
|
||||
const devClientPerPlatform = zipObject(platforms, devClientPerPlatformList);
|
||||
const platformsToCheck = platforms.filter(platform => devClientPerPlatform[platform]);
|
||||
const platformsToCheck = buildProfilesWithDevelopmentClientRequired.map(
|
||||
({ platform }) => platform
|
||||
);
|
||||
|
||||
const workflowPerPlatformList = await Promise.all(
|
||||
platformsToCheck.map(platform => resolveWorkflowAsync(projectDir, platform))
|
||||
);
|
||||
@@ -67,15 +65,15 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
if (install) {
|
||||
await installExpoDevClientAsync(projectDir, { nonInteractive });
|
||||
} else {
|
||||
error(`Install ${chalk.bold('expo-dev-client')} on your own and come back later.`, {
|
||||
error(`Install ${chalk.bold('expo-dev-client')} manually and come back later.`, {
|
||||
exit: 1,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Log.warn(`Unfortunately, you need to install ${chalk.bold('expo-dev-client')} on your own.`);
|
||||
Log.warn(`You'll need to install ${chalk.bold('expo-dev-client')} manually.`);
|
||||
Log.warn(
|
||||
learnMore('https://docs.expo.dev/clients/installation/', {
|
||||
learnMoreMessage: 'See installation instructions on how to do it',
|
||||
learnMoreMessage: 'See installation instructions on how to do it.',
|
||||
dim: false,
|
||||
})
|
||||
);
|
||||
@@ -110,7 +108,9 @@ async function installExpoDevClientAsync(
|
||||
Log.newLine();
|
||||
await expoCommandAsync(projectDir, ['install', 'expo-dev-client']);
|
||||
Log.newLine();
|
||||
await reviewAndCommitChangesAsync('Install expo-dev-client', {
|
||||
nonInteractive,
|
||||
});
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
await reviewAndCommitChangesAsync('Install expo-dev-client', {
|
||||
nonInteractive,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -120,11 +120,5 @@ export function formatGraphQLBuild(build: BuildFragment): string {
|
||||
}
|
||||
|
||||
const getActorName = (build: BuildFragment): string => {
|
||||
if (!build.initiatingActor) {
|
||||
return 'unknown';
|
||||
} else if (build.initiatingActor.__typename === 'User') {
|
||||
return build.initiatingActor.username;
|
||||
} else {
|
||||
return build.initiatingActor.firstName ?? 'unknown';
|
||||
}
|
||||
return build.initiatingActor?.displayName || 'unknown';
|
||||
};
|
||||
@@ -24,7 +24,9 @@ export function printLogsUrls(builds: BuildFragment[]): void {
|
||||
builds.forEach(build => {
|
||||
const logsUrl = getBuildLogsUrl(build);
|
||||
Log.log(
|
||||
`${appPlatformDisplayNames[build.platform]} build details: ${chalk.underline(logsUrl)}`
|
||||
`${appPlatformEmojis[build.platform]} ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
} build details: ${chalk.underline(logsUrl)}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { formatBytes } from '../../utils/files';
|
||||
import { getTmpDirectory } from '../../utils/paths';
|
||||
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
|
||||
import vcs from '../../vcs';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
|
||||
export async function maybeBailOnRepoStatusAsync(): Promise<void> {
|
||||
if (!(await vcs.hasUncommittedChangesAsync())) {
|
||||
if (!(await getVcsClient().isCommitRequiredAsync())) {
|
||||
return;
|
||||
}
|
||||
Log.addNewLineIfNone();
|
||||
@@ -33,7 +33,7 @@ export async function maybeBailOnRepoStatusAsync(): Promise<void> {
|
||||
}
|
||||
|
||||
export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<void> {
|
||||
if (!(await vcs.hasUncommittedChangesAsync())) {
|
||||
if (!(await getVcsClient().isCommitRequiredAsync())) {
|
||||
return;
|
||||
}
|
||||
Log.addNewLineIfNone();
|
||||
@@ -70,7 +70,7 @@ export async function commitPromptAsync({
|
||||
initial: initialCommitMessage,
|
||||
validate: (input: string) => input !== '',
|
||||
});
|
||||
await vcs.commitAsync({ commitAllFiles, commitMessage: message });
|
||||
await getVcsClient().commitAsync({ commitAllFiles, commitMessage: message });
|
||||
}
|
||||
|
||||
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
|
||||
@@ -95,7 +95,7 @@ export async function makeProjectTarballAsync(): Promise<{ path: string; size: n
|
||||
startTimer(compressTimerLabel);
|
||||
|
||||
try {
|
||||
await vcs.makeShallowCopyAsync(shallowClonePath);
|
||||
await getVcsClient().makeShallowCopyAsync(shallowClonePath);
|
||||
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
|
||||
'.',
|
||||
]);
|
||||
@@ -133,7 +133,10 @@ export async function reviewAndCommitChangesAsync(
|
||||
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
|
||||
): Promise<void> {
|
||||
if (process.env.EAS_BUILD_AUTOCOMMIT) {
|
||||
await vcs.commitAsync({ commitMessage: initialCommitMessage, commitAllFiles: false });
|
||||
await getVcsClient().commitAsync({
|
||||
commitMessage: initialCommitMessage,
|
||||
commitAllFiles: false,
|
||||
});
|
||||
Log.withTick('Committed changes.');
|
||||
return;
|
||||
}
|
||||
@@ -166,7 +169,7 @@ export async function reviewAndCommitChangesAsync(
|
||||
await commitPromptAsync({ initialCommitMessage });
|
||||
Log.withTick('Committed changes.');
|
||||
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
|
||||
await vcs.showDiffAsync();
|
||||
await getVcsClient().showDiffAsync();
|
||||
await reviewAndCommitChangesAsync(initialCommitMessage, {
|
||||
nonInteractive,
|
||||
askedFirstTime: false,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ExpoConfig, getPackageJson } from '@expo/config';
|
||||
|
||||
export function isExpoUpdatesInstalled(projectDir: string): boolean {
|
||||
const packageJson = getPackageJson(projectDir);
|
||||
return packageJson.dependencies && 'expo-updates' in packageJson.dependencies;
|
||||
return !!(packageJson.dependencies && 'expo-updates' in packageJson.dependencies);
|
||||
}
|
||||
|
||||
export function ensureValidVersions(exp: ExpoConfig): void {
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log, { learnMore } from '../log';
|
||||
import vcs from '../vcs';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { BuildContext } from './context';
|
||||
|
||||
export function checkNodeEnvVariable(ctx: BuildContext<Platform>): void {
|
||||
@@ -25,12 +25,12 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
|
||||
if (!googleServicesFilePath) {
|
||||
return;
|
||||
}
|
||||
const rootDir = path.normalize(await vcs.getRootPathAsync());
|
||||
const rootDir = path.normalize(await getVcsClient().getRootPathAsync());
|
||||
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
|
||||
if (
|
||||
(await fs.pathExists(absGoogleServicesFilePath)) &&
|
||||
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
|
||||
(await vcs.isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
|
||||
(await getVcsClient().isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
|
||||
) {
|
||||
Log.warn(
|
||||
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
|
||||
@@ -41,7 +41,7 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
|
||||
)}`
|
||||
);
|
||||
Log.warn(
|
||||
'If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting GOOGLE_SERVICES_FILE in your build profile in eas.json to any non-falsy value.'
|
||||
`If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting your build profile's env.GOOGLE_SERVICES_FILE in eas.json to any non-empty string.`
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { Command } from '@oclif/command';
|
||||
import semver from 'semver';
|
||||
|
||||
import {
|
||||
AnalyticsEvent,
|
||||
@@ -6,8 +8,13 @@ import {
|
||||
initAsync as initAnalyticsAsync,
|
||||
logEvent,
|
||||
} from '../analytics';
|
||||
import Log from '../log';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { getUserAsync } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { setVcsClient } from '../vcs';
|
||||
import GitClient from '../vcs/clients/git';
|
||||
|
||||
export default abstract class EasCommand extends Command {
|
||||
/**
|
||||
@@ -20,7 +27,10 @@ export default abstract class EasCommand extends Command {
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
async run(): Promise<any> {
|
||||
EasJsonReader.setLog(Log);
|
||||
|
||||
await initAnalyticsAsync();
|
||||
await this.applyCliConfigAsync();
|
||||
|
||||
if (this.requiresAuthentication) {
|
||||
const { flags } = this.parse();
|
||||
@@ -42,4 +52,18 @@ export default abstract class EasCommand extends Command {
|
||||
await flushAnalyticsAsync();
|
||||
return super.finally(err);
|
||||
}
|
||||
|
||||
private async applyCliConfigAsync(): Promise<void> {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const config = await easJsonReader.getCliConfigAsync();
|
||||
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
|
||||
throw new Error(
|
||||
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
|
||||
);
|
||||
}
|
||||
if (config?.requireCommit) {
|
||||
setVcsClient(new GitClient());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import vcs from '../../vcs';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
|
||||
export async function createUpdateBranchOnAppAsync({
|
||||
appId,
|
||||
@@ -75,11 +75,7 @@ export default class BranchCreate extends EasCommand {
|
||||
flags,
|
||||
} = this.parse(BranchCreate);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
@@ -94,7 +90,8 @@ export default class BranchCreate extends EasCommand {
|
||||
name: 'name',
|
||||
message: 'Please name the branch:',
|
||||
initial:
|
||||
(await vcs.getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`,
|
||||
(await getVcsClient().getBranchNameAsync()) ||
|
||||
`branch-${Math.random().toString(36).substr(2, 4)}`,
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ async function getBranchInfoAsync({
|
||||
{
|
||||
appId,
|
||||
name,
|
||||
}
|
||||
},
|
||||
{ additionalTypenames: ['UpdateBranch'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
@@ -98,10 +99,7 @@ export default class BranchDelete extends EasCommand {
|
||||
flags: { json: jsonFlag },
|
||||
} = this.parse(BranchDelete);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
@@ -44,7 +44,8 @@ export async function listBranchesAsync({
|
||||
{
|
||||
appId: projectId,
|
||||
limit: BRANCHES_LIMIT,
|
||||
}
|
||||
},
|
||||
{ additionalTypenames: ['UpdateBranch'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
@@ -67,10 +68,7 @@ export default class BranchList extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = this.parse(BranchList);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const branches = await listBranchesAsync({ projectId });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ExpoConfig, getConfig, getDefaultTarget } from '@expo/config';
|
||||
import { getRuntimeVersionForSDKVersion } from '@expo/sdk-runtime-versions';
|
||||
import { ExpoConfig, getConfig } from '@expo/config';
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { flags } from '@oclif/command';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
@@ -28,11 +29,12 @@ import {
|
||||
collectAssetsAsync,
|
||||
uploadAssetsAsync,
|
||||
} from '../../project/publish';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync, selectAsync } from '../../prompts';
|
||||
import { formatUpdate } from '../../update/utils';
|
||||
import uniqBy from '../../utils/expodash/uniqBy';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import vcs from '../../vcs';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { createUpdateBranchOnAppAsync } from './create';
|
||||
import { listBranchesAsync } from './list';
|
||||
import { viewUpdateBranchAsync } from './view';
|
||||
@@ -60,7 +62,8 @@ async function getUpdateGroupAsync({
|
||||
`,
|
||||
{
|
||||
group,
|
||||
}
|
||||
},
|
||||
{ additionalTypenames: ['Update'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
@@ -162,23 +165,20 @@ export default class BranchPublish extends EasCommand {
|
||||
// If a group was specified, that means we are republishing it.
|
||||
republish = group ? true : republish;
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, {
|
||||
skipSDKVersionRequirement: true,
|
||||
isPublicConfig: true,
|
||||
});
|
||||
|
||||
const runtimeVersions = getRuntimeVersionObject(exp, platformFlag, projectDir);
|
||||
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
|
||||
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!branchName && autoFlag) {
|
||||
branchName =
|
||||
(await vcs.getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`;
|
||||
(await getVcsClient().getBranchNameAsync()) ||
|
||||
`branch-${Math.random().toString(36).substr(2, 4)}`;
|
||||
}
|
||||
|
||||
if (!branchName) {
|
||||
@@ -301,7 +301,7 @@ export default class BranchPublish extends EasCommand {
|
||||
}
|
||||
|
||||
if (!message && autoFlag) {
|
||||
message = (await vcs.getLastCommitMessageAsync())?.trim();
|
||||
message = (await getVcsClient().getLastCommitMessageAsync())?.trim();
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
@@ -315,7 +315,7 @@ export default class BranchPublish extends EasCommand {
|
||||
message: `Please enter a publication message.`,
|
||||
initial: republish
|
||||
? `Republish "${oldMessage!}" - group: ${group}`
|
||||
: (await vcs.getLastCommitMessageAsync())?.trim(),
|
||||
: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
@@ -392,59 +392,26 @@ export default class BranchPublish extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
function getRuntimeVersionObject(
|
||||
async function getRuntimeVersionObjectAsync(
|
||||
exp: ExpoConfig,
|
||||
platformFlag: string,
|
||||
platformFlag: PlatformFlag,
|
||||
projectDir: string
|
||||
): Record<string, string> {
|
||||
let { runtimeVersion: defaultRuntimeVersion, sdkVersion } = exp;
|
||||
): Promise<Record<string, string>> {
|
||||
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
|
||||
|
||||
// When a SDK version is supplied instead of a runtime version and we're in the managed workflow
|
||||
// construct the runtimeVersion with special meaning indicating that the runtime is an
|
||||
// Expo SDK preset runtime that can be launched in Expo Go.
|
||||
const isManagedProject = getDefaultTarget(projectDir) === 'managed';
|
||||
if (!defaultRuntimeVersion && sdkVersion && isManagedProject) {
|
||||
Log.withTick('Generating runtime version from sdk version');
|
||||
defaultRuntimeVersion = getRuntimeVersionForSDKVersion(sdkVersion);
|
||||
}
|
||||
const iOSRuntimeVersion = (exp as any).ios?.runtimeVersion; // TODO-JJ remove cast to any
|
||||
const androidRuntimeVersion = (exp as any).android?.runtimeVersion; // TODO-JJ remove cast to any
|
||||
let runtimeVersions: Record<string, string>;
|
||||
switch (platformFlag) {
|
||||
case 'ios': {
|
||||
runtimeVersions = {
|
||||
ios: iOSRuntimeVersion ?? defaultRuntimeVersion,
|
||||
};
|
||||
break;
|
||||
for (const platform of platforms) {
|
||||
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
|
||||
if (isPolicy) {
|
||||
const isManaged = (await resolveWorkflowAsync(projectDir, platform)) === Workflow.MANAGED;
|
||||
if (!isManaged) {
|
||||
throw new Error('Runtime version policies are only supported in the managed workflow.');
|
||||
}
|
||||
}
|
||||
case 'android': {
|
||||
runtimeVersions = {
|
||||
android: androidRuntimeVersion ?? defaultRuntimeVersion,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'all': {
|
||||
runtimeVersions = {
|
||||
ios: iOSRuntimeVersion ?? defaultRuntimeVersion,
|
||||
android: androidRuntimeVersion ?? defaultRuntimeVersion,
|
||||
};
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error('Platform flag must be "ios", "android", or "all"');
|
||||
}
|
||||
if (Object.values(runtimeVersions).some(runtime => !runtime)) {
|
||||
throw new Error(
|
||||
"Couldn't find a 'runtimeVersion' for every platform. Please specify it under the 'expo' key in 'app.json'"
|
||||
);
|
||||
}
|
||||
if (Object.values(runtimeVersions).some(runtime => typeof runtime !== 'string')) {
|
||||
throw new Error(
|
||||
`Please ensure that all of the runtime versions defined in the app.json are strings.`
|
||||
);
|
||||
}
|
||||
|
||||
return runtimeVersions;
|
||||
return Object.fromEntries(
|
||||
platforms.map(platform => [platform, Updates.getRuntimeVersion(exp, platform)])
|
||||
);
|
||||
}
|
||||
|
||||
function formatUpdateTitle(
|
||||
|
||||
@@ -74,10 +74,7 @@ export default class BranchRename extends EasCommand {
|
||||
flags: { json: jsonFlag, from: currentName, to: newName },
|
||||
} = this.parse(BranchRename);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
@@ -58,7 +58,8 @@ export async function viewUpdateBranchAsync({
|
||||
appId,
|
||||
name,
|
||||
limit: PAGE_LIMIT,
|
||||
}
|
||||
},
|
||||
{ additionalTypenames: ['UpdateBranch', 'Update'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
@@ -90,11 +91,7 @@ export default class BranchView extends EasCommand {
|
||||
flags: { json: jsonFlag },
|
||||
} = this.parse(BranchView);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ export default class BuildCancel extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
const { BUILD_ID: buildIdFromArg } = this.parse(BuildCancel).args;
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectFullName = await getProjectFullNameAsync(exp);
|
||||
|
||||
@@ -31,7 +31,7 @@ export default class BuildConfigure extends EasCommand {
|
||||
|
||||
await configureAsync({
|
||||
platform,
|
||||
projectDir: (await findProjectRootAsync()) ?? process.cwd(),
|
||||
projectDir: await findProjectRootAsync(),
|
||||
});
|
||||
|
||||
Log.newLine();
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import {
|
||||
EasJsonReader,
|
||||
hasMismatchedExtendsAsync,
|
||||
isUsingDeprecatedFormatAsync,
|
||||
migrateAsync,
|
||||
} from '@expo/eas-json';
|
||||
import { BuildProfile, EasJsonReader } from '@expo/eas-json';
|
||||
import { flags } from '@oclif/command';
|
||||
import { error } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
import figures from 'figures';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { prepareAndroidBuildAsync } from '../../build/android/build';
|
||||
@@ -27,7 +24,7 @@ import {
|
||||
SubmissionFragment,
|
||||
} from '../../graphql/generated';
|
||||
import { toAppPlatform, toPlatform } from '../../graphql/types/AppPlatform';
|
||||
import Log from '../../log';
|
||||
import Log, { learnMore, link } from '../../log';
|
||||
import {
|
||||
RequestedPlatform,
|
||||
appPlatformDisplayNames,
|
||||
@@ -41,21 +38,24 @@ import {
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import { selectAsync } from '../../prompts';
|
||||
import { createSubmissionContextAsync } from '../../submit/context';
|
||||
import {
|
||||
submitAsync,
|
||||
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
|
||||
} from '../../submit/submit';
|
||||
import { printSubmissionDetailsUrls } from '../../submit/utils/urls';
|
||||
import { easCliVersion } from '../../utils/easCli';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
import vcs from '../../vcs';
|
||||
import { ProfileData, getProfilesAsync } from '../../utils/profiles';
|
||||
import { getVcsClient, setVcsClient } from '../../vcs';
|
||||
import GitClient from '../../vcs/clients/git';
|
||||
|
||||
interface RawBuildFlags {
|
||||
platform?: string;
|
||||
'skip-credentials-check': boolean;
|
||||
'skip-project-configuration': boolean;
|
||||
profile: string;
|
||||
profile?: string;
|
||||
'non-interactive': boolean;
|
||||
local: boolean;
|
||||
wait: boolean;
|
||||
@@ -68,7 +68,7 @@ interface RawBuildFlags {
|
||||
interface BuildFlags {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
skipProjectConfiguration: boolean;
|
||||
profile: string;
|
||||
profile: string | null;
|
||||
nonInteractive: boolean;
|
||||
local: boolean;
|
||||
wait: boolean;
|
||||
@@ -99,8 +99,8 @@ export default class Build extends EasCommand {
|
||||
description: 'Skip project configuration',
|
||||
}),
|
||||
profile: flags.string({
|
||||
default: 'release',
|
||||
description: 'Name of the build profile from eas.json',
|
||||
description:
|
||||
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
|
||||
helpValue: 'PROFILE_NAME',
|
||||
}),
|
||||
'non-interactive': flags.boolean({
|
||||
@@ -143,53 +143,69 @@ export default class Build extends EasCommand {
|
||||
const flags = await this.sanitizeFlagsAsync(rawFlags);
|
||||
const { requestedPlatform } = flags;
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const projectDir = await findProjectRootAsync();
|
||||
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
|
||||
|
||||
await vcs.ensureRepoExistsAsync();
|
||||
await getVcsClient().ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(flags.nonInteractive);
|
||||
|
||||
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
|
||||
|
||||
const platforms = toPlatforms(requestedPlatform);
|
||||
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const buildProfiles = await getProfilesAsync({
|
||||
platforms,
|
||||
projectDir,
|
||||
profile: flags.profile,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
profileName: flags.profile,
|
||||
async readProfileAsync(platform, profileName) {
|
||||
return await easJsonReader.readBuildProfileAsync(platform, profileName);
|
||||
},
|
||||
});
|
||||
|
||||
const startedBuilds: BuildFragment[] = [];
|
||||
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
projectDir,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
buildProfiles,
|
||||
});
|
||||
|
||||
const startedBuilds: {
|
||||
build: BuildFragment;
|
||||
buildProfile: ProfileData<BuildProfile>;
|
||||
}[] = [];
|
||||
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
|
||||
for (const platform of platforms) {
|
||||
|
||||
for (const buildProfile of buildProfiles) {
|
||||
const { build: maybeBuild, buildCtx } = await this.prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
platform,
|
||||
flags,
|
||||
moreBuilds: platforms.length > 1,
|
||||
buildProfile,
|
||||
});
|
||||
if (maybeBuild) {
|
||||
startedBuilds.push(maybeBuild);
|
||||
startedBuilds.push({ build: maybeBuild, buildProfile });
|
||||
}
|
||||
buildCtxByPlatform[toAppPlatform(platform)] = buildCtx;
|
||||
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
|
||||
}
|
||||
|
||||
if (flags.local) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
printLogsUrls(startedBuilds);
|
||||
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
|
||||
Log.newLine();
|
||||
|
||||
const submissions: SubmissionFragment[] = [];
|
||||
if (flags.autoSubmit) {
|
||||
for (const build of startedBuilds) {
|
||||
for (const startedBuild of startedBuilds) {
|
||||
const submission = await this.prepareAndStartSubmissionAsync({
|
||||
build,
|
||||
credentialsCtx: nullthrows(buildCtxByPlatform[build.platform]?.credentialsCtx),
|
||||
build: startedBuild.build,
|
||||
credentialsCtx: nullthrows(
|
||||
buildCtxByPlatform[startedBuild.build.platform]?.credentialsCtx
|
||||
),
|
||||
flags,
|
||||
moreBuilds: startedBuilds.length > 1,
|
||||
projectDir,
|
||||
buildProfile: startedBuild.buildProfile,
|
||||
});
|
||||
submissions.push(submission);
|
||||
}
|
||||
@@ -203,7 +219,7 @@ export default class Build extends EasCommand {
|
||||
return;
|
||||
}
|
||||
|
||||
const builds = await waitForBuildEndAsync(startedBuilds.map(build => build.id));
|
||||
const builds = await waitForBuildEndAsync(startedBuilds.map(({ build }) => build.id));
|
||||
printBuildResults(builds, flags.json);
|
||||
|
||||
const haveAllBuildsFailedOrCanceled = builds.every(
|
||||
@@ -252,7 +268,7 @@ export default class Build extends EasCommand {
|
||||
return {
|
||||
requestedPlatform,
|
||||
skipProjectConfiguration: flags['skip-project-configuration'],
|
||||
profile,
|
||||
profile: profile ?? null,
|
||||
nonInteractive,
|
||||
local: flags['local'],
|
||||
wait: flags['wait'],
|
||||
@@ -263,34 +279,31 @@ export default class Build extends EasCommand {
|
||||
};
|
||||
}
|
||||
|
||||
private async prepareAndStartBuildAsync<T extends Platform>({
|
||||
private async prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
platform,
|
||||
flags,
|
||||
moreBuilds,
|
||||
buildProfile,
|
||||
}: {
|
||||
projectDir: string;
|
||||
platform: T;
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<T> }> {
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, flags.profile);
|
||||
|
||||
buildProfile: ProfileData<BuildProfile>;
|
||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
|
||||
const buildCtx = await createBuildContextAsync({
|
||||
buildProfileName: flags.profile,
|
||||
buildProfileName: buildProfile.profileName,
|
||||
clearCache: flags.clearCache,
|
||||
buildProfile,
|
||||
buildProfile: buildProfile.profile,
|
||||
local: flags.local,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
platform,
|
||||
platform: buildProfile.platform,
|
||||
projectDir,
|
||||
skipProjectConfiguration: flags.skipProjectConfiguration,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
Log.newLine();
|
||||
const appPlatform = toAppPlatform(platform);
|
||||
const appPlatform = toAppPlatform(buildProfile.platform);
|
||||
Log.log(
|
||||
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
|
||||
`${appPlatformDisplayNames[appPlatform]} build`
|
||||
@@ -330,16 +343,17 @@ export default class Build extends EasCommand {
|
||||
flags,
|
||||
moreBuilds,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
}: {
|
||||
build: BuildFragment;
|
||||
credentialsCtx: CredentialsContext;
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
projectDir: string;
|
||||
buildProfile: ProfileData<BuildProfile>;
|
||||
}): Promise<SubmissionFragment> {
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const platform = toPlatform(build.platform);
|
||||
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, flags.profile);
|
||||
const submitProfile = await easJsonReader.readSubmitProfileAsync(platform, flags.submitProfile);
|
||||
const submissionCtx = await createSubmissionContextAsync({
|
||||
platform,
|
||||
@@ -348,7 +362,7 @@ export default class Build extends EasCommand {
|
||||
profile: submitProfile,
|
||||
archiveFlags: { id: build.id },
|
||||
nonInteractive: flags.nonInteractive,
|
||||
env: buildProfile.env,
|
||||
env: buildProfile.profile.env,
|
||||
credentialsCtx,
|
||||
});
|
||||
|
||||
@@ -378,36 +392,52 @@ export async function handleDeprecatedEasJsonAsync(
|
||||
projectDir: string,
|
||||
nonInteractive: boolean
|
||||
): Promise<void> {
|
||||
if (await isUsingDeprecatedFormatAsync(projectDir)) {
|
||||
const hasMismatchedExtendsKeys = await hasMismatchedExtendsAsync(projectDir);
|
||||
if (nonInteractive) {
|
||||
Log.error('We detected that your eas.json is using a deprecated format.');
|
||||
Log.error(
|
||||
'We will convert it automatically if run this command without --non-interactive flag. Alternatively, you can update eas.json manually according to https://docs.expo.dev/build/eas-json'
|
||||
);
|
||||
error('Unsupported eas.json format', { exit: 1 });
|
||||
}
|
||||
if (!(await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir)))) {
|
||||
return;
|
||||
}
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const rawEasJson = await easJsonReader.readRawAsync();
|
||||
if (rawEasJson?.cli) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message:
|
||||
'We detected that your eas.json is using a deprecated format, do you want to migrate to the new format?',
|
||||
});
|
||||
if (confirm) {
|
||||
await migrateAsync(projectDir);
|
||||
if (hasMismatchedExtendsKeys) {
|
||||
Log.warn(
|
||||
'"extends" keyword can only be migrated automatically to the new format if both Android and iOS profiles extend base profiles with the same names for both platforms'
|
||||
);
|
||||
Log.warn(
|
||||
'Migration was successful, but you need to manually adjust the extend rules for your profiles'
|
||||
);
|
||||
error('Fix eas.json manually', { exit: 1 });
|
||||
}
|
||||
} else {
|
||||
Log.error(
|
||||
"Aborting, update your eas.json according to https://docs.expo.dev/build/eas-json and run 'eas build' again"
|
||||
);
|
||||
error('Unsupported eas.json format', { exit: 1 });
|
||||
}
|
||||
if (nonInteractive) {
|
||||
Log.warn(
|
||||
`${
|
||||
figures.warning
|
||||
} Action required: the default behavior of EAS CLI has changed and your eas.json must be updated to remove ambiguity around which Git integration workflow to use. Refer to ${link(
|
||||
'https://expo.fyi/eas-vcs-workflow'
|
||||
)} for more information.`
|
||||
);
|
||||
Log.warn(
|
||||
'This warning will become an error in an upcoming EAS CLI release. For now, we will proceed with the old default behavior to avoid disruption of your builds.'
|
||||
);
|
||||
setVcsClient(new GitClient());
|
||||
return;
|
||||
}
|
||||
Log.log(
|
||||
`${chalk.bold(
|
||||
'eas-cli@>=0.34.0 no longer requires that you commit changes to Git before starting a build.'
|
||||
)} ${learnMore('https://expo.fyi/eas-vcs-workflow')}`
|
||||
);
|
||||
Log.log(
|
||||
`If you want to continue using the Git integration, you can opt in with ${chalk.bold(
|
||||
'cli.requireCommit'
|
||||
)} in ${chalk.bold('eas.json')} or with the following prompt.`
|
||||
);
|
||||
Log.newLine();
|
||||
|
||||
const mode = await selectAsync('Select your preferred Git integration', [
|
||||
{ title: 'Require changes to be committed in Git (old default)', value: 'requireCommit' },
|
||||
{ title: 'Allow builds with dirty Git working tree (new default)', value: 'noCommit' },
|
||||
]);
|
||||
|
||||
rawEasJson.cli =
|
||||
mode === 'requireCommit'
|
||||
? { version: `>= ${easCliVersion}`, requireCommit: true }
|
||||
: { version: `>= ${easCliVersion}` };
|
||||
await fs.writeJSON(EasJsonReader.formatEasJsonPath(projectDir), rawEasJson, { spaces: 2 });
|
||||
if (mode === 'requireCommit') {
|
||||
setVcsClient(new GitClient());
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ export default class BuildList extends EasCommand {
|
||||
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
|
||||
const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution);
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectName = await getProjectFullNameAsync(exp);
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class BuildView extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectName = await getProjectFullNameAsync(exp);
|
||||
|
||||
@@ -84,10 +84,7 @@ export default class ChannelCreate extends EasCommand {
|
||||
flags: { json: jsonFlag },
|
||||
} = this.parse(ChannelCreate);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
|
||||
@@ -49,7 +49,8 @@ async function getChannelByNameForAppAsync({
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appId, channelName }
|
||||
{ appId, channelName },
|
||||
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
@@ -121,10 +122,7 @@ export default class ChannelEdit extends EasCommand {
|
||||
flags: { branch: branchName, json: jsonFlag },
|
||||
} = this.parse(ChannelEdit);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
|
||||
@@ -58,7 +58,8 @@ async function getAllUpdateChannelForAppAsync({
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appId, offset: 0, limit: CHANNEL_LIMIT }
|
||||
{ appId, offset: 0, limit: CHANNEL_LIMIT },
|
||||
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
@@ -80,10 +81,7 @@ export default class ChannelList extends EasCommand {
|
||||
flags: { json: jsonFlag },
|
||||
} = this.parse(ChannelList);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
|
||||
@@ -346,10 +346,7 @@ export default class ChannelRollout extends EasCommand {
|
||||
flags: { branch: branchName, percent },
|
||||
} = this.parse(ChannelRollout);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
@@ -133,7 +133,8 @@ export async function getUpdateChannelByNameForAppAsync({
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appId, channelName }
|
||||
{ appId, channelName },
|
||||
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
@@ -215,10 +216,7 @@ export default class ChannelView extends EasCommand {
|
||||
flags: { json: jsonFlag },
|
||||
} = this.parse(ChannelView);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class Config extends EasCommand {
|
||||
profile?: string;
|
||||
};
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const projectDir = await findProjectRootAsync();
|
||||
await handleDeprecatedEasJsonAsync(projectDir, false);
|
||||
|
||||
const reader = new EasJsonReader(projectDir);
|
||||
|
||||
@@ -22,7 +22,7 @@ export default class BuildList extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
let appleTeamIdentifier = this.parse(BuildList).flags['apple-team-id'];
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ If you are not sure what is the UDID of the device you are looking for, run:
|
||||
throw new Error('Device UDID is missing');
|
||||
}
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@ import envinfo from 'envinfo';
|
||||
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import Log from '../log';
|
||||
|
||||
const packageJSON = require('../../package.json');
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
|
||||
export default class Diagnostics extends EasCommand {
|
||||
static description = 'log environment info to the console';
|
||||
@@ -30,7 +29,7 @@ export default class Diagnostics extends EasCommand {
|
||||
npmGlobalPackages: ['eas-cli', 'expo-cli'],
|
||||
},
|
||||
{
|
||||
title: `EAS CLI ${packageJSON.version} environment info`,
|
||||
title: `EAS CLI ${easCliVersion} environment info`,
|
||||
}
|
||||
);
|
||||
Log.log(info);
|
||||
|
||||
@@ -22,7 +22,8 @@ async function projectInfoByIdAsync(appId: string): Promise<AppInfoQuery> {
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appId }
|
||||
{ appId },
|
||||
{ additionalTypenames: ['App'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
@@ -34,10 +35,7 @@ export default class ProjectInfo extends EasCommand {
|
||||
static description = 'information about the current project';
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const { app } = await projectInfoByIdAsync(projectId);
|
||||
|
||||
@@ -10,10 +10,7 @@ export default class ProjectInit extends EasCommand {
|
||||
static aliases = ['init'];
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
if (exp.extra?.eas?.projectId) {
|
||||
|
||||
@@ -50,7 +50,7 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
flags: { name, value: secretValue, scope, force },
|
||||
} = this.parse(EnvironmentSecretCreate);
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
|
||||
export default class EnvironmentSecretDelete extends EasCommand {
|
||||
static description = `Delete an environment secret by ID.
|
||||
Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
Unsure where to find the secret's ID? Run ${chalk.bold('eas secret:list')}`;
|
||||
|
||||
static flags = {
|
||||
id: flags.string({
|
||||
@@ -32,7 +32,7 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectAccountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
@@ -20,7 +20,7 @@ export default class EnvironmentSecretList extends EasCommand {
|
||||
static description = 'Lists environment secrets available for your current app';
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectAccountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
@@ -7,7 +7,7 @@ import chalk from 'chalk';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { SubmissionFragment } from '../graphql/generated';
|
||||
import { toAppPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log, { learnMore } from '../log';
|
||||
import Log, { link } from '../log';
|
||||
import {
|
||||
RequestedPlatform,
|
||||
appPlatformDisplayNames,
|
||||
@@ -20,6 +20,7 @@ import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils
|
||||
import { SubmitArchiveFlags, createSubmissionContextAsync } from '../submit/context';
|
||||
import { submitAsync, waitToCompleteAsync } from '../submit/submit';
|
||||
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
|
||||
import { getProfilesAsync } from '../utils/profiles';
|
||||
|
||||
interface RawFlags {
|
||||
platform?: string;
|
||||
@@ -44,9 +45,7 @@ interface Flags {
|
||||
|
||||
export default class Submit extends EasCommand {
|
||||
static description = `submit build archive to app store
|
||||
See how to configure submits with eas.json: ${learnMore('https://docs.expo.dev/submit/eas-json/', {
|
||||
learnMoreMessage: '',
|
||||
})}`;
|
||||
See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit/eas-json/')}`;
|
||||
static aliases = ['build:submit'];
|
||||
|
||||
static flags = {
|
||||
@@ -56,7 +55,7 @@ See how to configure submits with eas.json: ${learnMore('https://docs.expo.dev/s
|
||||
}),
|
||||
profile: flags.string({
|
||||
description:
|
||||
'Name of the submit profile from eas.json. Defaults to "release" if defined in eas.json.',
|
||||
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
|
||||
}),
|
||||
latest: flags.boolean({
|
||||
description: 'Submit the latest build for specified platform',
|
||||
@@ -93,7 +92,7 @@ See how to configure submits with eas.json: ${learnMore('https://docs.expo.dev/s
|
||||
const { flags: rawFlags } = this.parse(Submit);
|
||||
const flags = await this.sanitizeFlagsAsync(rawFlags);
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
@@ -103,23 +102,30 @@ See how to configure submits with eas.json: ${learnMore('https://docs.expo.dev/s
|
||||
return;
|
||||
}
|
||||
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const platforms = toPlatforms(flags.requestedPlatform);
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const submissionProfiles = await getProfilesAsync({
|
||||
platforms,
|
||||
profileName: flags.profile,
|
||||
async readProfileAsync(platform, profileName) {
|
||||
return await easJsonReader.readSubmitProfileAsync(platform, profileName);
|
||||
},
|
||||
});
|
||||
|
||||
const submissions: SubmissionFragment[] = [];
|
||||
for (const platform of platforms) {
|
||||
const profile = await easJsonReader.readSubmitProfileAsync(platform, flags.profile);
|
||||
for (const submissionProfile of submissionProfiles) {
|
||||
const ctx = await createSubmissionContextAsync({
|
||||
platform,
|
||||
platform: submissionProfile.platform,
|
||||
projectDir,
|
||||
projectId,
|
||||
profile,
|
||||
profile: submissionProfile.profile,
|
||||
archiveFlags: flags.archiveFlags,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
|
||||
if (platforms.length > 1) {
|
||||
if (submissionProfiles.length > 1) {
|
||||
Log.newLine();
|
||||
const appPlatform = toAppPlatform(platform);
|
||||
const appPlatform = toAppPlatform(submissionProfile.platform);
|
||||
Log.log(
|
||||
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
|
||||
`${appPlatformDisplayNames[appPlatform]} submission`
|
||||
|
||||
@@ -39,7 +39,8 @@ export async function viewUpdateAsync({
|
||||
`,
|
||||
{
|
||||
groupId,
|
||||
}
|
||||
},
|
||||
{ additionalTypenames: ['Update'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
@@ -30,10 +30,7 @@ export default class WebhookCreate extends EasCommand {
|
||||
const { flags } = this.parse(WebhookCreate);
|
||||
const webhookInputParams = await prepareInputParamsAsync(flags);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
|
||||
@@ -29,10 +29,7 @@ export default class WebhookDelete extends EasCommand {
|
||||
args: { ID: webhookId },
|
||||
} = this.parse(WebhookDelete);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
|
||||
@@ -29,10 +29,7 @@ export default class WebhookList extends EasCommand {
|
||||
flags: { event },
|
||||
} = this.parse(WebhookList);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectFullName = await getProjectFullNameAsync(exp);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AuthCtx } from '../ios/appstore/authenticate';
|
||||
export const testAuthCtx: AuthCtx = {
|
||||
appleId: 'test-apple-id',
|
||||
appleIdPassword: 'test-apple-password',
|
||||
team: { id: 'test-team-id', name: 'test-team-name', inHouse: false },
|
||||
team: { id: 'test-apple-team-identifier', name: 'test-team-name', inHouse: false },
|
||||
};
|
||||
|
||||
export function getAppstoreMock(): AppStoreApi {
|
||||
@@ -22,5 +22,9 @@ export function getAppstoreMock(): AppStoreApi {
|
||||
createProvisioningProfileAsync: jest.fn(),
|
||||
revokeProvisioningProfileAsync: jest.fn(),
|
||||
createOrReuseAdhocProvisioningProfileAsync: jest.fn(),
|
||||
createAscApiKeyAsync: jest.fn(),
|
||||
getAscApiKeyAsync: jest.fn(),
|
||||
revokeAscApiKeyAsync: jest.fn(),
|
||||
listAscApiKeysAsync: jest.fn(),
|
||||
} as any;
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import { UserRole } from '@expo/apple-utils';
|
||||
|
||||
import {
|
||||
AppFragment,
|
||||
AppStoreConnectApiKeyFragment,
|
||||
AppStoreConnectUserRole,
|
||||
AppleAppIdentifierFragment,
|
||||
AppleDistributionCertificateFragment,
|
||||
AppleProvisioningProfileFragment,
|
||||
@@ -10,12 +14,27 @@ import {
|
||||
IosDistributionType,
|
||||
} from '../../graphql/generated';
|
||||
import * as IosGraphqlClient from '../ios/api/GraphqlClient';
|
||||
import { DistributionCertificate, ProvisioningProfile } from '../ios/appstore/Credentials.types';
|
||||
import {
|
||||
AscApiKey,
|
||||
DistributionCertificate,
|
||||
ProvisioningProfile,
|
||||
} from '../ios/appstore/Credentials.types';
|
||||
import { Target } from '../ios/types';
|
||||
import { testProvisioningProfileBase64 } from './fixtures-base64-data';
|
||||
|
||||
const now = new Date();
|
||||
|
||||
export const testAscApiKey: AscApiKey = {
|
||||
keyId: 'test-keyIdentifier',
|
||||
issuerId: 'test-issuer-id-from-apple',
|
||||
teamId: 'test-team-id',
|
||||
teamName: 'test-team-name',
|
||||
name: 'test-name',
|
||||
roles: [UserRole.ADMIN],
|
||||
isRevoked: false,
|
||||
keyP8: 'test-key-p8',
|
||||
};
|
||||
|
||||
export const testProvisioningProfile: ProvisioningProfile = {
|
||||
provisioningProfileId: 'test-id',
|
||||
provisioningProfile: testProvisioningProfileBase64,
|
||||
@@ -38,6 +57,17 @@ export const testAppFragment: AppFragment = {
|
||||
slug: 'testapp',
|
||||
};
|
||||
|
||||
export const testAscApiKeyFragment: AppStoreConnectApiKeyFragment = {
|
||||
id: 'test-asc-api-key-id',
|
||||
appleTeam: { ...testAppleTeamFragment },
|
||||
issuerIdentifier: 'test-issuerIdentifier',
|
||||
keyIdentifier: 'test-keyIdentifier',
|
||||
name: 'test-name',
|
||||
roles: [AppStoreConnectUserRole.Admin],
|
||||
updatedAt: now,
|
||||
createdAt: now,
|
||||
};
|
||||
|
||||
export const testPushKey: ApplePushKeyFragment = {
|
||||
id: 'test-push-key-id',
|
||||
keyIdentifier: 'test-key-identifier',
|
||||
@@ -112,6 +142,7 @@ export const testCommonIosAppCredentialsFragment: CommonIosAppCredentialsFragmen
|
||||
appleAppIdentifier: testAppleAppIdentifierFragment,
|
||||
pushKey: testPushKey,
|
||||
iosAppBuildCredentialsList: [testIosAppBuildCredentialsFragment],
|
||||
appStoreConnectApiKeyForSubmissions: testAscApiKeyFragment,
|
||||
};
|
||||
|
||||
export const testAppleTeam = {
|
||||
@@ -139,6 +170,9 @@ export function getNewIosApiMock(): { [key in keyof typeof IosGraphqlClient]?: a
|
||||
createPushKeyAsync: jest.fn(),
|
||||
getPushKeyForAppAsync: jest.fn(),
|
||||
deletePushKeyAsync: jest.fn(),
|
||||
createAscApiKeyAsync: jest.fn(),
|
||||
deleteAscApiKeyAsync: jest.fn(),
|
||||
getAscApiKeysForAccountAsync: jest.fn(() => []),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,6 @@ 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.');
|
||||
@@ -31,6 +25,11 @@ export class SetupGoogleServiceAccountKey {
|
||||
'androidAppCredentials cannot be null if google service account key is already set up'
|
||||
);
|
||||
}
|
||||
if (ctx.nonInteractive) {
|
||||
throw new MissingCredentialsNonInteractiveError(
|
||||
'Google Service Account Keys cannot be set up in --non-interactive mode.'
|
||||
);
|
||||
}
|
||||
|
||||
const keysForAccount = await ctx.android.getGoogleServiceAccountKeysForAccountAsync(
|
||||
this.app.account
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ describe(CreateGoogleServiceAccountKey, () => {
|
||||
'/google-service-account-key.json': JSON.stringify({
|
||||
type: 'service_account',
|
||||
private_key: 'super secret',
|
||||
client_email: '[email protected]',
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
+1
@@ -45,6 +45,7 @@ describe(SetupGoogleServiceAccountKey, () => {
|
||||
'/google-service-account-key.json': JSON.stringify({
|
||||
type: 'service_account',
|
||||
private_key: 'super secret',
|
||||
client_email: '[email protected]',
|
||||
}),
|
||||
});
|
||||
const ctx = createCtxMock({
|
||||
|
||||
+3
@@ -29,6 +29,9 @@ export const GoogleServiceAccountKeyQuery = {
|
||||
`,
|
||||
{
|
||||
accountName,
|
||||
},
|
||||
{
|
||||
additionalTypenames: ['GoogleServiceAccountKey'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
|
||||
@@ -19,6 +19,8 @@ export type KeystoreWithType = Keystore & {
|
||||
export type GoogleServiceAccountKey = {
|
||||
[key: string]: any;
|
||||
private_key: string;
|
||||
type: string;
|
||||
client_email: string;
|
||||
};
|
||||
|
||||
export type AndroidCredentials = {
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ beforeAll(() => {
|
||||
const mockDetectableServiceAccountJson = JSON.stringify({
|
||||
type: 'service_account',
|
||||
private_key: 'super secret',
|
||||
client_email: '[email protected]',
|
||||
});
|
||||
|
||||
vol.fromJSON({
|
||||
|
||||
@@ -13,6 +13,7 @@ import { GoogleServiceAccountKey } from '../credentials';
|
||||
export const MinimalGoogleServiceAccountKeySchema = Joi.object({
|
||||
type: Joi.string().required(),
|
||||
private_key: Joi.string().required(),
|
||||
client_email: Joi.string().required(),
|
||||
});
|
||||
|
||||
function fileIsServiceAccountKey(keyJsonPath: string): boolean {
|
||||
|
||||
@@ -19,7 +19,7 @@ export class CredentialsContext {
|
||||
public readonly user: Actor;
|
||||
|
||||
private shouldAskAuthenticateAppStore: boolean = true;
|
||||
private _exp?: ExpoConfig;
|
||||
private resolvedExp?: ExpoConfig;
|
||||
|
||||
constructor(
|
||||
private options: {
|
||||
@@ -33,10 +33,10 @@ export class CredentialsContext {
|
||||
this.user = options.user;
|
||||
this.nonInteractive = options.nonInteractive ?? false;
|
||||
|
||||
this._exp = options.exp;
|
||||
if (!this._exp) {
|
||||
this.resolvedExp = options.exp;
|
||||
if (!this.resolvedExp) {
|
||||
try {
|
||||
this._exp = getExpoConfig(options.projectDir);
|
||||
this.resolvedExp = getExpoConfig(options.projectDir);
|
||||
} catch (error) {
|
||||
// ignore error, context might be created outside of expo project
|
||||
}
|
||||
@@ -44,12 +44,12 @@ export class CredentialsContext {
|
||||
}
|
||||
|
||||
get hasProjectContext(): boolean {
|
||||
return !!this._exp;
|
||||
return !!this.resolvedExp;
|
||||
}
|
||||
|
||||
get exp(): ExpoConfig {
|
||||
this.ensureProjectContext();
|
||||
return this._exp!;
|
||||
return this.resolvedExp!;
|
||||
}
|
||||
|
||||
public ensureProjectContext(): void {
|
||||
|
||||
@@ -6,8 +6,8 @@ import { AndroidAppBuildCredentialsFragment, IosDistributionType } from '../../g
|
||||
import Log from '../../log';
|
||||
import { findApplicationTarget, findTargetByName } from '../../project/ios/target';
|
||||
import zipObject from '../../utils/expodash/zipObject';
|
||||
import vcs from '../../vcs';
|
||||
import GitClient from '../../vcs/git';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import GitClient from '../../vcs/clients/git';
|
||||
import { CredentialsContext } from '../context';
|
||||
import { App, Target, TargetCredentials } from '../ios/types';
|
||||
import { readRawAsync } from './read';
|
||||
@@ -294,8 +294,9 @@ async function updateFileAsync(
|
||||
}
|
||||
|
||||
async function isFileUntrackedAsync(path: string): Promise<boolean> {
|
||||
if (vcs instanceof GitClient) {
|
||||
return await vcs.isFileUntrackedAsync(path);
|
||||
const vcsClient = getVcsClient();
|
||||
if (vcsClient instanceof GitClient) {
|
||||
return await vcsClient.isFileUntrackedAsync(path);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5,3 +5,15 @@ export class MissingCredentialsNonInteractiveError extends Error {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class MissingCredentialsError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message ?? 'Credentials are not set up.');
|
||||
}
|
||||
}
|
||||
|
||||
export class UnsupportedCredentialsChoiceError extends Error {
|
||||
constructor(message: string, public choice: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,7 @@ export default class IosCredentialsProvider {
|
||||
}
|
||||
|
||||
const confirmSetup = await confirmAsync({
|
||||
message: `Would you like to setup Push Notifications for your project?`,
|
||||
message: `Would you like to set up Push Notifications for your project?`,
|
||||
});
|
||||
if (!confirmSetup) {
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import path from 'path';
|
||||
|
||||
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
|
||||
import Log, { learnMore } from '../../../log';
|
||||
import { confirmAsync, promptAsync } from '../../../prompts';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { fromNow } from '../../../utils/date';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import {
|
||||
getCredentialsFromUserAsync,
|
||||
shouldAutoGenerateCredentialsAsync,
|
||||
} from '../../utils/promptForCredentials';
|
||||
import { AscApiKey } from '../appstore/Credentials.types';
|
||||
import {
|
||||
AscApiKeyPath,
|
||||
MinimalAscApiKey,
|
||||
ascApiKeyIdSchema,
|
||||
ascApiKeyIssuerIdSchema,
|
||||
} from '../credentials';
|
||||
import { isAscApiKeyValidAndTrackedAsync } from '../validators/validateAscApiKey';
|
||||
import { formatAppleTeam } from './AppleTeamUtils';
|
||||
|
||||
export enum AppStoreApiKeyPurpose {
|
||||
SUBMISSION_SERVICE = 'EAS Submit',
|
||||
}
|
||||
|
||||
export async function promptForAscApiKeyPathAsync(ctx: CredentialsContext): Promise<AscApiKeyPath> {
|
||||
const { keyId, keyP8Path } = await promptForKeyP8AndIdAsync();
|
||||
|
||||
const bestEffortIssuerId = await getBestEffortIssuerIdAsync(ctx, keyId);
|
||||
if (bestEffortIssuerId) {
|
||||
Log.log(`Detected Issuer ID: ${bestEffortIssuerId}`);
|
||||
return { keyId, issuerId: bestEffortIssuerId, keyP8Path };
|
||||
}
|
||||
const issuerId = await promptForIssuerIdAsync();
|
||||
return { keyId, issuerId, keyP8Path };
|
||||
}
|
||||
|
||||
export async function promptForIssuerIdAsync(): Promise<string> {
|
||||
Log.log(chalk.bold('An App Store Connect Issuer ID is required'));
|
||||
Log.log(
|
||||
`If you're not sure what this is or how to find yours, ${learnMore(
|
||||
'https://expo.fyi/asc-issuer-id'
|
||||
)}`
|
||||
);
|
||||
|
||||
// Do not perform uuid validation - Apple's issuerIds are not RFC4122 compliant
|
||||
const { issuerId } = await getCredentialsFromUserAsync(ascApiKeyIssuerIdSchema);
|
||||
return issuerId;
|
||||
}
|
||||
|
||||
export async function getMinimalAscApiKeyAsync(ascApiKey: AscApiKey): Promise<MinimalAscApiKey> {
|
||||
return {
|
||||
...ascApiKey,
|
||||
issuerId: ascApiKey.issuerId ?? (await promptForIssuerIdAsync()),
|
||||
};
|
||||
}
|
||||
|
||||
export async function provideOrGenerateAscApiKeyAsync(
|
||||
ctx: CredentialsContext,
|
||||
purpose: AppStoreApiKeyPurpose
|
||||
): Promise<MinimalAscApiKey> {
|
||||
if (ctx.nonInteractive) {
|
||||
return await generateAscApiKeyAsync(ctx, purpose);
|
||||
}
|
||||
|
||||
const userProvided = await promptForAscApiKeyAsync(ctx);
|
||||
if (!userProvided) {
|
||||
return await generateAscApiKeyAsync(ctx, purpose);
|
||||
}
|
||||
|
||||
if (!ctx.appStore.authCtx) {
|
||||
Log.warn('Unable to validate App Store Connect Api Key, you are not authenticated with Apple.');
|
||||
return userProvided;
|
||||
}
|
||||
|
||||
const isValidAndTracked = await isAscApiKeyValidAndTrackedAsync(ctx, userProvided);
|
||||
if (isValidAndTracked) {
|
||||
return userProvided;
|
||||
}
|
||||
const useUserProvided = await confirmAsync({
|
||||
message: `App Store Connect Api Key with ID ${userProvided.keyId} is not valid on Apple's servers. Proceed anyway?`,
|
||||
});
|
||||
if (useUserProvided) {
|
||||
return userProvided;
|
||||
}
|
||||
return await provideOrGenerateAscApiKeyAsync(ctx, purpose);
|
||||
}
|
||||
|
||||
async function generateAscApiKeyAsync(
|
||||
ctx: CredentialsContext,
|
||||
purpose: AppStoreApiKeyPurpose
|
||||
): Promise<MinimalAscApiKey> {
|
||||
await ctx.appStore.ensureAuthenticatedAsync();
|
||||
const ascApiKey = await ctx.appStore.createAscApiKeyAsync({
|
||||
nickname: getAscApiKeyName(purpose),
|
||||
});
|
||||
return await getMinimalAscApiKeyAsync(ascApiKey);
|
||||
}
|
||||
|
||||
export function getAscApiKeyName(purpose: AppStoreApiKeyPurpose): string {
|
||||
const nameParts = ['[Expo]', purpose, nanoid(10)];
|
||||
return nameParts.join(' ');
|
||||
}
|
||||
|
||||
async function promptForAscApiKeyAsync(ctx: CredentialsContext): Promise<MinimalAscApiKey | null> {
|
||||
const shouldAutoGenerateCredentials = await shouldAutoGenerateCredentialsAsync(ascApiKeyIdSchema);
|
||||
if (shouldAutoGenerateCredentials) {
|
||||
return null;
|
||||
}
|
||||
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
|
||||
const { keyP8Path, keyId, issuerId } = ascApiKeyPath;
|
||||
return { keyP8: await fs.readFile(keyP8Path, 'utf-8'), keyId, issuerId };
|
||||
}
|
||||
|
||||
async function promptForKeyP8AndIdAsync(): Promise<Pick<AscApiKeyPath, 'keyP8Path' | 'keyId'>> {
|
||||
Log.log(
|
||||
chalk.bold('An App Store Connect Api key is required to upload your app to the Apple App Store')
|
||||
);
|
||||
Log.log(
|
||||
`If you're not sure what this is or how to create one, ${learnMore(
|
||||
'https://expo.fyi/creating-asc-api-key'
|
||||
)}`
|
||||
);
|
||||
|
||||
const { keyP8Path } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'keyP8Path',
|
||||
message: 'Path to App Store Connect Api Key:',
|
||||
initial: 'AuthKey_ABCD.p8',
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
validate: async (filePath: string) => {
|
||||
try {
|
||||
const stats = await fs.stat(filePath);
|
||||
if (stats.isFile()) {
|
||||
return true;
|
||||
}
|
||||
return 'Input is not a file.';
|
||||
} catch {
|
||||
return 'File does not exist.';
|
||||
}
|
||||
},
|
||||
});
|
||||
const regex = /^AuthKey_(?<keyId>\w+)\.p8$/; // Common ASC Api file name downloaded from Apple
|
||||
const bestEffortKeyId = path.basename(keyP8Path).match(regex)?.groups?.keyId;
|
||||
const { keyId } = await getCredentialsFromUserAsync(ascApiKeyIdSchema, {
|
||||
keyId: bestEffortKeyId,
|
||||
});
|
||||
return { keyId, keyP8Path };
|
||||
}
|
||||
|
||||
async function getBestEffortIssuerIdAsync(
|
||||
ctx: CredentialsContext,
|
||||
ascApiKeyId: string
|
||||
): Promise<string | null> {
|
||||
if (!ctx.appStore.authCtx) {
|
||||
return null;
|
||||
}
|
||||
const ascApiKeyInfo = await ctx.appStore.getAscApiKeyAsync(ascApiKeyId);
|
||||
return ascApiKeyInfo?.issuerId ?? null;
|
||||
}
|
||||
|
||||
export async function getAscApiKeysFromAccountAsync(
|
||||
ctx: CredentialsContext,
|
||||
account: Account,
|
||||
{ filterDifferentAppleTeam }: { filterDifferentAppleTeam?: boolean } = {}
|
||||
): Promise<AppStoreConnectApiKeyFragment[]> {
|
||||
const ascApiKeysForAccount = await ctx.ios.getAscApiKeysForAccountAsync(account);
|
||||
|
||||
if (!filterDifferentAppleTeam) {
|
||||
return ascApiKeysForAccount;
|
||||
} else {
|
||||
return filterKeysFromDifferentAppleTeam(ctx, ascApiKeysForAccount);
|
||||
}
|
||||
}
|
||||
|
||||
export async function selectAscApiKeysFromAccountAsync(
|
||||
ctx: CredentialsContext,
|
||||
account: Account,
|
||||
{ filterDifferentAppleTeam }: { filterDifferentAppleTeam?: boolean } = {}
|
||||
): Promise<AppStoreConnectApiKeyFragment | null> {
|
||||
const ascApiKeysForAccount = await getAscApiKeysFromAccountAsync(ctx, account, {
|
||||
filterDifferentAppleTeam,
|
||||
});
|
||||
if (ascApiKeysForAccount.length === 0) {
|
||||
if (filterDifferentAppleTeam) {
|
||||
const maybeAppleTeamId = ctx.appStore.authCtx?.team.id;
|
||||
Log.warn(
|
||||
`There are no App Store Connect Api Keys in your EAS account${
|
||||
maybeAppleTeamId ? ` matching Apple Team ID: ${maybeAppleTeamId}.` : '.'
|
||||
}`
|
||||
);
|
||||
} else {
|
||||
Log.warn(`There are no App Store Connect Api Keys available in your EAS account.`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return selectAscApiKeysAsync(ascApiKeysForAccount);
|
||||
}
|
||||
|
||||
async function selectAscApiKeysAsync(
|
||||
ascApiKeys: AppStoreConnectApiKeyFragment[]
|
||||
): Promise<AppStoreConnectApiKeyFragment | null> {
|
||||
const sortedAscApiKeys = sortAscApiKeysByUpdatedAtDesc(ascApiKeys);
|
||||
const { chosenAscApiKey } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'chosenAscApiKey',
|
||||
message: 'Select an Api Key from the list:',
|
||||
choices: sortedAscApiKeys.map(ascApiKey => ({
|
||||
title: formatAscApiKey(ascApiKey),
|
||||
value: ascApiKey,
|
||||
})),
|
||||
});
|
||||
return chosenAscApiKey;
|
||||
}
|
||||
|
||||
function filterKeysFromDifferentAppleTeam(
|
||||
ctx: CredentialsContext,
|
||||
keys: AppStoreConnectApiKeyFragment[]
|
||||
): AppStoreConnectApiKeyFragment[] {
|
||||
if (!ctx.appStore.authCtx) {
|
||||
return keys;
|
||||
}
|
||||
const teamId = ctx.appStore.authCtx.team.id;
|
||||
return keys.filter(key => !key.appleTeam || key.appleTeam?.appleTeamIdentifier === teamId);
|
||||
}
|
||||
|
||||
export function sortAscApiKeysByUpdatedAtDesc(
|
||||
keys: AppStoreConnectApiKeyFragment[]
|
||||
): AppStoreConnectApiKeyFragment[] {
|
||||
return keys.sort(
|
||||
(keyA, keyB) => new Date(keyB.updatedAt).getTime() - new Date(keyA.updatedAt).getTime()
|
||||
);
|
||||
}
|
||||
|
||||
export function formatAscApiKey(ascApiKey: AppStoreConnectApiKeyFragment): string {
|
||||
const { keyIdentifier, appleTeam, name, updatedAt } = ascApiKey;
|
||||
let line: string = '';
|
||||
line += `Key ID: ${keyIdentifier}`;
|
||||
|
||||
if (name) {
|
||||
line += chalk.gray(`\n Name: ${name}`);
|
||||
}
|
||||
if (appleTeam) {
|
||||
line += chalk.gray(`\n ${formatAppleTeam(appleTeam)}`);
|
||||
}
|
||||
|
||||
line += chalk.gray(`\n Updated: ${fromNow(new Date(updatedAt))} ago`);
|
||||
return line;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
AppStoreConnectApiKeyFragment,
|
||||
CommonIosAppCredentialsFragment,
|
||||
} from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
|
||||
import { AppStoreApiKeyPurpose } from './AscApiKeyUtils';
|
||||
|
||||
export class AssignAscApiKey {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
public async runAsync(
|
||||
ctx: CredentialsContext,
|
||||
ascApiKey: AppStoreConnectApiKeyFragment,
|
||||
purpose: AppStoreApiKeyPurpose
|
||||
): Promise<CommonIosAppCredentialsFragment> {
|
||||
const appleTeam =
|
||||
(await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app)) ?? ascApiKey.appleTeam ?? null;
|
||||
const appCredentials = await ctx.ios.createOrGetIosAppCredentialsWithCommonFieldsAsync(
|
||||
this.app,
|
||||
{ appleTeam: appleTeam ?? undefined }
|
||||
);
|
||||
let updatedAppCredentials;
|
||||
if (purpose === AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
|
||||
updatedAppCredentials = await ctx.ios.updateIosAppCredentialsAsync(appCredentials, {
|
||||
ascApiKeyIdForSubmissions: ascApiKey.id,
|
||||
});
|
||||
} else {
|
||||
throw new Error(`${purpose} is not yet supported.`);
|
||||
}
|
||||
Log.succeed(
|
||||
`App Store Connect Api Key assigned to ${this.app.projectName}: ${this.app.bundleIdentifier} for ${purpose}.`
|
||||
);
|
||||
return updatedAppCredentials;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { AppStoreApiKeyPurpose, provideOrGenerateAscApiKeyAsync } from './AscApiKeyUtils';
|
||||
|
||||
export class CreateAscApiKey {
|
||||
constructor(private account: Account) {}
|
||||
|
||||
public async runAsync(
|
||||
ctx: CredentialsContext,
|
||||
purpose: AppStoreApiKeyPurpose
|
||||
): Promise<AppStoreConnectApiKeyFragment> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(`A new App Store Connect Api Key cannot be created in non-interactive mode.`);
|
||||
}
|
||||
|
||||
const ascApiKey = await provideOrGenerateAscApiKeyAsync(ctx, purpose);
|
||||
const result = await ctx.ios.createAscApiKeyAsync(this.account, ascApiKey);
|
||||
Log.succeed('Created App Store Connect Api Key');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export class CreateDistributionCertificate {
|
||||
public async runAsync(
|
||||
ctx: CredentialsContext
|
||||
): Promise<AppleDistributionCertificateMutationResult> {
|
||||
const distCert = await provideOrGenerateDistributionCertificateAsync(ctx, this.account.name);
|
||||
const distCert = await provideOrGenerateDistributionCertificateAsync(ctx);
|
||||
const result = await ctx.ios.createDistributionCertificateAsync(this.account, distCert);
|
||||
Log.succeed('Created distribution certificate');
|
||||
return result;
|
||||
|
||||
@@ -12,7 +12,7 @@ export class CreatePushKey {
|
||||
throw new Error(`A new push key cannot be created in non-interactive mode.`);
|
||||
}
|
||||
|
||||
const pushKey = await provideOrGeneratePushKeyAsync(ctx, this.account.name);
|
||||
const pushKey = await provideOrGeneratePushKeyAsync(ctx);
|
||||
const result = await ctx.ios.createPushKeyAsync(this.account, pushKey);
|
||||
Log.succeed('Created push key');
|
||||
return result;
|
||||
|
||||
@@ -61,7 +61,7 @@ export function formatDistributionCertificate(
|
||||
return line;
|
||||
}
|
||||
|
||||
async function _selectDistributionCertificateAsync(
|
||||
async function selectDistributionCertificateAsync(
|
||||
distCerts: AppleDistributionCertificateFragment[],
|
||||
validDistributionCertificates?: AppleDistributionCertificateFragment[]
|
||||
): Promise<AppleDistributionCertificateFragment | null> {
|
||||
@@ -93,7 +93,7 @@ export async function selectDistributionCertificateWithDependenciesAsync(
|
||||
return null;
|
||||
}
|
||||
if (!ctx.appStore.authCtx) {
|
||||
return _selectDistributionCertificateAsync(distCertsForAccount);
|
||||
return selectDistributionCertificateAsync(distCertsForAccount);
|
||||
}
|
||||
|
||||
// get valid certs on the developer portal
|
||||
@@ -103,7 +103,7 @@ export async function selectDistributionCertificateWithDependenciesAsync(
|
||||
certInfoFromApple
|
||||
);
|
||||
|
||||
return _selectDistributionCertificateAsync(distCertsForAccount, validDistCerts);
|
||||
return selectDistributionCertificateAsync(distCertsForAccount, validDistCerts);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,7 +121,7 @@ export async function selectValidDistributionCertificateAsync(
|
||||
return null;
|
||||
}
|
||||
if (!ctx.appStore.authCtx) {
|
||||
return _selectDistributionCertificateAsync(distCertsForAccount);
|
||||
return selectDistributionCertificateAsync(distCertsForAccount);
|
||||
}
|
||||
|
||||
// filter by apple team
|
||||
@@ -140,7 +140,7 @@ export async function selectValidDistributionCertificateAsync(
|
||||
Log.log(
|
||||
`${validDistCerts.length}/${distCertsForAccount.length} Distribution Certificates are currently valid for Apple Team ${ctx.appStore.authCtx?.team.id}.`
|
||||
);
|
||||
return _selectDistributionCertificateAsync(validDistCerts);
|
||||
return selectDistributionCertificateAsync(validDistCerts);
|
||||
}
|
||||
|
||||
const APPLE_DIST_CERTS_TOO_MANY_GENERATED_ERROR = `
|
||||
@@ -152,15 +152,14 @@ Please remember that Apple Distribution Certificates are not application specifi
|
||||
`;
|
||||
|
||||
export async function provideOrGenerateDistributionCertificateAsync(
|
||||
ctx: CredentialsContext,
|
||||
accountName: string
|
||||
ctx: CredentialsContext
|
||||
): Promise<DistributionCertificate> {
|
||||
if (!ctx.nonInteractive) {
|
||||
const userProvided = await promptForDistCertAsync(ctx);
|
||||
if (userProvided) {
|
||||
if (!ctx.appStore.authCtx) {
|
||||
Log.warn(
|
||||
'Unable to validate distribution certificate due to insufficient Apple Credentials'
|
||||
'Unable to validate distribution certificate, you are not authenticated with Apple.'
|
||||
);
|
||||
return userProvided;
|
||||
} else {
|
||||
@@ -168,13 +167,11 @@ export async function provideOrGenerateDistributionCertificateAsync(
|
||||
if (!isValid) {
|
||||
Log.warn("Provided Distribution Certificate is no longer valid on Apple's server");
|
||||
}
|
||||
return isValid
|
||||
? userProvided
|
||||
: await provideOrGenerateDistributionCertificateAsync(ctx, accountName);
|
||||
return isValid ? userProvided : await provideOrGenerateDistributionCertificateAsync(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
return await generateDistributionCertificateAsync(ctx, accountName);
|
||||
return await generateDistributionCertificateAsync(ctx);
|
||||
}
|
||||
|
||||
async function promptForDistCertAsync(
|
||||
@@ -200,8 +197,7 @@ async function promptForDistCertAsync(
|
||||
}
|
||||
|
||||
async function generateDistributionCertificateAsync(
|
||||
ctx: CredentialsContext,
|
||||
accountName: string
|
||||
ctx: CredentialsContext
|
||||
): Promise<DistributionCertificate> {
|
||||
await ctx.appStore.ensureAuthenticatedAsync();
|
||||
try {
|
||||
@@ -246,7 +242,7 @@ async function generateDistributionCertificateAsync(
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return await generateDistributionCertificateAsync(ctx, accountName);
|
||||
return await generateDistributionCertificateAsync(ctx);
|
||||
}
|
||||
|
||||
function formatDistributionCertificateFromApple(
|
||||
|
||||
@@ -14,15 +14,12 @@ import { pushKeySchema } from '../credentials';
|
||||
import { isPushKeyValidAndTrackedAsync } from '../validators/validatePushKey';
|
||||
import { formatAppleTeam } from './AppleTeamUtils';
|
||||
|
||||
export async function provideOrGeneratePushKeyAsync(
|
||||
ctx: CredentialsContext,
|
||||
accountName: string
|
||||
): Promise<PushKey> {
|
||||
export async function provideOrGeneratePushKeyAsync(ctx: CredentialsContext): Promise<PushKey> {
|
||||
if (!ctx.nonInteractive) {
|
||||
const userProvided = await promptForPushKeyAsync(ctx);
|
||||
if (userProvided) {
|
||||
if (!ctx.appStore.authCtx) {
|
||||
Log.warn('Unable to validate push key due to insufficient Apple Credentials');
|
||||
Log.warn('Unable to validate push key, you are not authenticated with Apple.');
|
||||
return userProvided;
|
||||
} else {
|
||||
const isValidAndTracked = await isPushKeyValidAndTrackedAsync(ctx, userProvided);
|
||||
@@ -36,11 +33,11 @@ export async function provideOrGeneratePushKeyAsync(
|
||||
if (useUserProvided) {
|
||||
return userProvided;
|
||||
}
|
||||
return await provideOrGeneratePushKeyAsync(ctx, accountName);
|
||||
return await provideOrGeneratePushKeyAsync(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
return await generatePushKeyAsync(ctx, accountName);
|
||||
return await generatePushKeyAsync(ctx);
|
||||
}
|
||||
|
||||
async function promptForPushKeyAsync(ctx: CredentialsContext): Promise<PushKey | null> {
|
||||
@@ -63,10 +60,7 @@ async function promptForPushKeyAsync(ctx: CredentialsContext): Promise<PushKey |
|
||||
return userProvided;
|
||||
}
|
||||
|
||||
async function generatePushKeyAsync(
|
||||
ctx: CredentialsContext,
|
||||
accountName: string
|
||||
): Promise<PushKey> {
|
||||
async function generatePushKeyAsync(ctx: CredentialsContext): Promise<PushKey> {
|
||||
await ctx.appStore.ensureAuthenticatedAsync();
|
||||
try {
|
||||
return await ctx.appStore.createPushKeyAsync();
|
||||
@@ -106,7 +100,7 @@ async function generatePushKeyAsync(
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return await generatePushKeyAsync(ctx, accountName);
|
||||
return await generatePushKeyAsync(ctx);
|
||||
}
|
||||
|
||||
function formatPushKeyFromApple(pushKey: PushKeyStoreInfo): string {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { selectAscApiKeysFromAccountAsync } from './AscApiKeyUtils';
|
||||
|
||||
export class SelectAndRemoveAscApiKey {
|
||||
constructor(private account: Account) {}
|
||||
|
||||
async runAsync(ctx: CredentialsContext): Promise<void> {
|
||||
const selected = await selectAscApiKeysFromAccountAsync(ctx, this.account);
|
||||
if (selected) {
|
||||
await new RemoveAscApiKey(this.account, selected).runAsync(ctx);
|
||||
Log.succeed('Removed App Store Connect Api Key');
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoveAscApiKey {
|
||||
constructor(private account: Account, private ascApiKey: AppStoreConnectApiKeyFragment) {}
|
||||
|
||||
public async runAsync(ctx: CredentialsContext): Promise<void> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(`Cannot remove App Store Connect Api Keys in non-interactive mode.`);
|
||||
}
|
||||
|
||||
// TODO(quin): add an extra edge on AppStoreConnectApiKey to find the apps using it
|
||||
const confirm = await confirmAsync({
|
||||
message: `Deleting this Api Key may affect your projects that rely on it. Do you want to continue?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
Log.log('Aborting');
|
||||
return;
|
||||
}
|
||||
|
||||
Log.log('Removing Api Key');
|
||||
await ctx.ios.deleteAscApiKeyAsync(this.ascApiKey.id);
|
||||
|
||||
let shouldRevoke = false;
|
||||
if (!ctx.nonInteractive) {
|
||||
shouldRevoke = await confirmAsync({
|
||||
message: `Do you also want to revoke this Api Key on the Apple Developer Portal?`,
|
||||
});
|
||||
} else if (ctx.nonInteractive) {
|
||||
Log.log('Skipping Api Key revocation on the Apple Developer Portal.');
|
||||
}
|
||||
if (shouldRevoke) {
|
||||
await ctx.appStore.revokeAscApiKeyAsync(this.ascApiKey.keyIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import assert from 'assert';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import {
|
||||
AppStoreConnectApiKeyFragment,
|
||||
CommonIosAppCredentialsFragment,
|
||||
} from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync, promptAsync } from '../../../prompts';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import {
|
||||
MissingCredentialsNonInteractiveError,
|
||||
UnsupportedCredentialsChoiceError,
|
||||
} from '../../errors';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { getValidAndTrackedAscApiKeysAsync } from '../validators/validateAscApiKey';
|
||||
import {
|
||||
AppStoreApiKeyPurpose,
|
||||
formatAscApiKey,
|
||||
getAscApiKeysFromAccountAsync,
|
||||
selectAscApiKeysFromAccountAsync,
|
||||
sortAscApiKeysByUpdatedAtDesc,
|
||||
} from './AscApiKeyUtils';
|
||||
import { AssignAscApiKey } from './AssignAscApiKey';
|
||||
import { CreateAscApiKey } from './CreateAscApiKey';
|
||||
|
||||
export enum SetupAscApiKeyChoice {
|
||||
GENERATE = 'GENERATE',
|
||||
USE_EXISTING = 'USE_EXISTING',
|
||||
}
|
||||
export class SetUpAscApiKey {
|
||||
public choices: { title: string; value: string }[] = [
|
||||
{
|
||||
title: '[Choose an existing key]',
|
||||
value: SetupAscApiKeyChoice.USE_EXISTING,
|
||||
},
|
||||
{ title: '[Add a new key]', value: SetupAscApiKeyChoice.GENERATE },
|
||||
];
|
||||
|
||||
constructor(private app: AppLookupParams, private purpose: AppStoreApiKeyPurpose) {}
|
||||
|
||||
public async runAsync(ctx: CredentialsContext): Promise<CommonIosAppCredentialsFragment> {
|
||||
const isKeySetup = await this.isAscApiKeySetupAsync(ctx, this.purpose);
|
||||
if (isKeySetup) {
|
||||
Log.succeed('App Store Connect Api Key already set up.');
|
||||
return nullthrows(
|
||||
await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(this.app),
|
||||
'iosAppCredentials cannot be null if App Store Connect Api Key is already set up'
|
||||
);
|
||||
}
|
||||
if (ctx.nonInteractive) {
|
||||
throw new MissingCredentialsNonInteractiveError(
|
||||
'App Store Connect Api Keys cannot be set up in --non-interactive mode.'
|
||||
);
|
||||
}
|
||||
|
||||
const keysForAccount = await getAscApiKeysFromAccountAsync(ctx, this.app.account, {
|
||||
filterDifferentAppleTeam: true,
|
||||
});
|
||||
const maybeAutoselectedKey = await this.doBestEffortAutoselectAsync(ctx, keysForAccount);
|
||||
if (maybeAutoselectedKey) {
|
||||
return await new AssignAscApiKey(this.app).runAsync(ctx, maybeAutoselectedKey, this.purpose);
|
||||
}
|
||||
|
||||
const availableChoices =
|
||||
keysForAccount.length === 0
|
||||
? this.choices.filter(choice => choice.value !== SetupAscApiKeyChoice.USE_EXISTING)
|
||||
: this.choices;
|
||||
const ascApiKey = await this.processChoicesAsync(ctx, this.purpose, availableChoices);
|
||||
return await new AssignAscApiKey(this.app).runAsync(ctx, ascApiKey, this.purpose);
|
||||
}
|
||||
|
||||
private async doBestEffortAutoselectAsync(
|
||||
ctx: CredentialsContext,
|
||||
keysForAccount: AppStoreConnectApiKeyFragment[]
|
||||
): Promise<AppStoreConnectApiKeyFragment | null> {
|
||||
if (!ctx.appStore.authCtx) {
|
||||
return null;
|
||||
}
|
||||
if (keysForAccount.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// only provide autoselect if we can find a key that is certainly valid
|
||||
const validKeys = await getValidAndTrackedAscApiKeysAsync(ctx, keysForAccount);
|
||||
if (validKeys.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const [autoselectedKey] = sortAscApiKeysByUpdatedAtDesc(validKeys);
|
||||
const useAutoselected = await confirmAsync({
|
||||
message: `Reuse this App Store Connect Api Key?\n${formatAscApiKey(autoselectedKey)}`,
|
||||
});
|
||||
|
||||
if (useAutoselected) {
|
||||
Log.log(`Using App Store Connect Api Key with ID ${autoselectedKey.keyIdentifier}`);
|
||||
return autoselectedKey;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async isAscApiKeySetupAsync(
|
||||
ctx: CredentialsContext,
|
||||
purpose: AppStoreApiKeyPurpose
|
||||
): Promise<boolean> {
|
||||
const appCredentials = await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(this.app);
|
||||
if (purpose !== AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
|
||||
throw new Error(`App Store Connect Api Key setup is not yet supported for ${purpose}.`);
|
||||
}
|
||||
return !!appCredentials?.appStoreConnectApiKeyForSubmissions;
|
||||
}
|
||||
|
||||
private async processChoicesAsync(
|
||||
ctx: CredentialsContext,
|
||||
purpose: AppStoreApiKeyPurpose,
|
||||
choices: { title: string; value: string }[]
|
||||
): Promise<AppStoreConnectApiKeyFragment> {
|
||||
assert(choices.length > 0, 'SetupAscApiKey: There must be at least one choice');
|
||||
let choice: string;
|
||||
if (choices.length === 1) {
|
||||
choice = choices[0].value;
|
||||
} else {
|
||||
const result = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'choice',
|
||||
message: 'Select the App Store Connect Api Key to use for your project:',
|
||||
choices: this.choices,
|
||||
});
|
||||
choice = result.choice;
|
||||
}
|
||||
|
||||
if (choice === SetupAscApiKeyChoice.GENERATE) {
|
||||
return await new CreateAscApiKey(this.app.account).runAsync(ctx, purpose);
|
||||
} else if (choice === SetupAscApiKeyChoice.USE_EXISTING) {
|
||||
const selectedAscApiKey = await selectAscApiKeysFromAccountAsync(ctx, this.app.account, {
|
||||
filterDifferentAppleTeam: true,
|
||||
});
|
||||
if (!selectedAscApiKey) {
|
||||
return await this.processChoicesAsync(ctx, purpose, choices);
|
||||
}
|
||||
return selectedAscApiKey;
|
||||
}
|
||||
throw new UnsupportedCredentialsChoiceError(
|
||||
`AscApiKey Setup does not support choice:${choice}`,
|
||||
choice
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
IosDistributionType,
|
||||
} from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync, pressAnyKeyToContinueAsync } from '../../../prompts';
|
||||
import { confirmAsync, pressAnyKeyToContinueAsync, promptAsync } from '../../../prompts';
|
||||
import differenceBy from '../../../utils/expodash/differenceBy';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../errors';
|
||||
@@ -23,6 +23,12 @@ import { assignBuildCredentialsAsync, getBuildCredentialsAsync } from './BuildCr
|
||||
import { chooseDevicesAsync, formatDeviceLabel } from './DeviceUtils';
|
||||
import { SetupDistributionCertificate } from './SetupDistributionCertificate';
|
||||
|
||||
enum ReuseAction {
|
||||
Yes,
|
||||
ShowDevices,
|
||||
No,
|
||||
}
|
||||
|
||||
export class SetupAdhocProvisioningProfile {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
@@ -184,10 +190,23 @@ export class SetupAdhocProvisioningProfile {
|
||||
);
|
||||
|
||||
if (allRegisteredDevicesAreProvisioned) {
|
||||
return await confirmAsync({
|
||||
message: `All your registered devices are present in the Provisioning Profile. Would you like to reuse it?`,
|
||||
initial: true,
|
||||
});
|
||||
const reuseAction = await this.promptForReuseActionAsync();
|
||||
if (reuseAction === ReuseAction.Yes) {
|
||||
return true;
|
||||
} else if (reuseAction === ReuseAction.No) {
|
||||
return false;
|
||||
} else {
|
||||
Log.newLine();
|
||||
Log.log('Devices registered in the Provisioning Profile:');
|
||||
for (const device of provisionedDevices) {
|
||||
Log.log(`- ${formatDeviceLabel(device)}`);
|
||||
}
|
||||
Log.newLine();
|
||||
return (
|
||||
(await this.promptForReuseActionAsync({ showShowDevicesOption: false })) ===
|
||||
ReuseAction.Yes
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const missingDevices = differenceBy(registeredAppleDevices, provisionedDevices, 'identifier');
|
||||
Log.warn(`The provisioning profile is missing the following devices:`);
|
||||
@@ -201,6 +220,36 @@ export class SetupAdhocProvisioningProfile {
|
||||
}
|
||||
}
|
||||
|
||||
private async promptForReuseActionAsync({
|
||||
showShowDevicesOption = true,
|
||||
} = {}): Promise<ReuseAction> {
|
||||
const { selected } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'selected',
|
||||
message: `${
|
||||
showShowDevicesOption
|
||||
? 'All your registered devices are present in the Provisioning Profile. '
|
||||
: ''
|
||||
}Would you like to reuse the profile?`,
|
||||
choices: [
|
||||
{ title: 'Yes', value: ReuseAction.Yes },
|
||||
...(showShowDevicesOption
|
||||
? [
|
||||
{
|
||||
title: 'Show devices and ask me again',
|
||||
value: ReuseAction.ShowDevices,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: 'No, let me choose devices again',
|
||||
value: ReuseAction.No,
|
||||
},
|
||||
],
|
||||
});
|
||||
return selected;
|
||||
}
|
||||
|
||||
private async registerDevicesAsync(
|
||||
ctx: CredentialsContext,
|
||||
appleTeam: AppleTeamFragment
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import chalk from 'chalk';
|
||||
import wrapAnsi from 'wrap-ansi';
|
||||
|
||||
import { CommonIosAppCredentialsFragment } from '../../../graphql/generated';
|
||||
import Log, { learnMore } from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { UnsupportedCredentialsChoiceError } from '../../errors';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { AppStoreApiKeyPurpose } from './AscApiKeyUtils';
|
||||
import { SetupAscApiKey } from './SetupAscApiKey';
|
||||
|
||||
const PROMPT_FOR_APP_SPECIFIC_PASSWORD = 'PROMPT_FOR_APP_SPECIFIC_PASSWORD';
|
||||
export class SetupSubmissionCredential {
|
||||
private setupAscApiKeyAction;
|
||||
constructor(app: AppLookupParams) {
|
||||
this.setupAscApiKeyAction = new SetupAscApiKey(app, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
|
||||
this.setupAscApiKeyAction.choices = this.setupAscApiKeyAction.choices.concat({
|
||||
title: '[Enter an App Specific Password]',
|
||||
value: PROMPT_FOR_APP_SPECIFIC_PASSWORD,
|
||||
});
|
||||
}
|
||||
|
||||
public async runAsync(
|
||||
ctx: CredentialsContext
|
||||
): Promise<CommonIosAppCredentialsFragment | string> {
|
||||
try {
|
||||
return await this.setupAscApiKeyAction.runAsync(ctx);
|
||||
} catch (e) {
|
||||
if (e instanceof UnsupportedCredentialsChoiceError) {
|
||||
const choice = e.choice;
|
||||
if (choice === PROMPT_FOR_APP_SPECIFIC_PASSWORD) {
|
||||
return await this.promptForAppSpecificPasswordAsync();
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async promptForAppSpecificPasswordAsync(): Promise<string> {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
wrapAnsi(
|
||||
`This option will be deprecated soon. You will still be able to provide your password with the ${chalk.bold(
|
||||
'EXPO_APPLE_APP_SPECIFIC_PASSWORD'
|
||||
)} environment variable.`,
|
||||
process.stdout.columns || 80
|
||||
)
|
||||
);
|
||||
Log.log(`Please enter your Apple app-specific password.`);
|
||||
Log.log(learnMore('https://expo.fyi/apple-app-specific-password'));
|
||||
|
||||
const { appSpecificPassword } = await promptAsync({
|
||||
name: 'appSpecificPassword',
|
||||
message: 'Your Apple app-specific password:',
|
||||
type: 'password',
|
||||
validate: (val: string) => val !== '' || 'Apple app-specific password cannot be empty!',
|
||||
});
|
||||
return appSpecificPassword;
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ export class SetupTargetBuildCredentials {
|
||||
try {
|
||||
return await this.setupBuildCredentialsAsync(ctx);
|
||||
} catch (error) {
|
||||
Log.error('Failed to setup credentials.');
|
||||
Log.error('Failed to set up credentials.');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { promptAsync } from '../../../../prompts';
|
||||
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { testAscApiKey } from '../../../__tests__/fixtures-ios';
|
||||
import { getCredentialsFromUserAsync } from '../../../utils/promptForCredentials';
|
||||
import {
|
||||
AppStoreApiKeyPurpose,
|
||||
getAscApiKeyName,
|
||||
promptForAscApiKeyPathAsync,
|
||||
} from '../AscApiKeyUtils';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
jest.mock('../../../utils/promptForCredentials');
|
||||
|
||||
function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
|
||||
return Object.keys(obj) as K[];
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
asMock(promptAsync).mockClear();
|
||||
asMock(getCredentialsFromUserAsync).mockClear();
|
||||
});
|
||||
|
||||
describe(getAscApiKeyName, () => {
|
||||
// Apple enforces a 30 char limit on this name
|
||||
it('produces a name under 30 chars', async () => {
|
||||
for (const value of enumKeys(AppStoreApiKeyPurpose)) {
|
||||
const purpose = AppStoreApiKeyPurpose[value];
|
||||
const name = getAscApiKeyName(purpose);
|
||||
expect(name.length < 30).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe(promptForAscApiKeyPathAsync, () => {
|
||||
it('prompts for keyId, keyP8Path and issuerId when user is not authenticated to Apple', async () => {
|
||||
asMock(promptAsync).mockImplementationOnce(() => ({
|
||||
keyP8Path: '/asc-api-key.p8',
|
||||
}));
|
||||
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
|
||||
keyId: 'test-key-id',
|
||||
issuerId: 'test-issuer-id',
|
||||
}));
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
ensureAuthenticatedAsync: jest.fn(() => {
|
||||
throw new Error('this should not be called');
|
||||
}),
|
||||
authCtx: null,
|
||||
},
|
||||
});
|
||||
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
|
||||
expect(ascApiKeyPath).toEqual({
|
||||
keyId: 'test-key-id',
|
||||
issuerId: 'test-issuer-id',
|
||||
keyP8Path: '/asc-api-key.p8',
|
||||
});
|
||||
expect(promptAsync).toHaveBeenCalledTimes(1); // keyP8Path
|
||||
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(2); // keyId, issuerId
|
||||
});
|
||||
it('prompts for keyId, keyP8Path and detects issuerId when user is authenticated to Apple', async () => {
|
||||
asMock(promptAsync).mockImplementationOnce(() => ({
|
||||
keyP8Path: '/asc-api-key.p8',
|
||||
}));
|
||||
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
|
||||
keyId: 'test-key-id',
|
||||
}));
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
|
||||
authCtx: testAuthCtx,
|
||||
getAscApiKeyAsync: jest.fn(() => testAscApiKey),
|
||||
},
|
||||
});
|
||||
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
|
||||
expect(ascApiKeyPath).toEqual({
|
||||
keyId: 'test-key-id',
|
||||
issuerId: 'test-issuer-id-from-apple',
|
||||
keyP8Path: '/asc-api-key.p8',
|
||||
});
|
||||
expect(promptAsync).toHaveBeenCalledTimes(1); // keyP8Path
|
||||
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(1); // keyId
|
||||
expect(asMock(ctx.appStore.getAscApiKeyAsync).mock.calls.length).toBe(1); // issuerId
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { findApplicationTarget } from '../../../../project/ios/target';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { testAscApiKeyFragment, testTargets } from '../../../__tests__/fixtures-ios';
|
||||
import { AppStoreApiKeyPurpose } from '../AscApiKeyUtils';
|
||||
import { AssignAscApiKey } from '../AssignAscApiKey';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
|
||||
describe(AssignAscApiKey, () => {
|
||||
it('assigns an App Store Connect Api Key in Interactive Mode for EAS Submit', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const assignAscApiKeyAction = new AssignAscApiKey(appLookupParams);
|
||||
await assignAscApiKeyAction.runAsync(
|
||||
ctx,
|
||||
testAscApiKeyFragment,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
);
|
||||
|
||||
// expect app credentials to be fetched/created, then updated
|
||||
expect(ctx.ios.createOrGetIosAppCredentialsWithCommonFieldsAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.ios.updateIosAppCredentialsAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('works in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const assignAscApiKeyAction = new AssignAscApiKey(appLookupParams);
|
||||
await assignAscApiKeyAction.runAsync(
|
||||
ctx,
|
||||
testAscApiKeyFragment,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
);
|
||||
|
||||
// dont fail if users are running in non-interactive mode
|
||||
await expect(
|
||||
assignAscApiKeyAction.runAsync(
|
||||
ctx,
|
||||
testAscApiKeyFragment,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
)
|
||||
).resolves.not.toThrowError();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { findApplicationTarget } from '../../../../project/ios/target';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { testAscApiKey, testTargets } from '../../../__tests__/fixtures-ios';
|
||||
import { AppStoreApiKeyPurpose } from '../AscApiKeyUtils';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { CreateAscApiKey } from '../CreateAscApiKey';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
|
||||
describe(CreateAscApiKey, () => {
|
||||
it('creates a App Store Api Key in Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
|
||||
authCtx: testAuthCtx,
|
||||
createAscApiKeyAsync: jest.fn(() => testAscApiKey),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const createAscApiKeyAction = new CreateAscApiKey(appLookupParams.account);
|
||||
await createAscApiKeyAction.runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
|
||||
|
||||
// expect api key to be created on expo servers
|
||||
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
|
||||
// expect api key to be created on apple portal
|
||||
expect(asMock(ctx.appStore.createAscApiKeyAsync).mock.calls.length).toBe(1);
|
||||
});
|
||||
it('errors in Non Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const createAscApiKeyAction = new CreateAscApiKey(appLookupParams.account);
|
||||
await expect(
|
||||
createAscApiKeyAction.runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE)
|
||||
).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { findApplicationTarget } from '../../../../project/ios/target';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { testAscApiKeyFragment, testTargets } from '../../../__tests__/fixtures-ios';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { RemoveAscApiKey } from '../RemoveAscApiKey';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
|
||||
describe(RemoveAscApiKey, () => {
|
||||
it('removes an Asc Api Key', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: false });
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const removeAscApiKeyAction = new RemoveAscApiKey(
|
||||
appLookupParams.account,
|
||||
testAscApiKeyFragment
|
||||
);
|
||||
await removeAscApiKeyAction.runAsync(ctx);
|
||||
expect(ctx.ios.deleteAscApiKeyAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.appStore.revokeAscApiKeyAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: true });
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const removeAscApiKeyAction = new RemoveAscApiKey(
|
||||
appLookupParams.account,
|
||||
testAscApiKeyFragment
|
||||
);
|
||||
await expect(removeAscApiKeyAction.runAsync(ctx)).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { findApplicationTarget } from '../../../../project/ios/target';
|
||||
import { confirmAsync, promptAsync } from '../../../../prompts';
|
||||
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import {
|
||||
getNewIosApiMock,
|
||||
testAscApiKey,
|
||||
testAscApiKeyFragment,
|
||||
testCommonIosAppCredentialsFragment,
|
||||
testTargets,
|
||||
} from '../../../__tests__/fixtures-ios';
|
||||
import { AppStoreApiKeyPurpose } from '../AscApiKeyUtils';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { SetUpAscApiKey, SetupAscApiKeyChoice } from '../SetUpAscApiKey';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
|
||||
describe(SetUpAscApiKey, () => {
|
||||
it('skips setting up a ASC Api Key if it is already configured', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
ios: {
|
||||
...getNewIosApiMock(),
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testCommonIosAppCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupAscApiKeyAction = new SetUpAscApiKey(
|
||||
appLookupParams,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
);
|
||||
await setupAscApiKeyAction.runAsync(ctx);
|
||||
|
||||
// expect not to create a new ASC Api Key on expo servers
|
||||
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
|
||||
// expect configuration not to be updated with a new ASC Api Key
|
||||
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
|
||||
});
|
||||
it('sets up a ASC Api Key, creating a new one if there are no existing ASC Api Keys', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
createAscApiKeyAsync: jest.fn(() => testAscApiKey),
|
||||
},
|
||||
ios: {
|
||||
...getNewIosApiMock(),
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(),
|
||||
createAscApiKeyAsync: jest.fn(() => testAscApiKeyFragment),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupAscApiKeyAction = new SetUpAscApiKey(
|
||||
appLookupParams,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
);
|
||||
await setupAscApiKeyAction.runAsync(ctx);
|
||||
|
||||
// expect to create a new ASC Api Key on expo servers
|
||||
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
|
||||
// expect configuration to be updated with a new ASC Api Key
|
||||
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
|
||||
});
|
||||
it('sets up a ASC Api Key, use autoselected ASC Api Key when there are existing keys', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
|
||||
authCtx: testAuthCtx,
|
||||
listAscApiKeysAsync: jest.fn(() => [testAscApiKey]),
|
||||
},
|
||||
ios: {
|
||||
...getNewIosApiMock(),
|
||||
getAscApiKeysForAccountAsync: jest.fn(() => [testAscApiKeyFragment]),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupAscApiKeyAction = new SetUpAscApiKey(
|
||||
appLookupParams,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
);
|
||||
await setupAscApiKeyAction.runAsync(ctx);
|
||||
|
||||
// expect not to create a new ASC Api Key on expo servers, we are using the existing one
|
||||
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
|
||||
// expect configuration to be updated with a new ASC Api Key
|
||||
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
|
||||
});
|
||||
it('sets up a ASC Api Key, allowing user to choose existing keys', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
ios: {
|
||||
...getNewIosApiMock(),
|
||||
getAscApiKeysForAccountAsync: jest.fn(() => [testAscApiKeyFragment]),
|
||||
},
|
||||
});
|
||||
asMock(promptAsync).mockImplementation(() => ({
|
||||
choice: SetupAscApiKeyChoice.USE_EXISTING,
|
||||
chosenAscApiKey: testAscApiKeyFragment,
|
||||
}));
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupAscApiKeyAction = new SetUpAscApiKey(
|
||||
appLookupParams,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
);
|
||||
await setupAscApiKeyAction.runAsync(ctx);
|
||||
|
||||
// expect not to create a new ASC Api Key on expo servers, we are using the existing one
|
||||
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
|
||||
// expect configuration to be updated with a new ASC Api Key
|
||||
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
|
||||
});
|
||||
it('errors in Non Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const createAscApiKeyAction = new SetUpAscApiKey(
|
||||
appLookupParams,
|
||||
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
|
||||
);
|
||||
await expect(createAscApiKeyAction.runAsync(ctx)).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,10 @@
|
||||
import { UserRole } from '@expo/apple-utils';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import {
|
||||
AppFragment,
|
||||
AppStoreConnectApiKeyFragment,
|
||||
AppStoreConnectUserRole,
|
||||
AppleAppIdentifierFragment,
|
||||
AppleDeviceFragment,
|
||||
AppleDistributionCertificateFragment,
|
||||
@@ -14,7 +17,9 @@ import {
|
||||
import { isWildcardBundleIdentifier } from '../../../project/ios/bundleIdentifier';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { DistributionCertificate, PushKey } from '../appstore/Credentials.types';
|
||||
import { MinimalAscApiKey } from '../credentials';
|
||||
import { AppleTeamMissingError } from '../errors';
|
||||
import { AppStoreConnectApiKeyMutation } from './graphql/mutations/AppStoreConnectApiKeyMutation';
|
||||
import { AppleAppIdentifierMutation } from './graphql/mutations/AppleAppIdentifierMutation';
|
||||
import {
|
||||
AppleDistributionCertificateMutation,
|
||||
@@ -29,6 +34,7 @@ import { AppleTeamMutation } from './graphql/mutations/AppleTeamMutation';
|
||||
import { IosAppBuildCredentialsMutation } from './graphql/mutations/IosAppBuildCredentialsMutation';
|
||||
import { IosAppCredentialsMutation } from './graphql/mutations/IosAppCredentialsMutation';
|
||||
import { AppQuery } from './graphql/queries/AppQuery';
|
||||
import { AppStoreConnectApiKeyQuery } from './graphql/queries/AppStoreConnectApiKeyQuery';
|
||||
import { AppleAppIdentifierQuery } from './graphql/queries/AppleAppIdentifierQuery';
|
||||
import { AppleDeviceQuery } from './graphql/queries/AppleDeviceQuery';
|
||||
import { AppleDistributionCertificateQuery } from './graphql/queries/AppleDistributionCertificateQuery';
|
||||
@@ -161,8 +167,10 @@ export async function updateIosAppCredentialsAsync(
|
||||
appCredentials: CommonIosAppCredentialsFragment,
|
||||
{
|
||||
applePushKeyId,
|
||||
ascApiKeyIdForSubmissions,
|
||||
}: {
|
||||
applePushKeyId?: string;
|
||||
ascApiKeyIdForSubmissions?: string;
|
||||
}
|
||||
): Promise<CommonIosAppCredentialsFragment> {
|
||||
let updatedAppCredentials = appCredentials;
|
||||
@@ -172,6 +180,13 @@ export async function updateIosAppCredentialsAsync(
|
||||
applePushKeyId
|
||||
);
|
||||
}
|
||||
if (ascApiKeyIdForSubmissions) {
|
||||
updatedAppCredentials =
|
||||
await IosAppCredentialsMutation.setAppStoreConnectApiKeyForSubmissionsAsync(
|
||||
updatedAppCredentials.id,
|
||||
ascApiKeyIdForSubmissions
|
||||
);
|
||||
}
|
||||
return updatedAppCredentials;
|
||||
}
|
||||
|
||||
@@ -413,5 +428,78 @@ export async function deletePushKeyAsync(pushKeyId: string): Promise<void> {
|
||||
return await ApplePushKeyMutation.deleteApplePushKeyAsync(pushKeyId);
|
||||
}
|
||||
|
||||
export async function createAscApiKeyAsync(
|
||||
account: Account,
|
||||
ascApiKey: MinimalAscApiKey
|
||||
): Promise<AppStoreConnectApiKeyFragment> {
|
||||
const maybeAppleTeam = ascApiKey.teamId
|
||||
? await createOrGetExistingAppleTeamAsync(account, {
|
||||
appleTeamIdentifier: ascApiKey.teamId,
|
||||
appleTeamName: ascApiKey.teamName,
|
||||
})
|
||||
: null;
|
||||
return await AppStoreConnectApiKeyMutation.createAppStoreConnectApiKeyAsync(
|
||||
{
|
||||
issuerIdentifier: ascApiKey.issuerId,
|
||||
keyIdentifier: ascApiKey.keyId,
|
||||
keyP8: ascApiKey.keyP8,
|
||||
name: ascApiKey.name ?? null,
|
||||
roles: ascApiKey.roles?.map(role => convertUserRoleToGraphqlType(role)) ?? null,
|
||||
appleTeamId: maybeAppleTeam ? maybeAppleTeam.id : null,
|
||||
},
|
||||
account.id
|
||||
);
|
||||
}
|
||||
|
||||
export async function getAscApiKeysForAccountAsync(
|
||||
account: Account
|
||||
): Promise<AppStoreConnectApiKeyFragment[]> {
|
||||
return await AppStoreConnectApiKeyQuery.getAllForAccountAsync(account.name);
|
||||
}
|
||||
|
||||
export async function getAscApiKeyForAppSubmissionsAsync(
|
||||
appLookupParams: AppLookupParams
|
||||
): Promise<AppStoreConnectApiKeyFragment | null> {
|
||||
const maybeIosAppCredentials = await getIosAppCredentialsWithCommonFieldsAsync(appLookupParams);
|
||||
return maybeIosAppCredentials?.appStoreConnectApiKeyForSubmissions ?? null;
|
||||
}
|
||||
|
||||
export async function deleteAscApiKeyAsync(ascApiKeyId: string): Promise<void> {
|
||||
return await AppStoreConnectApiKeyMutation.deleteAppStoreConnectApiKeyAsync(ascApiKeyId);
|
||||
}
|
||||
|
||||
function convertUserRoleToGraphqlType(userRole: UserRole): AppStoreConnectUserRole {
|
||||
switch (userRole) {
|
||||
case UserRole.ADMIN:
|
||||
return AppStoreConnectUserRole.Admin;
|
||||
case UserRole.ACCESS_TO_REPORTS:
|
||||
return AppStoreConnectUserRole.AccessToReports;
|
||||
case UserRole.ACCOUNT_HOLDER:
|
||||
return AppStoreConnectUserRole.AccountHolder;
|
||||
case UserRole.APP_MANAGER:
|
||||
return AppStoreConnectUserRole.AppManager;
|
||||
case UserRole.CLOUD_MANAGED_APP_DISTRIBUTION:
|
||||
return AppStoreConnectUserRole.CloudManagedAppDistribution;
|
||||
case UserRole.CLOUD_MANAGED_DEVELOPER_ID:
|
||||
return AppStoreConnectUserRole.CloudManagedDeveloperId;
|
||||
case UserRole.CREATE_APPS:
|
||||
return AppStoreConnectUserRole.CreateApps;
|
||||
case UserRole.CUSTOMER_SUPPORT:
|
||||
return AppStoreConnectUserRole.CustomerSupport;
|
||||
case UserRole.DEVELOPER:
|
||||
return AppStoreConnectUserRole.Developer;
|
||||
case UserRole.FINANCE:
|
||||
return AppStoreConnectUserRole.Finance;
|
||||
case UserRole.MARKETING:
|
||||
return AppStoreConnectUserRole.Marketing;
|
||||
case UserRole.READ_ONLY:
|
||||
return AppStoreConnectUserRole.ReadOnly;
|
||||
case UserRole.SALES:
|
||||
return AppStoreConnectUserRole.Sales;
|
||||
case UserRole.TECHNICAL:
|
||||
return AppStoreConnectUserRole.Technical;
|
||||
}
|
||||
}
|
||||
|
||||
const formatProjectFullName = ({ account, projectName }: AppLookupParams): string =>
|
||||
`@${account.name}/${projectName}`;
|
||||
+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 {
|
||||
AppStoreConnectApiKeyFragment,
|
||||
AppStoreConnectApiKeyInput,
|
||||
CreateAppStoreConnectApiKeyMutation,
|
||||
DeleteAppStoreConnectApiKeyMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AppStoreConnectApiKeyFragmentNode } from '../../../../../graphql/types/credentials/AppStoreConnectApiKey';
|
||||
|
||||
export const AppStoreConnectApiKeyMutation = {
|
||||
async createAppStoreConnectApiKeyAsync(
|
||||
appStoreConnectApiKeyInput: AppStoreConnectApiKeyInput,
|
||||
accountId: string
|
||||
): Promise<AppStoreConnectApiKeyFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateAppStoreConnectApiKeyMutation>(
|
||||
gql`
|
||||
mutation CreateAppStoreConnectApiKeyMutation(
|
||||
$appStoreConnectApiKeyInput: AppStoreConnectApiKeyInput!
|
||||
$accountId: ID!
|
||||
) {
|
||||
appStoreConnectApiKey {
|
||||
createAppStoreConnectApiKey(
|
||||
appStoreConnectApiKeyInput: $appStoreConnectApiKeyInput
|
||||
accountId: $accountId
|
||||
) {
|
||||
id
|
||||
...AppStoreConnectApiKeyFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(AppStoreConnectApiKeyFragmentNode)}
|
||||
`,
|
||||
{
|
||||
appStoreConnectApiKeyInput,
|
||||
accountId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(
|
||||
data.appStoreConnectApiKey.createAppStoreConnectApiKey,
|
||||
'GraphQL: `createAppStoreConnectApiKey` not defined in server response'
|
||||
);
|
||||
return data.appStoreConnectApiKey.createAppStoreConnectApiKey;
|
||||
},
|
||||
async deleteAppStoreConnectApiKeyAsync(appStoreConnectApiKeyId: string): Promise<void> {
|
||||
await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<DeleteAppStoreConnectApiKeyMutation>(
|
||||
gql`
|
||||
mutation DeleteAppStoreConnectApiKeyMutation($appStoreConnectApiKeyId: ID!) {
|
||||
appStoreConnectApiKey {
|
||||
deleteAppStoreConnectApiKey(id: $appStoreConnectApiKeyId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
appStoreConnectApiKeyId,
|
||||
},
|
||||
{
|
||||
additionalTypenames: ['AppStoreConnectApiKey', 'IosAppCredentials'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
},
|
||||
};
|
||||
+34
@@ -7,6 +7,7 @@ import {
|
||||
CommonIosAppCredentialsFragment,
|
||||
CreateIosAppCredentialsMutation,
|
||||
IosAppCredentialsInput,
|
||||
SetAppStoreConnectApiKeyForSubmissionsMutation,
|
||||
SetPushKeyMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { CommonIosAppCredentialsFragmentNode } from '../../../../../graphql/types/credentials/IosAppCredentials';
|
||||
@@ -80,4 +81,37 @@ export const IosAppCredentialsMutation = {
|
||||
);
|
||||
return data.iosAppCredentials.setPushKey;
|
||||
},
|
||||
async setAppStoreConnectApiKeyForSubmissionsAsync(
|
||||
iosAppCredentialsId: string,
|
||||
ascApiKeyId: string
|
||||
): Promise<CommonIosAppCredentialsFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<SetAppStoreConnectApiKeyForSubmissionsMutation>(
|
||||
gql`
|
||||
mutation SetAppStoreConnectApiKeyForSubmissionsMutation(
|
||||
$iosAppCredentialsId: ID!
|
||||
$ascApiKeyId: ID!
|
||||
) {
|
||||
iosAppCredentials {
|
||||
setAppStoreConnectApiKeyForSubmissions(
|
||||
id: $iosAppCredentialsId
|
||||
ascApiKeyId: $ascApiKeyId
|
||||
) {
|
||||
id
|
||||
...CommonIosAppCredentialsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(CommonIosAppCredentialsFragmentNode)}
|
||||
`,
|
||||
{
|
||||
iosAppCredentialsId,
|
||||
ascApiKeyId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.iosAppCredentials.setAppStoreConnectApiKeyForSubmissions;
|
||||
},
|
||||
};
|
||||
@@ -22,7 +22,10 @@ export const AppQuery = {
|
||||
}
|
||||
${print(AppFragmentNode)}
|
||||
`,
|
||||
{ fullName }
|
||||
{ fullName },
|
||||
{
|
||||
additionalTypenames: ['App'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import {
|
||||
AppStoreConnectApiKeyByAccountQuery,
|
||||
AppStoreConnectApiKeyFragment,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AppStoreConnectApiKeyFragmentNode } from '../../../../../graphql/types/credentials/AppStoreConnectApiKey';
|
||||
|
||||
export const AppStoreConnectApiKeyQuery = {
|
||||
async getAllForAccountAsync(accountName: string): Promise<AppStoreConnectApiKeyFragment[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<AppStoreConnectApiKeyByAccountQuery>(
|
||||
gql`
|
||||
query AppStoreConnectApiKeyByAccountQuery($accountName: String!) {
|
||||
account {
|
||||
byName(accountName: $accountName) {
|
||||
id
|
||||
appStoreConnectApiKeys {
|
||||
id
|
||||
...AppStoreConnectApiKeyFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(AppStoreConnectApiKeyFragmentNode)}
|
||||
`,
|
||||
{
|
||||
accountName,
|
||||
},
|
||||
{
|
||||
additionalTypenames: ['AppStoreConnectApiKey'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.account.byName.appStoreConnectApiKeys;
|
||||
},
|
||||
};
|
||||
@@ -111,7 +111,10 @@ export const AppleDeviceQuery = {
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ accountName, appleTeamIdentifier }
|
||||
{ accountName, appleTeamIdentifier },
|
||||
{
|
||||
additionalTypenames: ['AppleDevice', 'AppleTeam'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
@@ -147,7 +150,10 @@ export const AppleDeviceQuery = {
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ accountName, identifier }
|
||||
{ accountName, identifier },
|
||||
{
|
||||
additionalTypenames: ['AppleDevice', 'AppleTeam'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
+8
-1
@@ -60,7 +60,11 @@ export const AppleDistributionCertificateQuery = {
|
||||
iosDistributionType,
|
||||
},
|
||||
{
|
||||
additionalTypenames: ['IosAppCredentials', 'IosAppBuildCredentials'],
|
||||
additionalTypenames: [
|
||||
'AppleDistributionCertificate',
|
||||
'IosAppCredentials',
|
||||
'IosAppBuildCredentials',
|
||||
],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
@@ -93,6 +97,9 @@ export const AppleDistributionCertificateQuery = {
|
||||
`,
|
||||
{
|
||||
accountName,
|
||||
},
|
||||
{
|
||||
additionalTypenames: ['AppleDistributionCertificate'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
|
||||
Loaded 100 of 193 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user