mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8126b2276e | ||
|
|
e1de21015f | ||
|
|
61e505f1f8 | ||
|
|
2e3611161e | ||
|
|
95f5eaaacf | ||
|
|
87a3ea99e2 | ||
|
|
ddab65a55d | ||
|
|
0cf8f8ce0d | ||
|
|
2b531ec70c | ||
|
|
6cd7966904 | ||
|
|
ad72ab990f | ||
|
|
b53412fc00 | ||
|
|
cbb2de9e11 | ||
|
|
593c42f601 | ||
|
|
84b10b9dad | ||
|
|
0f286061e4 | ||
|
|
c8aad01480 | ||
|
|
d70a6bee69 | ||
|
|
67799b5c54 | ||
|
|
05b8eef622 | ||
|
|
50e56b5bc4 | ||
|
|
30ab4e62ec | ||
|
|
a1d901294b | ||
|
|
eed818cfe5 | ||
|
|
db0b6bfcf5 | ||
|
|
da81c1c6df | ||
|
|
42d3296f07 | ||
|
|
f9b4b1a6e8 | ||
|
|
d03603a75f | ||
|
|
ffa9e5ef66 | ||
|
|
e7f3cf9c74 | ||
|
|
e82a4a0a57 | ||
|
|
175f07fac9 | ||
|
|
2869744a5d | ||
|
|
99f898a483 | ||
|
|
1448db96e6 | ||
|
|
e02036a46a | ||
|
|
09f33263cb | ||
|
|
412b1ec74c | ||
|
|
9a1538fb4d | ||
|
|
0ad9b31ee3 | ||
|
|
4cbb7bb2f2 | ||
|
|
ae109cd8c6 | ||
|
|
fbaba774a6 | ||
|
|
38bc8062a4 | ||
|
|
d41d184b55 | ||
|
|
d6d32a7a7d | ||
|
|
15a9532fa7 | ||
|
|
3b9fb724fe | ||
|
|
33767b5635 | ||
|
|
175fc2b30e | ||
|
|
aec9347d69 | ||
|
|
91500d07c0 | ||
|
|
62ca14b504 |
No files matched your search
@@ -22,7 +22,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn lerna run build
|
||||
- run: yarn typecheck
|
||||
- run: yarn test
|
||||
if: ${{ !matrix.coverage }}
|
||||
- run: yarn test --coverage
|
||||
|
||||
+81
-3
@@ -8,16 +8,94 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Implement offline distribution certificate validation when running a build in non-interactive mode. ([#344](https://github.com/expo/eas-cli/pull/344) by [@dsokal](https://github.com/dsokal))
|
||||
- Add support for building internal distribution apps for Apple Enterprise Teams. ([#344](https://github.com/expo/eas-cli/pull/344) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Display descriptive error message when API for EAS Build changes. ([#359](https://github.com/expo/eas-cli/pull/359) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [0.12.0](https://github.com/expo/eas-cli/releases/tag/v0.12.0) - 2021-04-22
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add the initiating user's username to the build metadata. ([#354](https://github.com/expo/eas-cli/pull/354) by [@dsokal](https://github.com/dsokal))
|
||||
- Add `--clear-cache` flag for `eas build`. ([#355](https://github.com/expo/eas-cli/pull/355) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the bug where the account name was used as the username. ([#354](https://github.com/expo/eas-cli/pull/354) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.11.1](https://github.com/expo/eas-cli/releases/tag/v0.11.1) - 2021-04-20
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Resolve ios distribution types. ([#348](https://github.com/expo/eas-cli/pull/348) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Bump version of `@expo/apple-utils` to fix capabilities sync immediately after initial identifier registration. ([593c4](https://github.com/expo/eas-cli/commit/593c42f601396deba1751489343ec07b0a8876da) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.11.0](https://github.com/expo/eas-cli/releases/tag/v0.11.0) - 2021-04-20
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- `"credentialsSource": "auto"` is now deprecated. The option will use `"remote"` as the default value. ([#345](https://github.com/expo/eas-cli/pull/345) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add validation when setting up ad-hoc provisioning profile in non-interactive mode. ([#338](https://github.com/expo/eas-cli/pull/338) by [@dsokal](https://github.com/dsokal))
|
||||
- Allow for registering devices when running an ad-hoc build for the first time. ([#340](https://github.com/expo/eas-cli/pull/340) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the bug where all Android managed builds produced AAB archives. The `buildType` property from `eas.json` is not ignored now. ([#349](https://github.com/expo/eas-cli/pull/349) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.10.0](https://github.com/expo/eas-cli/releases/tag/v0.10.0) - 2021-04-16
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- `secrets:<COMMAND>` is renamed to `secret:<COMMAND>` ([#315](https://github.com/expo/eas-cli/pull/315) by [@fiberjw](https://github.com/fiberjw))
|
||||
- `secret:delete` now accepts `id` through a flag rather than an argument ([#315](https://github.com/expo/eas-cli/pull/315) by [@fiberjw](https://github.com/fiberjw))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Use special Expo SDK runtime version for managed projects ([#336](https://github.com/expo/eas-cli/pull/336)) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the behavior where the provisioning profile was invalidated after syncing bundle id capabilities. ([#334](https://github.com/expo/eas-cli/pull/334) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Change build credentials summary format. ([#321](https://github.com/expo/eas-cli/pull/321) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.9.1](https://github.com/expo/eas-cli/releases/tag/v0.9.1) - 2021-04-09
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix provisioning profile validation to actually check status. ([#318](https://github.com/expo/eas-cli/pull/318) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Replace Credentials REST API calls with GraphQL counterparts, also improved DX. ([#293](https://github.com/expo/eas-cli/pull/293), [#299](https://github.com/expo/eas-cli/pull/299), [#301](https://github.com/expo/eas-cli/pull/301), [#317](https://github.com/expo/eas-cli/pull/317) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.9.0](https://github.com/expo/eas-cli/releases/tag/v0.9.0) - 2021-04-09
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Retry graphQL call on network or transient server errors. ([#320](https://github.com/expo/eas-cli/pull/320) by [@jkhales](https://github.com/jkhales))
|
||||
- Display more friendly error messages when `eas submit` fails. ([#311](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
|
||||
- Add support for managing webhooks (new commands: `webhook:create`, `webhook:view`, `webhook:list`, `webhook:update`, and `webhook:delete`). ([#314](https://github.com/expo/eas-cli/pull/314) by [@dsokal](https://github.com/dsokal))
|
||||
- Support for local builds [experimental]. ([#305](https://github.com/expo/eas-cli/pull/305) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the issue where the first iOS build fails for a project without `ios.bundleIdentifier` set in `app.json`. ([#319](https://github.com/expo/eas-cli/pull/319) by [@dsokal](https://github.com/dsokal))
|
||||
- Bump @expo/config-plugins to fix `eas build:configure` on Windows
|
||||
|
||||
### 🧹 Chores
|
||||
- Bump @expo/config-plugins to fix `eas build:configure` on Windows. ([d0f3e](https://github.com/expo/eas-cli/commit/d0f3e387e7f31dbce4c3b93b8d083c7d20d9e30d) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Replace REST API calls with GraphQL counterparts. ([#333](https://github.com/expo/eas-cli/pull/333) by [@barthap](https://github.com/barthap))
|
||||
|
||||
## [0.8.1](https://github.com/expo/eas-cli/releases/tag/v0.8.1) - 2021-04-06
|
||||
|
||||
|
||||
+27
-6
@@ -1,9 +1,30 @@
|
||||
## Releasing a new version
|
||||
|
||||
1. Add release notes to `CHANGELOG.md`.
|
||||
1. Run `yarn release` in the repository root folder. This will create a new tag
|
||||
like `v1.0.0`.
|
||||
1. Wait until the `release` GitHub workflow has finished.
|
||||
1. Once the release ready to go, you can go to
|
||||
https://github.com/expo/eas-cli/releases and mark it as not draft.
|
||||
1. Run `yarn release` in the repository root folder. This will create a new tag like `v1.0.0`.
|
||||
1. **Wait until the `release` GitHub workflow has finished.**
|
||||
1. Once the release is ready to go, you can go to https://github.com/expo/eas-cli/releases and mark it as not draft.
|
||||
- Remember to copy the changelog entries from `CHANGELOG.md`.
|
||||
1. Run `yarn lerna publish from-git` in the repository root folder. This will publish the packages to npm.
|
||||
|
||||
- This command works only if `HEAD` is on the release commit, e.g. when `git log -1` prints:
|
||||
|
||||
```
|
||||
commit cbb2de9e118bfbcb5a597b261d2dbef0bb5d4b0b (tag: v0.11.1)
|
||||
Author: Brent Vatne <[email protected]>
|
||||
Date: Tue Apr 20 15:00:15 2021 -0700
|
||||
|
||||
v0.11.1
|
||||
```
|
||||
|
||||
- If you run `yarn lerna publish from-git` on any other commit, it'll print something like:
|
||||
|
||||
```
|
||||
lerna notice from-git No tagged release found
|
||||
lerna success No changed packages to publish
|
||||
```
|
||||
|
||||
- If you ever run into the above issue, use `git checkout RELEASE_COMMIT_HASH` to checkout to the release commit. Later, when you've published the packages to npm, run `git checkout main` to go back to the latest commit.
|
||||
|
||||
1. Update `CHANGELOG.md`.
|
||||
- Add the new section with the release version in the title.
|
||||
- Leave the `main` section empty (without any changelog entries).
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.9.0"
|
||||
"version": "0.13.0"
|
||||
}
|
||||
@@ -6,6 +6,9 @@
|
||||
"scripts": {
|
||||
"build": "lerna run build",
|
||||
"build:tarballs:linux": "yarn build && yarn workspace eas-cli oclif-dev pack --targets linux-x64",
|
||||
"typecheck": "lerna run typecheck",
|
||||
"start": "lerna run watch --parallel",
|
||||
"watch": "yarn start",
|
||||
"eas": "packages/eas-cli/bin/run",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"release": "lerna version",
|
||||
|
||||
+45
-23
@@ -47,9 +47,9 @@ eas --help COMMAND
|
||||
* [`eas device:list`](#eas-devicelist)
|
||||
* [`eas device:view [UDID]`](#eas-deviceview-udid)
|
||||
* [`eas help [COMMAND]`](#eas-help-command)
|
||||
* [`eas secrets:create`](#eas-secretscreate)
|
||||
* [`eas secrets:delete [ID]`](#eas-secretsdelete-id)
|
||||
* [`eas secrets:list`](#eas-secretslist)
|
||||
* [`eas secret:create`](#eas-secretcreate)
|
||||
* [`eas secret:delete`](#eas-secretdelete)
|
||||
* [`eas secret:list`](#eas-secretlist)
|
||||
* [`eas submit --platform=(android|ios)`](#eas-submit---platformandroidios)
|
||||
* [`eas webhook:create`](#eas-webhookcreate)
|
||||
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
|
||||
@@ -69,7 +69,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/account/login.ts)_
|
||||
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -83,7 +83,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/account/logout.ts)_
|
||||
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -97,7 +97,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/account/view.ts)_
|
||||
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -108,6 +108,8 @@ USAGE
|
||||
$ eas analytics [STATUS]
|
||||
```
|
||||
|
||||
_See code: [build/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/analytics.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
Start a build
|
||||
@@ -118,6 +120,8 @@ USAGE
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios|all)
|
||||
--clear-cache Clear cache before the build
|
||||
--local Run build locally [experimental]
|
||||
--non-interactive Run command in --non-interactive mode
|
||||
--profile=profile [default: release] Name of the build profile from eas.json
|
||||
--skip-credentials-check Skip validation of build credentials
|
||||
@@ -125,7 +129,7 @@ OPTIONS
|
||||
--[no-]wait Wait for build(s) to complete
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/build/index.ts)_
|
||||
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -136,6 +140,8 @@ USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
Configure the project to support EAS Build.
|
||||
@@ -149,7 +155,7 @@ OPTIONS
|
||||
--allow-experimental Enable experimental configuration steps.
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/build/configure.ts)_
|
||||
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -165,7 +171,7 @@ OPTIONS
|
||||
--status=(in-queue|in-progress|errored|finished|canceled)
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/build/list.ts)_
|
||||
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/build/list.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -176,7 +182,7 @@ USAGE
|
||||
$ eas build:view [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/build/view.ts)_
|
||||
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/build/view.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -187,7 +193,7 @@ USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/credentials.ts)_
|
||||
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -198,7 +204,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/device/create.ts)_
|
||||
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/device/create.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -212,7 +218,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/device/list.ts)_
|
||||
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -223,7 +229,7 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/device/view.ts)_
|
||||
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/device/view.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -242,13 +248,13 @@ OPTIONS
|
||||
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.0/src/commands/help.ts)_
|
||||
|
||||
## `eas secrets:create`
|
||||
## `eas secret:create`
|
||||
|
||||
Create an environment secret on the current project or owner account.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas secrets:create
|
||||
$ eas secret:create
|
||||
|
||||
OPTIONS
|
||||
--name=name Name of the secret
|
||||
@@ -256,30 +262,36 @@ OPTIONS
|
||||
--value=value Value of the secret
|
||||
```
|
||||
|
||||
## `eas secrets:delete [ID]`
|
||||
_See code: [build/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/secret/create.ts)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
Delete an environment secret by ID.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas secrets:delete [ID]
|
||||
$ eas secret:delete
|
||||
|
||||
ARGUMENTS
|
||||
ID ID of the secret to delete
|
||||
OPTIONS
|
||||
--id=id ID of the secret to delete
|
||||
|
||||
DESCRIPTION
|
||||
Unsure where to find the secret's ID? Run eas secrets:list
|
||||
```
|
||||
|
||||
## `eas secrets:list`
|
||||
_See code: [build/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
Lists environment secrets available for your current app
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas secrets:list
|
||||
$ eas secret:list
|
||||
```
|
||||
|
||||
_See code: [build/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/secret/list.ts)_
|
||||
|
||||
## `eas submit --platform=(android|ios)`
|
||||
|
||||
Submits build artifact to app store
|
||||
@@ -357,7 +369,7 @@ EXAMPLES
|
||||
and provide its App ID
|
||||
```
|
||||
|
||||
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/submit.ts)_
|
||||
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/submit.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -376,6 +388,8 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
Delete a webhook on the current project.
|
||||
@@ -388,6 +402,8 @@ ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
List webhooks on the current project.
|
||||
@@ -400,6 +416,8 @@ OPTIONS
|
||||
--event=(BUILD) Event type that triggers the webhook
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
Create a webhook on the current project.
|
||||
@@ -418,6 +436,8 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
View a webhook on the current project.
|
||||
@@ -429,4 +449,6 @@ USAGE
|
||||
ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.13.0/build/commands/webhook/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
@@ -518,6 +518,22 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "submissions",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SubmissionQuery",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "userInvitationPublicData",
|
||||
"description": "Top-level query object for querying UserInvitationPublicData publicly.",
|
||||
@@ -961,6 +977,11 @@
|
||||
"kind": "OBJECT",
|
||||
"name": "BuildJob",
|
||||
"ofType": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "Submission",
|
||||
"ofType": null
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2840,6 +2861,73 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "submissions",
|
||||
"description": "EAS Submissions associated with this app",
|
||||
"args": [
|
||||
{
|
||||
"name": "filter",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "SubmissionFilter",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "offset",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Submission",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "iosAppCredentials",
|
||||
"description": "iOS app credentials for the project",
|
||||
@@ -2983,13 +3071,9 @@
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "UpdateChannel",
|
||||
"ofType": null
|
||||
}
|
||||
"kind": "OBJECT",
|
||||
"name": "UpdateChannel",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
@@ -3067,13 +3151,9 @@
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "UpdateBranch",
|
||||
"ofType": null
|
||||
}
|
||||
"kind": "OBJECT",
|
||||
"name": "UpdateBranch",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
@@ -5791,6 +5871,280 @@
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "SubmissionFilter",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "platform",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "ENUM",
|
||||
"name": "AppPlatform",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "ENUM",
|
||||
"name": "SubmissionStatus",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "SubmissionStatus",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "IN_QUEUE",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "IN_PROGRESS",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "FINISHED",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "ERRORED",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "Submission",
|
||||
"description": "Represents an EAS Submission",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "actor",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "INTERFACE",
|
||||
"name": "Actor",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "activityTimestamp",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "DateTime",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "app",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "App",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "initiatingActor",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "INTERFACE",
|
||||
"name": "Actor",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "platform",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "AppPlatform",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "SubmissionStatus",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "logsUrl",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SubmissionError",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "createdAt",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "DateTime",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "updatedAt",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "DateTime",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
{
|
||||
"kind": "INTERFACE",
|
||||
"name": "ActivityTimelineProjectActivity",
|
||||
"ofType": null
|
||||
}
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "SubmissionError",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "errorCode",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "IosAppCredentialsFilter",
|
||||
@@ -9916,8 +10270,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
"isDeprecated": true,
|
||||
"deprecationReason": "Use App.builds instead"
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
@@ -10118,6 +10472,75 @@
|
||||
"name": "ProjectQuery",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "byAccountNameAndSlug",
|
||||
"description": "",
|
||||
"args": [
|
||||
{
|
||||
"name": "accountName",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "slug",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "platform",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "ENUM",
|
||||
"name": "AppPlatform",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "sdkVersions",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INTERFACE",
|
||||
"name": "Project",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "byUsernameAndSlug",
|
||||
"description": "",
|
||||
@@ -10184,8 +10607,8 @@
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
"isDeprecated": true,
|
||||
"deprecationReason": "See byAccountNameAndSlug"
|
||||
},
|
||||
{
|
||||
"name": "byPaths",
|
||||
@@ -10219,8 +10642,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
"isDeprecated": true,
|
||||
"deprecationReason": "No longer supported"
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
@@ -10396,6 +10819,48 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "SubmissionQuery",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "byId",
|
||||
"description": "Look up EAS Submission by submission ID",
|
||||
"args": [
|
||||
{
|
||||
"name": "submissionId",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Submission",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "UserInvitationPublicDataQuery",
|
||||
@@ -11001,6 +11466,22 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "submission",
|
||||
"description": "Mutations that modify an EAS Submit submission",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SubmissionMutation",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "updateChannel",
|
||||
"description": "",
|
||||
@@ -11694,6 +12175,57 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "extendOffer",
|
||||
"description": "Extend offer to account",
|
||||
"args": [
|
||||
{
|
||||
"name": "accountName",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "offer",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "StandardOffer",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "suppressMessage",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Account",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "sendEmail",
|
||||
"description": "Send an email to primary account email",
|
||||
@@ -11778,7 +12310,7 @@
|
||||
},
|
||||
{
|
||||
"name": "rename",
|
||||
"description": "Rename this account",
|
||||
"description": "Rename this account and the primary user's username if this account is a personal account",
|
||||
"args": [
|
||||
{
|
||||
"name": "accountID",
|
||||
@@ -11827,6 +12359,41 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "StandardOffer",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "DEFAULT",
|
||||
"description": "$29 USD per month, 30 day trial",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "YEARLY_SUB",
|
||||
"description": "$348 USD per year, 30 day trial",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "YC_DEALS",
|
||||
"description": "$29 USD per month, 1 year trial",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "SUPPORT",
|
||||
"description": "$800 USD per month",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "EmailTemplate",
|
||||
@@ -14985,6 +15552,16 @@
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "buildType",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "ENUM",
|
||||
"name": "AndroidManagedBuildType",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "username",
|
||||
"description": "",
|
||||
@@ -15000,6 +15577,35 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "AndroidManagedBuildType",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "APK",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "APP_BUNDLE",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "DEVELOPMENT_CLIENT",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "IosGenericJobInput",
|
||||
@@ -16169,6 +16775,128 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "SubmissionMutation",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "createSubmission",
|
||||
"description": "Create an EAS Submit submission",
|
||||
"args": [
|
||||
{
|
||||
"name": "input",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "CreateSubmissionInput",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "CreateSubmissionResult",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "CreateSubmissionInput",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "appId",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "platform",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "AppPlatform",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "config",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "JSONObject",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "CreateSubmissionResult",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "submission",
|
||||
"description": "Created submission",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Submission",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "UpdateChannelMutation",
|
||||
@@ -16336,11 +17064,11 @@
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "deleteUpdate",
|
||||
"description": "Delete an EAS update",
|
||||
"name": "deleteUpdateGroup",
|
||||
"description": "Delete an EAS update group",
|
||||
"args": [
|
||||
{
|
||||
"name": "updateId",
|
||||
"name": "group",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
@@ -16359,7 +17087,7 @@
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "DeleteUpdateResult",
|
||||
"name": "DeleteUpdateGroupResult",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
@@ -16374,11 +17102,11 @@
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "DeleteUpdateResult",
|
||||
"name": "DeleteUpdateGroupResult",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"name": "group",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
@@ -18924,35 +19652,6 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "StandardOffer",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "DEFAULT",
|
||||
"description": "$29 USD per month, 30 day trial",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "YC_DEALS",
|
||||
"description": "$29 USD per month, 1 year trial",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "SUPPORT",
|
||||
"description": "$800 USD per month",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "BaseSearchResult",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.9.0",
|
||||
"version": "0.13.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -10,15 +10,16 @@
|
||||
"dependencies": {
|
||||
"@amplitude/identify": "^1.5.0",
|
||||
"@amplitude/node": "^1.5.0",
|
||||
"@expo/apple-utils": "0.0.0-alpha.17",
|
||||
"@expo/apple-utils": "0.0.0-alpha.18",
|
||||
"@expo/config": "~3.3.19",
|
||||
"@expo/config-plugins": "1.0.24",
|
||||
"@expo/eas-build-job": "0.2.22",
|
||||
"@expo/eas-json": "^0.9.0",
|
||||
"@expo/eas-build-job": "0.2.28",
|
||||
"@expo/eas-json": "^0.13.0",
|
||||
"@expo/json-file": "^8.2.24",
|
||||
"@expo/plist": "^0.0.11",
|
||||
"@expo/plugin-warn-if-update-available": "^1.7.0",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@expo/sdk-runtime-versions": "^1.0.0",
|
||||
"@expo/spawn-async": "^1.5.0",
|
||||
"@hapi/joi": "^17.1.1",
|
||||
"@oclif/command": "^1",
|
||||
@@ -163,7 +164,9 @@
|
||||
"scripts": {
|
||||
"postpack": "rm -f oclif.manifest.json",
|
||||
"prepack": "rm -rf build && yarn build && yarn oclif-dev manifest && yarn oclif-dev readme",
|
||||
"build": "tsc",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"watch": "yarn build --watch --preserveWatchOutput",
|
||||
"typecheck": "tsc",
|
||||
"test": "jest",
|
||||
"version": "yarn oclif-dev readme && git add README.md",
|
||||
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml"
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('apiClient', () => {
|
||||
],
|
||||
});
|
||||
|
||||
let error: Error;
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await apiClient.post('test');
|
||||
} catch (e) {
|
||||
@@ -41,7 +41,7 @@ describe('apiClient', () => {
|
||||
it('does not convert non-APIv2 error to ApiV2Error', async () => {
|
||||
nock(getExpoApiBaseUrl()).post('/--/api/v2/test').reply(500, 'Something went wrong');
|
||||
|
||||
let error: Error;
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await apiClient.post('test');
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import prompts from 'prompts';
|
||||
|
||||
import { asMock } from '../../__tests__/utils';
|
||||
import { CredentialsProvider } from '../../credentials/CredentialsProvider';
|
||||
import { ensureCredentialsAsync } from '../credentials';
|
||||
|
||||
jest.mock('prompts');
|
||||
|
||||
function createMockCredentialsProvider({
|
||||
hasRemote,
|
||||
hasLocal,
|
||||
isLocalSynced,
|
||||
}: any): CredentialsProvider {
|
||||
return {
|
||||
platform: 'android',
|
||||
hasRemoteAsync: jest.fn().mockImplementation(() => hasRemote || false),
|
||||
hasLocalAsync: jest.fn().mockImplementation(() => hasLocal || false),
|
||||
isLocalSyncedAsync: jest.fn().mockImplementation(() => isLocalSynced || false),
|
||||
} as CredentialsProvider;
|
||||
}
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
asMock(prompts).mockReset();
|
||||
asMock(prompts).mockImplementation(() => {
|
||||
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
|
||||
});
|
||||
});
|
||||
|
||||
describe(ensureCredentialsAsync, () => {
|
||||
describe('for generic builds', () => {
|
||||
it("should use credentials.json if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it("should use credentials from server if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should use credentials.json if credentials are the same in credentials.json and on server', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it('should ask which credentials to use when local and remote crednitals have different values', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { select: 'local' };
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it('should ask when local and remote are not the same (select remote)', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { select: 'remote' };
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should should throw an error when local and remote are not the same (in non interactive mode)', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, true);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
'Contents of your local credentials.json for Android are not the same as credentials on EAS servers'
|
||||
);
|
||||
}
|
||||
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it('should ask when no credentials are present and return remote if confirm=true', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: false,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { value: true }; //confirm
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should ask when no credentials are present and abort if confimr=false', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: false,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { value: false }; //confirm
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, false);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
'Aborting build process, credentials are not configured for Android'
|
||||
);
|
||||
}
|
||||
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should throw an error when no credentials are present in non interactive mode', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: false,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementation(() => {
|
||||
return { value: true }; // confirm
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, true);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch('Credentials for this app are not configured');
|
||||
}
|
||||
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('for managed builds', () => {
|
||||
it("should use credentials.json if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.MANAGED,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it("should use credentials from server if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.MANAGED,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should use credentials.json if credentials are the same in credentials.json and on server', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.MANAGED,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
|
||||
it('should use credentials.json even if credentials on server are different', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.MANAGED,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generic builds with credentialsSource set to local', () => {
|
||||
it('should use credentials.json without asking even if credentials on server are different', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.LOCAL,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generic builds with credentialsSource set to remote', () => {
|
||||
it('should use credentials from server even if credentials.json exists and have different values', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.REMOTE,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,9 +13,9 @@ import Log from '../../log';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { CredentialsResult, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommandContext, createBuildContext } from '../context';
|
||||
import { ensureCredentialsAsync } from '../credentials';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { Platform } from '../types';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
import { ensureApplicationIdIsValidAsync } from './applicationId';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { transformGenericJob, transformManagedJob } from './graphql';
|
||||
@@ -24,7 +24,7 @@ import { prepareJobAsync } from './prepareJob';
|
||||
export async function prepareAndroidBuildAsync(
|
||||
commandCtx: CommandContext,
|
||||
easConfig: EasConfig
|
||||
): Promise<() => Promise<string>> {
|
||||
): Promise<() => Promise<string | undefined>> {
|
||||
const buildCtx = createBuildContext<Platform.ANDROID>({
|
||||
commandCtx,
|
||||
platform: Platform.ANDROID,
|
||||
@@ -125,12 +125,8 @@ async function ensureAndroidCredentialsAsync(
|
||||
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
|
||||
}
|
||||
);
|
||||
const credentialsSource = await ensureCredentialsAsync(
|
||||
provider,
|
||||
ctx.buildProfile.workflow,
|
||||
ctx.buildProfile.credentialsSource,
|
||||
ctx.commandCtx.nonInteractive
|
||||
);
|
||||
const { credentialsSource } = ctx.buildProfile;
|
||||
logCredentialsSource(credentialsSource, Platform.ANDROID);
|
||||
return {
|
||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
||||
source: credentialsSource,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Android } from '@expo/eas-build-job';
|
||||
|
||||
import { AndroidGenericJobInput, AndroidManagedJobInput } from '../../graphql/generated';
|
||||
import {
|
||||
AndroidGenericJobInput,
|
||||
AndroidManagedBuildType,
|
||||
AndroidManagedJobInput,
|
||||
} from '../../graphql/generated';
|
||||
import { transformProjectArchive } from '../graphql';
|
||||
|
||||
export function transformGenericJob(job: Android.GenericJob): AndroidGenericJobInput {
|
||||
@@ -25,5 +29,16 @@ export function transformManagedJob(job: Android.ManagedJob): AndroidManagedJobI
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
username: job.username,
|
||||
buildType: transformBuildType(job.buildType),
|
||||
};
|
||||
}
|
||||
|
||||
function transformBuildType(buildType: Android.ManagedBuildType): AndroidManagedBuildType {
|
||||
if (buildType === Android.ManagedBuildType.APK) {
|
||||
return AndroidManagedBuildType.Apk;
|
||||
} else if (buildType === Android.ManagedBuildType.APP_BUNDLE) {
|
||||
return AndroidManagedBuildType.AppBundle;
|
||||
} else {
|
||||
return AndroidManagedBuildType.DevelopmentClient;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,17 @@
|
||||
import {
|
||||
Android,
|
||||
ArchiveSource,
|
||||
ArchiveSourceType,
|
||||
Cache,
|
||||
Job,
|
||||
Workflow,
|
||||
sanitizeJob,
|
||||
} from '@expo/eas-build-job';
|
||||
import { Android, ArchiveSource, Cache, Job, Workflow, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { AndroidGenericBuildProfile, AndroidManagedBuildProfile } from '@expo/eas-json';
|
||||
import path from 'path';
|
||||
|
||||
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
|
||||
import { readEnvironmentSecretsAsync } from '../../credentials/credentialsJson/read';
|
||||
import { getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { gitRootDirectoryAsync } from '../../utils/git';
|
||||
import { BuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
|
||||
interface JobData {
|
||||
archiveBucketKey: string;
|
||||
projectArchive: ArchiveSource;
|
||||
credentials?: AndroidCredentials;
|
||||
}
|
||||
|
||||
@@ -71,10 +64,7 @@ async function prepareJobCommonAsync(
|
||||
|
||||
return {
|
||||
platform: Platform.ANDROID,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: jobData.archiveBucketKey,
|
||||
},
|
||||
projectArchive: jobData.projectArchive,
|
||||
builderEnvironment: {
|
||||
image: ctx.buildProfile.image,
|
||||
node: ctx.buildProfile.node,
|
||||
@@ -82,7 +72,10 @@ async function prepareJobCommonAsync(
|
||||
ndk: ctx.buildProfile.ndk,
|
||||
env: ctx.buildProfile.env,
|
||||
},
|
||||
cache: ctx.buildProfile.cache,
|
||||
cache: {
|
||||
...ctx.buildProfile.cache,
|
||||
clear: ctx.commandCtx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
...(environmentSecrets ? { environmentSecrets } : {}),
|
||||
...buildCredentials,
|
||||
@@ -114,11 +107,11 @@ async function prepareManagedJobAsync(
|
||||
): Promise<Partial<Android.ManagedJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.exp);
|
||||
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
type: Workflow.MANAGED,
|
||||
username: accountName,
|
||||
username,
|
||||
buildType: buildProfile.buildType,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
projectRootDirectory,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Job, Metadata } from '@expo/eas-build-job';
|
||||
import { ArchiveSource, ArchiveSourceType, Job, Metadata } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
@@ -11,6 +11,7 @@ import { formatBytes } from '../utils/files';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
import { requestedPlatformDisplayNames } from './constants';
|
||||
import { BuildContext } from './context';
|
||||
import { runLocalBuildAsync } from './local';
|
||||
import { collectMetadata } from './metadata';
|
||||
import { Platform, TrackingContext } from './types';
|
||||
import Analytics, { Event } from './utils/analytics';
|
||||
@@ -39,9 +40,9 @@ interface Builder<TPlatform extends Platform, Credentials, ProjectConfiguration,
|
||||
prepareJobAsync(
|
||||
ctx: BuildContext<TPlatform>,
|
||||
jobData: {
|
||||
archiveBucketKey: string;
|
||||
credentials?: Credentials;
|
||||
projectConfiguration?: ProjectConfiguration;
|
||||
projectArchive: ArchiveSource;
|
||||
}
|
||||
): Promise<Job>;
|
||||
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
|
||||
@@ -54,7 +55,7 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
TJob extends Job
|
||||
>(
|
||||
builder: Builder<TPlatform, Credentials, ProjectConfiguration, TJob>
|
||||
): Promise<() => Promise<string>> {
|
||||
): Promise<() => Promise<string | undefined>> {
|
||||
const credentialsResult = await withAnalyticsAsync(
|
||||
async () => await builder.ensureCredentialsAsync(builder.ctx),
|
||||
{
|
||||
@@ -83,25 +84,42 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
);
|
||||
}
|
||||
|
||||
const archiveBucketKey = await uploadProjectAsync(builder.ctx);
|
||||
const projectArchive = builder.ctx.commandCtx.local
|
||||
? ({
|
||||
type: ArchiveSourceType.PATH,
|
||||
path: (await makeProjectTarballAsync()).path,
|
||||
} as const)
|
||||
: ({
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: await uploadProjectAsync(builder.ctx),
|
||||
} as const);
|
||||
|
||||
const metadata = await collectMetadata(builder.ctx, {
|
||||
credentialsSource: credentialsResult?.source,
|
||||
});
|
||||
const job = await builder.prepareJobAsync(builder.ctx, {
|
||||
archiveBucketKey,
|
||||
projectArchive,
|
||||
credentials: credentialsResult?.credentials,
|
||||
projectConfiguration: builder.projectConfiguration,
|
||||
});
|
||||
|
||||
return async () => {
|
||||
try {
|
||||
return sendBuildRequestAsync(builder, job, metadata);
|
||||
} catch (error) {
|
||||
if (error?.expoApiV2ErrorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli');
|
||||
if (builder.ctx.commandCtx.local) {
|
||||
await runLocalBuildAsync(job);
|
||||
return undefined;
|
||||
} else {
|
||||
try {
|
||||
return await sendBuildRequestAsync(builder, job, metadata);
|
||||
} catch (error) {
|
||||
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli version.');
|
||||
} else if (error?.graphQLErrors) {
|
||||
Log.error(
|
||||
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, please report the issue.'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
|
||||
const configureCommitMessage = {
|
||||
[RequestedPlatform.Android]: 'Configure EAS Build for Android',
|
||||
[RequestedPlatform.iOS]: 'Configure EAS Build for iOS',
|
||||
[RequestedPlatform.Ios]: 'Configure EAS Build for iOS',
|
||||
[RequestedPlatform.All]: 'Configure EAS Build',
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function configureAsync(options: {
|
||||
shouldConfigureAndroid: [RequestedPlatform.All, RequestedPlatform.Android].includes(
|
||||
options.platform
|
||||
),
|
||||
shouldConfigureIos: [RequestedPlatform.All, RequestedPlatform.iOS].includes(options.platform),
|
||||
shouldConfigureIos: [RequestedPlatform.All, RequestedPlatform.Ios].includes(options.platform),
|
||||
hasAndroidNativeProject: await fs.pathExists(path.join(options.projectDir, 'android')),
|
||||
hasIosNativeProject: await fs.pathExists(path.join(options.projectDir, 'ios')),
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AppPlatform } from '../graphql/generated';
|
||||
import { RequestedPlatform } from './types';
|
||||
|
||||
export const requestedPlatformDisplayNames: Record<RequestedPlatform, string> = {
|
||||
[RequestedPlatform.iOS]: 'iOS',
|
||||
[RequestedPlatform.Ios]: 'iOS',
|
||||
[RequestedPlatform.Android]: 'Android',
|
||||
[RequestedPlatform.All]: 'Android and iOS',
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidBuildProfile, EasConfig, iOSBuildProfile } from '@expo/eas-json';
|
||||
import { AndroidBuildProfile, EasConfig, IosBuildProfile } from '@expo/eas-json';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { getProjectAccountName } from '../project/projectUtils';
|
||||
@@ -19,6 +19,8 @@ export interface CommandContext {
|
||||
projectName: string;
|
||||
exp: ExpoConfig;
|
||||
nonInteractive: boolean;
|
||||
local: boolean;
|
||||
clearCache: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
skipProjectConfiguration: boolean;
|
||||
waitForBuildEnd: boolean;
|
||||
@@ -31,6 +33,8 @@ export async function createCommandContextAsync({
|
||||
projectDir,
|
||||
projectId,
|
||||
nonInteractive = false,
|
||||
local,
|
||||
clearCache = false,
|
||||
skipCredentialsCheck = false,
|
||||
skipProjectConfiguration = false,
|
||||
waitForBuildEnd,
|
||||
@@ -41,6 +45,8 @@ export async function createCommandContextAsync({
|
||||
projectId: string;
|
||||
projectDir: string;
|
||||
nonInteractive: boolean;
|
||||
local: boolean;
|
||||
clearCache: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
skipProjectConfiguration: boolean;
|
||||
waitForBuildEnd: boolean;
|
||||
@@ -59,6 +65,8 @@ export async function createCommandContextAsync({
|
||||
projectName,
|
||||
exp,
|
||||
nonInteractive,
|
||||
local,
|
||||
clearCache,
|
||||
skipCredentialsCheck,
|
||||
skipProjectConfiguration,
|
||||
waitForBuildEnd,
|
||||
@@ -79,7 +87,7 @@ export interface ConfigureContext {
|
||||
|
||||
type PlatformBuildProfile<T extends Platform> = T extends Platform.ANDROID
|
||||
? AndroidBuildProfile
|
||||
: iOSBuildProfile;
|
||||
: IosBuildProfile;
|
||||
|
||||
export interface BuildContext<T extends Platform> {
|
||||
commandCtx: CommandContext;
|
||||
|
||||
@@ -19,6 +19,9 @@ export async function buildAsync(commandCtx: CommandContext): Promise<void> {
|
||||
await ensureGitStatusIsCleanAsync(commandCtx.nonInteractive);
|
||||
|
||||
const scheduledBuilds = await startBuildsAsync(commandCtx);
|
||||
if (commandCtx.local) {
|
||||
return;
|
||||
}
|
||||
Log.newLine();
|
||||
printLogsUrls(commandCtx.accountName, scheduledBuilds);
|
||||
Log.newLine();
|
||||
@@ -36,7 +39,7 @@ async function startBuildsAsync(
|
||||
const shouldBuildAndroid = [RequestedPlatform.Android, RequestedPlatform.All].includes(
|
||||
commandCtx.requestedPlatform
|
||||
);
|
||||
const shouldBuildiOS = [RequestedPlatform.iOS, RequestedPlatform.All].includes(
|
||||
const shouldBuildIos = [RequestedPlatform.Ios, RequestedPlatform.All].includes(
|
||||
commandCtx.requestedPlatform
|
||||
);
|
||||
const easConfig = await new EasJsonReader(
|
||||
@@ -46,22 +49,24 @@ async function startBuildsAsync(
|
||||
|
||||
const builds: {
|
||||
platform: Platform;
|
||||
sendBuildRequestAsync: () => Promise<string>;
|
||||
sendBuildRequestAsync: () => Promise<string | undefined>;
|
||||
}[] = [];
|
||||
if (shouldBuildAndroid) {
|
||||
const sendBuildRequestAsync = await prepareAndroidBuildAsync(commandCtx, easConfig);
|
||||
builds.push({ platform: Platform.ANDROID, sendBuildRequestAsync });
|
||||
}
|
||||
if (shouldBuildiOS) {
|
||||
if (shouldBuildIos) {
|
||||
const sendBuildRequestAsync = await prepareIosBuildAsync(commandCtx, easConfig);
|
||||
builds.push({ platform: Platform.IOS, sendBuildRequestAsync });
|
||||
}
|
||||
return Promise.all(
|
||||
builds.map(async ({ platform, sendBuildRequestAsync }) => ({
|
||||
platform,
|
||||
buildId: await sendBuildRequestAsync(),
|
||||
}))
|
||||
);
|
||||
return (
|
||||
await Promise.all(
|
||||
builds.map(async ({ platform, sendBuildRequestAsync }) => ({
|
||||
platform,
|
||||
buildId: await sendBuildRequestAsync(),
|
||||
}))
|
||||
)
|
||||
).filter((build): build is { platform: Platform; buildId: string } => !!build.buildId);
|
||||
}
|
||||
|
||||
async function waitForBuildEndAsync(
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { CredentialsProvider } from '../credentials/CredentialsProvider';
|
||||
import Log from '../log';
|
||||
import { confirmAsync, promptAsync } from '../prompts';
|
||||
import { requestedPlatformDisplayNames } from './constants';
|
||||
|
||||
function logCredentials(target: 'local' | 'remote', platform: Platform) {
|
||||
let message = `Using ${target} ${requestedPlatformDisplayNames[platform]} credentials`;
|
||||
if (target === 'local') message += ` ${chalk.dim('(credentials.json)')}`;
|
||||
if (target === 'remote') message += ` ${chalk.dim('(Expo server)')}`;
|
||||
Log.succeed(message);
|
||||
}
|
||||
|
||||
async function ensureCredentialsAutoAsync(
|
||||
provider: CredentialsProvider,
|
||||
workflow: Workflow,
|
||||
nonInteractive: boolean
|
||||
): Promise<CredentialsSource.LOCAL | CredentialsSource.REMOTE> {
|
||||
const platform = requestedPlatformDisplayNames[provider.platform];
|
||||
switch (workflow) {
|
||||
case Workflow.MANAGED:
|
||||
if (await provider.hasLocalAsync()) {
|
||||
return CredentialsSource.LOCAL;
|
||||
} else {
|
||||
return CredentialsSource.REMOTE;
|
||||
}
|
||||
case Workflow.GENERIC: {
|
||||
const hasLocal = await provider.hasLocalAsync();
|
||||
const hasRemote = await provider.hasRemoteAsync();
|
||||
if (hasRemote && hasLocal) {
|
||||
if (!(await provider.isLocalSyncedAsync())) {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
`Contents of your local credentials.json for ${platform} are not the same as credentials on EAS servers. To use the desired credentials, set the "builds.${platform}.{profile}.credentialsSource" field in the credentials.json file to one of the following: "local", "remote".`
|
||||
);
|
||||
} else {
|
||||
Log.log(
|
||||
`Contents of your local credentials.json for ${platform} are not the same as credentials on EAS servers`
|
||||
);
|
||||
}
|
||||
|
||||
const { select } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'select',
|
||||
message: 'Which credentials do you want to use for this build?',
|
||||
choices: [
|
||||
{ title: 'Local credentials.json', value: CredentialsSource.LOCAL },
|
||||
{ title: 'Credentials stored on EAS servers.', value: CredentialsSource.REMOTE },
|
||||
],
|
||||
});
|
||||
return select;
|
||||
} else {
|
||||
return CredentialsSource.LOCAL;
|
||||
}
|
||||
} else if (hasLocal) {
|
||||
logCredentials('local', provider.platform);
|
||||
return CredentialsSource.LOCAL;
|
||||
} else if (hasRemote) {
|
||||
logCredentials('remote', provider.platform);
|
||||
return CredentialsSource.REMOTE;
|
||||
} else {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
`Credentials for this app are not configured and there is no entry in credentials.json for ${platform}. Either configure credentials.json, or launch the build without "--non-interactive" flag to get a prompt to generate credentials automatically.`
|
||||
);
|
||||
} else if (Log.isDebug) {
|
||||
Log.warn(
|
||||
`Credentials for this app are not configured and there is no entry in credentials.json for ${platform}`
|
||||
);
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: `${platform} credentials not found, generate now?`,
|
||||
});
|
||||
if (confirm) {
|
||||
return CredentialsSource.REMOTE;
|
||||
} else {
|
||||
throw new Error(`Aborting build process, credentials are not configured for ${platform}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureCredentialsAsync(
|
||||
provider: CredentialsProvider,
|
||||
workflow: Workflow,
|
||||
src: CredentialsSource,
|
||||
nonInteractive: boolean
|
||||
): Promise<CredentialsSource.LOCAL | CredentialsSource.REMOTE> {
|
||||
switch (src) {
|
||||
case CredentialsSource.LOCAL:
|
||||
logCredentials('local', provider.platform);
|
||||
return CredentialsSource.LOCAL;
|
||||
case CredentialsSource.REMOTE:
|
||||
logCredentials('remote', provider.platform);
|
||||
return CredentialsSource.REMOTE;
|
||||
case CredentialsSource.AUTO:
|
||||
return await ensureCredentialsAutoAsync(provider, workflow, nonInteractive);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
@@ -38,9 +39,11 @@ describe(bumpVersionAsync, () => {
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = await readPlistAsync('/repo/ios/myproject/Info.plist');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios.buildNumber).toBe('2');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('2');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
|
||||
@@ -57,9 +60,11 @@ describe(bumpVersionAsync, () => {
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = await readPlistAsync('/repo/ios/myproject/Info.plist');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.1');
|
||||
expect(fakeExp.ios.buildNumber).toBe('1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.1');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.1');
|
||||
@@ -76,9 +81,11 @@ describe(bumpVersionAsync, () => {
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = await readPlistAsync('/repo/ios/myproject/Info.plist');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios.buildNumber).toBe('1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
|
||||
@@ -99,7 +106,7 @@ describe(bumpVersionInAppJsonAsync, () => {
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios.buildNumber).toBe('2');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('2');
|
||||
});
|
||||
@@ -115,7 +122,7 @@ describe(bumpVersionInAppJsonAsync, () => {
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.1');
|
||||
expect(fakeExp.ios.buildNumber).toBe('1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.1');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
});
|
||||
@@ -131,7 +138,7 @@ describe(bumpVersionInAppJsonAsync, () => {
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios.buildNumber).toBe('1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ import { prepareJobAsync, sanitizedTargetName } from './prepareJob';
|
||||
export async function prepareIosBuildAsync(
|
||||
commandCtx: CommandContext,
|
||||
easConfig: EasConfig
|
||||
): Promise<() => Promise<string>> {
|
||||
): Promise<() => Promise<string | undefined>> {
|
||||
const buildCtx = createBuildContext<Platform.IOS>({
|
||||
commandCtx,
|
||||
platform: Platform.IOS,
|
||||
|
||||
@@ -3,9 +3,9 @@ import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import {
|
||||
EasJsonReader,
|
||||
IosBuildProfile,
|
||||
IosGenericBuildProfile,
|
||||
VersionAutoIncrement,
|
||||
iOSBuildProfile,
|
||||
iOSGenericBuildProfile,
|
||||
} from '@expo/eas-json';
|
||||
|
||||
import Log from '../../log';
|
||||
@@ -44,13 +44,13 @@ export async function validateAndSyncProjectConfigurationAsync({
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
buildProfile: iOSBuildProfile;
|
||||
buildProfile: IosBuildProfile;
|
||||
}): Promise<void> {
|
||||
const { workflow, autoIncrement } = buildProfile;
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement);
|
||||
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const genericBuildProfile = buildProfile as iOSGenericBuildProfile;
|
||||
const genericBuildProfile = buildProfile as IosGenericBuildProfile;
|
||||
if (!genericBuildProfile.disableIosBundleIdentifierValidation) {
|
||||
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
|
||||
projectDir
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { CredentialsSource, iOSDistributionType } from '@expo/eas-json';
|
||||
import { CredentialsSource, IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
|
||||
import { createCredentialsContextAsync } from '../../credentials/context';
|
||||
import IosCredentialsProvider, {
|
||||
IosCredentials,
|
||||
} from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { getAppLookupParamsFromContext } from '../../credentials/ios/actions/new/BuildCredentialsUtils';
|
||||
import { AppLookupParams } from '../../credentials/ios/credentials';
|
||||
import { CredentialsResult } from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { ensureCredentialsAsync } from '../credentials';
|
||||
import { Platform } from '../types';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
|
||||
export async function ensureIosCredentialsAsync(
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
@@ -28,6 +29,7 @@ export async function ensureIosCredentialsAsync(
|
||||
workflow: ctx.buildProfile.workflow,
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
distribution: ctx.buildProfile.distribution ?? 'store',
|
||||
enterpriseProvisioning: ctx.buildProfile.enterpriseProvisioning,
|
||||
nonInteractive: ctx.commandCtx.nonInteractive,
|
||||
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
|
||||
});
|
||||
@@ -37,7 +39,8 @@ interface ResolveCredentialsParams {
|
||||
app: AppLookupParams;
|
||||
workflow: Workflow;
|
||||
credentialsSource: CredentialsSource;
|
||||
distribution: iOSDistributionType;
|
||||
distribution: IosDistributionType;
|
||||
enterpriseProvisioning?: IosEnterpriseProvisioning;
|
||||
nonInteractive: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
}
|
||||
@@ -46,22 +49,18 @@ export async function resolveIosCredentialsAsync(
|
||||
projectDir: string,
|
||||
params: ResolveCredentialsParams
|
||||
): Promise<CredentialsResult<IosCredentials>> {
|
||||
const provider = new IosCredentialsProvider(
|
||||
await createCredentialsContextAsync(projectDir, {
|
||||
nonInteractive: params.nonInteractive,
|
||||
}),
|
||||
{
|
||||
app: params.app,
|
||||
distribution: params.distribution,
|
||||
skipCredentialsCheck: params.skipCredentialsCheck,
|
||||
}
|
||||
);
|
||||
const credentialsSource = await ensureCredentialsAsync(
|
||||
provider,
|
||||
params.workflow,
|
||||
params.credentialsSource,
|
||||
params.nonInteractive
|
||||
);
|
||||
const ctx = await createCredentialsContextAsync(projectDir, {
|
||||
nonInteractive: params.nonInteractive,
|
||||
});
|
||||
const app = getAppLookupParamsFromContext(ctx);
|
||||
const provider = new IosCredentialsProvider(ctx, {
|
||||
app,
|
||||
distribution: params.distribution,
|
||||
enterpriseProvisioning: params.enterpriseProvisioning,
|
||||
skipCredentialsCheck: params.skipCredentialsCheck,
|
||||
});
|
||||
const { credentialsSource } = params;
|
||||
logCredentialsSource(credentialsSource, Platform.IOS);
|
||||
return {
|
||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
||||
source: credentialsSource,
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import {
|
||||
ArchiveSource,
|
||||
ArchiveSourceType,
|
||||
Cache,
|
||||
Ios,
|
||||
Job,
|
||||
Workflow,
|
||||
sanitizeJob,
|
||||
} from '@expo/eas-build-job';
|
||||
import { iOSGenericBuildProfile, iOSManagedBuildProfile } from '@expo/eas-json';
|
||||
import { ArchiveSource, Cache, Ios, Job, Workflow, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { IosGenericBuildProfile, IosManagedBuildProfile } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import path from 'path';
|
||||
|
||||
@@ -17,13 +9,14 @@ import {
|
||||
isCredentialsMap,
|
||||
readEnvironmentSecretsAsync,
|
||||
} from '../../credentials/credentialsJson/read';
|
||||
import { getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { gitRootDirectoryAsync } from '../../utils/git';
|
||||
import { BuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
|
||||
interface JobData {
|
||||
archiveBucketKey: string;
|
||||
projectArchive: ArchiveSource;
|
||||
credentials?: IosCredentials;
|
||||
projectConfiguration: {
|
||||
iosBuildScheme?: string;
|
||||
@@ -62,10 +55,10 @@ interface CommonJobProperties {
|
||||
async function prepareJobCommonAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
{
|
||||
archiveBucketKey,
|
||||
credentials,
|
||||
targetName,
|
||||
}: { archiveBucketKey: string; credentials?: IosCredentials; targetName?: string }
|
||||
projectArchive,
|
||||
}: { credentials?: IosCredentials; targetName?: string; projectArchive: ArchiveSource }
|
||||
): Promise<Partial<CommonJobProperties>> {
|
||||
const environmentSecrets = await readEnvironmentSecretsAsync(ctx.commandCtx.projectDir);
|
||||
|
||||
@@ -87,10 +80,7 @@ async function prepareJobCommonAsync(
|
||||
|
||||
return {
|
||||
platform: Platform.IOS,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: archiveBucketKey,
|
||||
},
|
||||
projectArchive,
|
||||
distribution: ctx.buildProfile.distribution,
|
||||
builderEnvironment: {
|
||||
image: ctx.buildProfile.image,
|
||||
@@ -101,7 +91,10 @@ async function prepareJobCommonAsync(
|
||||
fastlane: ctx.buildProfile.fastlane,
|
||||
env: ctx.buildProfile.env,
|
||||
},
|
||||
cache: ctx.buildProfile.cache,
|
||||
cache: {
|
||||
...ctx.buildProfile.cache,
|
||||
clear: ctx.commandCtx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
...(environmentSecrets ? { environmentSecrets } : {}),
|
||||
buildCredentials,
|
||||
@@ -122,15 +115,15 @@ function prepareTargetCredentials(targetCredentials: IosTargetCredentials): Ios.
|
||||
async function prepareGenericJobAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData,
|
||||
buildProfile: iOSGenericBuildProfile
|
||||
buildProfile: IosGenericBuildProfile
|
||||
): Promise<Partial<Ios.GenericJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, {
|
||||
archiveBucketKey: jobData.archiveBucketKey,
|
||||
credentials: jobData.credentials,
|
||||
targetName: jobData.projectConfiguration.iosApplicationTarget,
|
||||
projectArchive: jobData.projectArchive,
|
||||
})),
|
||||
type: Workflow.GENERIC,
|
||||
scheme: jobData.projectConfiguration.iosBuildScheme,
|
||||
@@ -147,20 +140,20 @@ async function prepareGenericJobAsync(
|
||||
async function prepareManagedJobAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData,
|
||||
buildProfile: iOSManagedBuildProfile
|
||||
buildProfile: IosManagedBuildProfile
|
||||
): Promise<Partial<Ios.ManagedJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.exp);
|
||||
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, {
|
||||
archiveBucketKey: jobData.archiveBucketKey,
|
||||
credentials: jobData.credentials,
|
||||
targetName: jobData.projectConfiguration.iosApplicationTarget,
|
||||
projectArchive: jobData.projectArchive,
|
||||
})),
|
||||
type: Workflow.MANAGED,
|
||||
buildType: buildProfile.buildType,
|
||||
username: accountName,
|
||||
username,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
projectRootDirectory,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Job } from '@expo/eas-build-job';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log from '../log';
|
||||
|
||||
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
||||
|
||||
export async function runLocalBuildAsync(job: Job): Promise<void> {
|
||||
const execNameOrPath =
|
||||
process.env.EAS_LOCAL_BUILD_PLUGIN_PATH ??
|
||||
(await findWithNodeResolution()) ??
|
||||
PLUGIN_PACKAGE_NAME;
|
||||
try {
|
||||
const arg = Buffer.from(JSON.stringify({ job })).toString('base64');
|
||||
await spawnAsync(execNameOrPath, [arg], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
Log.warn(`Could not resolve executable ${execNameOrPath}.`);
|
||||
Log.warn(
|
||||
`Install ${PLUGIN_PACKAGE_NAME} package from npm e.g. ${chalk.bold(
|
||||
`npm install -g ${PLUGIN_PACKAGE_NAME}`
|
||||
)} and make sure it's in PATH.`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function findWithNodeResolution(): Promise<string | undefined> {
|
||||
try {
|
||||
const resolvedPath = path.resolve(
|
||||
path.dirname(require.resolve(PLUGIN_PACKAGE_NAME)),
|
||||
'../bin/run'
|
||||
);
|
||||
if (await fs.pathExists(resolvedPath)) {
|
||||
return resolvedPath;
|
||||
}
|
||||
} catch {}
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Metadata } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
|
||||
import { getAppIdentifierAsync } from '../project/projectUtils';
|
||||
import { getAppIdentifierAsync, getUsername } from '../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { gitCommitHashAsync } from '../utils/git';
|
||||
import { readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync } from './android/UpdatesModule';
|
||||
import { BuildContext } from './context';
|
||||
@@ -42,6 +43,7 @@ export async function collectMetadata<T extends Platform>(
|
||||
})) ?? undefined,
|
||||
buildProfile: ctx.commandCtx.profile,
|
||||
gitCommitHash: await gitCommitHashAsync(),
|
||||
username: getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync()),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ export { Platform } from '@expo/eas-build-job';
|
||||
|
||||
export enum RequestedPlatform {
|
||||
Android = 'android',
|
||||
iOS = 'ios',
|
||||
Ios = 'ios',
|
||||
All = 'all',
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../log';
|
||||
import { requestedPlatformDisplayNames } from '../constants';
|
||||
|
||||
export function logCredentialsSource(credentialsSource: CredentialsSource, platform: Platform) {
|
||||
let message = `Using ${credentialsSource} ${requestedPlatformDisplayNames[platform]} credentials`;
|
||||
if (credentialsSource === CredentialsSource.LOCAL)
|
||||
message += ` ${chalk.dim('(credentials.json)')}`;
|
||||
else if (credentialsSource === CredentialsSource.REMOTE)
|
||||
message += ` ${chalk.dim('(Expo server)')}`;
|
||||
Log.succeed(message);
|
||||
}
|
||||
@@ -44,7 +44,7 @@ async function getBranchInfoAsync({
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const branchId = data.app?.byId.updateBranchByName.id;
|
||||
const branchId = data.app?.byId.updateBranchByName?.id;
|
||||
if (!branchId) {
|
||||
throw new Error(`Could not find branch ${name} on ${appId}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import CliTable from 'cli-table3';
|
||||
import gql from 'graphql-tag';
|
||||
import { format } from 'timeago.js';
|
||||
@@ -17,7 +18,13 @@ import Log from '../../log';
|
||||
import { findProjectRootAsync, getProjectFullNameAsync } from '../../project/projectUtils';
|
||||
import { getActorDisplayName } from '../../user/User';
|
||||
|
||||
export type FormatUpdateParameter = Pick<Update, 'id' | 'createdAt' | 'message'> & {
|
||||
actor?: Maybe<Pick<User, 'username' | 'id'> | Pick<Robot, 'firstName' | 'id'>>;
|
||||
};
|
||||
|
||||
export const UPDATE_COLUMNS = ['update description', 'update runtime version', 'update group ID'];
|
||||
const BRANCHES_LIMIT = 10_000;
|
||||
|
||||
export async function listBranchesAsync({ fullName }: { fullName: string }) {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
@@ -43,8 +50,10 @@ export async function listBranchesAsync({ fullName }: { fullName: string }) {
|
||||
firstName
|
||||
}
|
||||
}
|
||||
updatedAt
|
||||
createdAt
|
||||
message
|
||||
runtimeVersion
|
||||
group
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,8 +96,19 @@ export default class BranchList extends Command {
|
||||
if (flags.json) {
|
||||
Log.log(JSON.stringify(branches, null, 2));
|
||||
} else {
|
||||
const table = new CliTable({ head: ['Branch', 'Latest update'] });
|
||||
table.push(...branches.map(branch => [branch.name, formatUpdate(branch.updates[0])]));
|
||||
const table = new CliTable({
|
||||
head: ['branch', ...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
table.push(
|
||||
...branches.map(branch => [
|
||||
branch.name,
|
||||
formatUpdate(branch.updates[0]),
|
||||
branch.updates[0]?.runtimeVersion ?? 'N/A',
|
||||
branch.updates[0]?.group ?? 'N/A',
|
||||
])
|
||||
);
|
||||
Log.log(chalk.bold('Branches with their most recent update group:'));
|
||||
Log.log(table.toString());
|
||||
if (branches.length >= BRANCHES_LIMIT) {
|
||||
Log.warn(`Showing first ${BRANCHES_LIMIT} branches, some results might be omitted.`);
|
||||
@@ -97,16 +117,12 @@ export default class BranchList extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
export function formatUpdate(
|
||||
update: Pick<Update, 'id' | 'updatedAt' | 'message'> & {
|
||||
actor?: Maybe<Pick<User, 'username' | 'id'> | Pick<Robot, 'firstName' | 'id'>>;
|
||||
}
|
||||
): string {
|
||||
export function formatUpdate(update: FormatUpdateParameter): string {
|
||||
if (!update) {
|
||||
return 'N/A';
|
||||
}
|
||||
const message = update.message ? `"${update.message}" ` : '';
|
||||
return `${message}(${format(update.updatedAt, 'en_US')} by ${getActorDisplayName(
|
||||
return `${message}(${format(update.createdAt, 'en_US')} by ${getActorDisplayName(
|
||||
update.actor as any
|
||||
)})`;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { getConfig, getDefaultTarget } from '@expo/config';
|
||||
import { getRuntimeVersionForSDKVersion } from '@expo/sdk-runtime-versions';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
@@ -64,10 +65,15 @@ export default class BranchPublish extends Command {
|
||||
static hidden = true;
|
||||
static description = 'Publish an update group to a branch.';
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'name',
|
||||
required: true,
|
||||
description: 'Name of the branch to publish on',
|
||||
},
|
||||
];
|
||||
|
||||
static flags = {
|
||||
branch: flags.string({
|
||||
description: 'name of the branch to publish on.',
|
||||
}),
|
||||
message: flags.string({
|
||||
description: 'Short message describing the updates.',
|
||||
required: false,
|
||||
@@ -97,9 +103,9 @@ export default class BranchPublish extends Command {
|
||||
|
||||
async run() {
|
||||
let {
|
||||
args: { name },
|
||||
flags: {
|
||||
json: jsonFlag,
|
||||
branch: name,
|
||||
message,
|
||||
republish,
|
||||
group,
|
||||
@@ -117,7 +123,17 @@ export default class BranchPublish extends Command {
|
||||
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug, runtimeVersion } = exp;
|
||||
let { slug, runtimeVersion, sdkVersion } = exp;
|
||||
|
||||
// When a SDK version is supplied instead of a runtime version and we're in the managed workflow
|
||||
// construct the runtimeVersion with special meaning indicating that the runtime is an
|
||||
// Expo SDK preset runtime that can be launched in Expo Go.
|
||||
const isManagedProject = getDefaultTarget(projectDir) === 'managed';
|
||||
if (!runtimeVersion && sdkVersion && isManagedProject) {
|
||||
Log.withTick('Generating runtime version from sdk version');
|
||||
runtimeVersion = getRuntimeVersionForSDKVersion(sdkVersion);
|
||||
}
|
||||
|
||||
if (!runtimeVersion) {
|
||||
throw new Error(
|
||||
"Couldn't find 'runtimeVersion'. Please specify it under the 'expo' key in 'app.json'"
|
||||
|
||||
@@ -19,6 +19,7 @@ import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { UPDATE_COLUMNS, formatUpdate } from './list';
|
||||
|
||||
const PAGE_LIMIT = 10_000;
|
||||
|
||||
@@ -59,7 +60,7 @@ export async function viewUpdateBranchAsync({
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
firstName
|
||||
username
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
@@ -152,17 +153,12 @@ export default class BranchView extends Command {
|
||||
}
|
||||
|
||||
const groupTable = new Table({
|
||||
head: ['createdAt', 'message', 'group', 'actor'],
|
||||
head: UPDATE_COLUMNS,
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const update of updates) {
|
||||
groupTable.push([
|
||||
new Date(update.createdAt).toLocaleString(),
|
||||
update.message,
|
||||
update.group,
|
||||
update.actor?.firstName,
|
||||
]);
|
||||
groupTable.push([formatUpdate(update), update.runtimeVersion, update.group]);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
|
||||
@@ -87,7 +87,7 @@ async function promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
},
|
||||
{
|
||||
title: 'iOS',
|
||||
value: RequestedPlatform.iOS,
|
||||
value: RequestedPlatform.Ios,
|
||||
},
|
||||
{
|
||||
title: 'Android',
|
||||
|
||||
@@ -52,11 +52,19 @@ export default class Build extends Command {
|
||||
default: false,
|
||||
description: 'Run command in --non-interactive mode',
|
||||
}),
|
||||
local: flags.boolean({
|
||||
default: false,
|
||||
description: 'Run build locally [experimental]',
|
||||
}),
|
||||
wait: flags.boolean({
|
||||
default: true,
|
||||
allowNo: true,
|
||||
description: 'Wait for build(s) to complete',
|
||||
}),
|
||||
'clear-cache': flags.boolean({
|
||||
default: false,
|
||||
description: 'Clear cache before the build',
|
||||
}),
|
||||
};
|
||||
|
||||
async run(): Promise<void> {
|
||||
@@ -76,12 +84,17 @@ export default class Build extends Command {
|
||||
let { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
if (!flags.local && !(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags.local && !verifyOptionsForLocalBuilds(platform)) {
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const accountHasPendingBuilds = await ensureNoPendingBuildsExistAsync({
|
||||
accountName,
|
||||
@@ -102,7 +115,9 @@ export default class Build extends Command {
|
||||
projectDir,
|
||||
projectId,
|
||||
nonInteractive,
|
||||
clearCache: flags['clear-cache'],
|
||||
skipCredentialsCheck: flags['skip-credentials-check'],
|
||||
local: flags.local,
|
||||
skipProjectConfiguration: flags['skip-project-configuration'],
|
||||
waitForBuildEnd: flags.wait,
|
||||
});
|
||||
@@ -156,6 +171,18 @@ async function ensureNoPendingBuildsExistAsync({
|
||||
return false;
|
||||
}
|
||||
|
||||
function verifyOptionsForLocalBuilds(platform: RequestedPlatform): boolean {
|
||||
if (platform === RequestedPlatform.All) {
|
||||
Log.error('Builds for multiple platforms are not supported with flag --local');
|
||||
return false;
|
||||
} else if (process.platform !== 'darwin' && platform === RequestedPlatform.Ios) {
|
||||
Log.error('Unsupported platform, macOS is required to build apps for iOS');
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function toAppPlatforms(requestedPlatform: RequestedPlatform): AppPlatform[] {
|
||||
if (requestedPlatform === RequestedPlatform.All) {
|
||||
return [AppPlatform.Android, AppPlatform.Ios];
|
||||
@@ -178,7 +205,7 @@ async function promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
},
|
||||
{
|
||||
title: 'iOS',
|
||||
value: RequestedPlatform.iOS,
|
||||
value: RequestedPlatform.Ios,
|
||||
},
|
||||
{
|
||||
title: 'Android',
|
||||
|
||||
@@ -19,7 +19,7 @@ export default class BuildList extends Command {
|
||||
|
||||
static flags = {
|
||||
platform: flags.enum({
|
||||
options: [RequestedPlatform.All, RequestedPlatform.Android, RequestedPlatform.iOS],
|
||||
options: [RequestedPlatform.All, RequestedPlatform.Android, RequestedPlatform.Ios],
|
||||
}),
|
||||
status: flags.enum({
|
||||
options: [
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { logChannelDetails } from './view';
|
||||
|
||||
const CHANNEL_LIMIT = 10_000;
|
||||
|
||||
@@ -31,18 +31,20 @@ async function getAllUpdateChannelForAppAsync({
|
||||
updateChannels(offset: $offset, limit: $limit) {
|
||||
id
|
||||
name
|
||||
updateBranches(offset: 0, limit: 1) {
|
||||
branchMapping
|
||||
updateBranches(offset: 0, limit: $limit) {
|
||||
id
|
||||
name
|
||||
updates(offset: 0, limit: 1) {
|
||||
id
|
||||
group
|
||||
message
|
||||
runtimeVersion
|
||||
createdAt
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
firstName
|
||||
username
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
@@ -102,27 +104,10 @@ export default class ChannelList extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
const table = new Table({
|
||||
head: ['channel', 'branch', 'update', 'message', 'created-at', 'actor'],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
Log.log(); // spacing
|
||||
for (const channel of channels) {
|
||||
// TODO (cedric): refactor when multiple branches per channel are available
|
||||
const branch = channel.updateBranches[0];
|
||||
const update = branch.updates[0];
|
||||
|
||||
table.push([
|
||||
channel.name,
|
||||
branch.name,
|
||||
update?.group,
|
||||
update?.message,
|
||||
update?.createdAt && new Date(update.createdAt).toLocaleString(),
|
||||
update?.actor?.firstName,
|
||||
]);
|
||||
Log.log(`Details of channel ${chalk.bold(chalk.cyan(channel.name))}:`);
|
||||
logChannelDetails(channel);
|
||||
}
|
||||
|
||||
Log.log(chalk`{bold Channels for this app:}`);
|
||||
Log.log(table.toString());
|
||||
}
|
||||
}
|
||||
@@ -44,18 +44,20 @@ function getRolloutInfo(
|
||||
oldBranch: Pick<UpdateBranch, 'name' | 'id'>;
|
||||
currentPercent: number;
|
||||
} {
|
||||
const { branchMapping } = getBranchMapping(getUpdateChannelByNameForAppResult);
|
||||
const { branchMapping } = getBranchMapping(
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.branchMapping
|
||||
);
|
||||
const [newBranchId, oldBranchId] = branchMapping.data.map(d => d.branchId);
|
||||
const newBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.updateBranches.filter(
|
||||
const newBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === newBranchId
|
||||
)[0];
|
||||
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.updateBranches.filter(
|
||||
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === oldBranchId
|
||||
)[0];
|
||||
|
||||
if (!newBranch || !oldBranch) {
|
||||
throw new Error(
|
||||
`Branch mapping rollout is missing a branch for channel "${getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.name}".`
|
||||
`Branch mapping rollout is missing a branch for channel "${getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.name}".`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,11 +136,11 @@ async function startRolloutAsync({
|
||||
],
|
||||
};
|
||||
const newChannelInfo = await updateChannelBranchMappingAsync({
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.id!,
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.id!,
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
|
||||
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.updateBranches.filter(
|
||||
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === oldBranchId
|
||||
)[0];
|
||||
if (!oldBranch) {
|
||||
@@ -200,7 +202,7 @@ async function editRolloutAsync({
|
||||
newBranchMapping.data[0].branchMappingLogic.operand = percent / 100;
|
||||
|
||||
const newChannelInfo = await updateChannelBranchMappingAsync({
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.id!,
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.id!,
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
|
||||
@@ -290,7 +292,7 @@ async function endRolloutAsync({
|
||||
};
|
||||
|
||||
const newChannelInfo = await updateChannelBranchMappingAsync({
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.id!,
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.id!,
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
const logMessage = `️Rollout on channel ${chalk.bold(
|
||||
@@ -359,7 +361,7 @@ export default class ChannelRollout extends Command {
|
||||
channelName: channelName!,
|
||||
});
|
||||
const { branchMapping: currentBranchMapping, isRollout } = getBranchMapping(
|
||||
getUpdateChannelByNameForAppResult
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.branchMapping
|
||||
);
|
||||
|
||||
if (currentBranchMapping.data.length === 0) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import { assert } from 'console';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
@@ -13,6 +14,7 @@ import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { FormatUpdateParameter, UPDATE_COLUMNS, formatUpdate } from '../branch/list';
|
||||
|
||||
export type BranchMapping = {
|
||||
version: number;
|
||||
@@ -31,15 +33,19 @@ export type BranchMapping = {
|
||||
* Ensure that the branch mapping is properly formatted.
|
||||
*/
|
||||
export function getBranchMapping(
|
||||
getChannelByNameForAppQuery: GetChannelByNameForAppQuery
|
||||
branchMappingString?: string
|
||||
): { branchMapping: BranchMapping; isRollout: boolean; rolloutPercent?: number } {
|
||||
const branchMappingString =
|
||||
getChannelByNameForAppQuery.app?.byId.updateChannelByName.branchMapping;
|
||||
if (!branchMappingString) {
|
||||
throw new Error('Missing branch mapping.');
|
||||
}
|
||||
let branchMapping: BranchMapping;
|
||||
try {
|
||||
branchMapping = JSON.parse(branchMappingString);
|
||||
} catch (e) {
|
||||
throw new Error(`Could not parse branchMapping string into a JSON: "${branchMappingString}"`);
|
||||
}
|
||||
assert(branchMapping, 'Branch Mapping must be defined.');
|
||||
|
||||
const branchMapping: BranchMapping = JSON.parse(branchMappingString);
|
||||
if (branchMapping.version !== 0) {
|
||||
throw new Error('Branch mapping must be version 0.');
|
||||
}
|
||||
@@ -100,11 +106,12 @@ export async function getUpdateChannelByNameForAppAsync({
|
||||
id
|
||||
group
|
||||
message
|
||||
runtimeVersion
|
||||
createdAt
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
firstName
|
||||
username
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
@@ -123,6 +130,49 @@ export async function getUpdateChannelByNameForAppAsync({
|
||||
);
|
||||
}
|
||||
|
||||
export function logChannelDetails(channel: {
|
||||
branchMapping: string;
|
||||
updateBranches: {
|
||||
updates: (FormatUpdateParameter & { runtimeVersion?: string; group?: string })[];
|
||||
name: string;
|
||||
id: string;
|
||||
}[];
|
||||
}): void {
|
||||
const { branchMapping, isRollout, rolloutPercent } = getBranchMapping(channel.branchMapping);
|
||||
|
||||
const table = new Table({
|
||||
head: ['branch', ...(isRollout ? ['rollout percent'] : []), ...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const index in branchMapping.data) {
|
||||
if (parseInt(index, 10) > 1) {
|
||||
throw new Error('Branch Mapping data must have length less than or equal to 2.');
|
||||
}
|
||||
|
||||
const { branchId } = branchMapping.data[index];
|
||||
const branch = channel.updateBranches.filter(branch => branch.id === branchId)[0];
|
||||
if (!branch) {
|
||||
throw new Error('Branch mapping is pointing at a missing branch.');
|
||||
}
|
||||
const update = branch.updates[0];
|
||||
table.push([
|
||||
branch.name,
|
||||
...(isRollout
|
||||
? [
|
||||
parseInt(index, 10) === 0
|
||||
? `${rolloutPercent! * 100}%`
|
||||
: `${(1 - rolloutPercent!) * 100}%`,
|
||||
]
|
||||
: []),
|
||||
formatUpdate(update),
|
||||
update?.runtimeVersion ?? 'N/A',
|
||||
update?.group ?? 'N/A,',
|
||||
]);
|
||||
}
|
||||
Log.log(table.toString());
|
||||
}
|
||||
|
||||
export default class ChannelView extends Command {
|
||||
static hidden = true;
|
||||
static description = 'View a channel on the current project.';
|
||||
@@ -187,53 +237,12 @@ export default class ChannelView extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
const { branchMapping, isRollout, rolloutPercent } = getBranchMapping(
|
||||
getUpdateChannelByNameForAppresult
|
||||
);
|
||||
|
||||
const table = new Table({
|
||||
head: [
|
||||
'branch',
|
||||
...(isRollout ? ['rollout percent'] : []),
|
||||
'active update group',
|
||||
'update message',
|
||||
'last publish',
|
||||
'actor',
|
||||
],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const index in branchMapping.data) {
|
||||
if (parseInt(index, 10) > 1) {
|
||||
throw new Error('Branch Mapping data must have length less than or equal to 2.');
|
||||
}
|
||||
|
||||
const { branchId } = branchMapping.data[index];
|
||||
const branch = channel.updateBranches.filter(branch => branch.id === branchId)[0];
|
||||
if (!branch) {
|
||||
throw new Error('Branch mapping is pointing at a missing branch.');
|
||||
}
|
||||
const update = branch.updates[0];
|
||||
table.push([
|
||||
branch.name,
|
||||
...(isRollout
|
||||
? [
|
||||
parseInt(index, 10) === 0
|
||||
? `${rolloutPercent! * 100}%`
|
||||
: `${(1 - rolloutPercent!) * 100}%`,
|
||||
]
|
||||
: []),
|
||||
update?.group,
|
||||
update?.message,
|
||||
update?.createdAt && new Date(update.createdAt).toLocaleString(),
|
||||
update?.actor?.firstName,
|
||||
]);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
chalk`Channel: {bold ${channel.name}} on project {bold ${accountName}/${slug}}. Channel ID: {bold ${channel.id}}`
|
||||
);
|
||||
Log.log(chalk`{bold Recent update groups published on this branch:}`);
|
||||
Log.log(table.toString());
|
||||
Log.log(
|
||||
chalk`{bold Branches, pointed at by this channel, and their most recent update group:}`
|
||||
);
|
||||
logChannelDetails(channel);
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+11
-9
@@ -1,5 +1,5 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command } from '@oclif/command';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
@@ -26,13 +26,11 @@ export default class EnvironmentSecretDelete extends Command {
|
||||
static description = `Delete an environment secret by ID.
|
||||
Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'id',
|
||||
required: false,
|
||||
description: `ID of the secret to delete`,
|
||||
},
|
||||
];
|
||||
static flags = {
|
||||
id: flags.string({
|
||||
description: 'ID of the secret to delete',
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
await ensureLoggedInAsync();
|
||||
@@ -50,11 +48,13 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
}
|
||||
|
||||
let {
|
||||
args: { id },
|
||||
flags: { id },
|
||||
} = this.parse(EnvironmentSecretDelete);
|
||||
let secret: EnvironmentSecretWithScope | undefined;
|
||||
|
||||
if (!id) {
|
||||
const validationMessage = 'You must select which secret to delete.';
|
||||
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectFullName);
|
||||
|
||||
({ secret } = await promptAsync({
|
||||
@@ -68,6 +68,8 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
}));
|
||||
|
||||
id = secret?.id;
|
||||
|
||||
if (!id) throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
+1
-2
@@ -18,9 +18,8 @@ import {
|
||||
} from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
export default class EnvironmentSecretsList extends Command {
|
||||
export default class EnvironmentSecretList extends Command {
|
||||
static description = 'Lists environment secrets available for your current app';
|
||||
static usage = 'secrets:list';
|
||||
|
||||
async run(): Promise<void> {
|
||||
await ensureLoggedInAsync();
|
||||
@@ -1,17 +1,14 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
|
||||
import { AppPlatform } from '../graphql/generated';
|
||||
import { learnMore } from '../log';
|
||||
import { isEasEnabledForProjectAsync, warnEasUnavailable } from '../project/isEasEnabledForProject';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { promptAsync } from '../prompts';
|
||||
import AndroidSubmitCommand from '../submissions/android/AndroidSubmitCommand';
|
||||
import IosSubmitCommand from '../submissions/ios/IosSubmitCommand';
|
||||
import {
|
||||
AndroidSubmitCommandFlags,
|
||||
IosSubmitCommandFlags,
|
||||
SubmissionPlatform,
|
||||
} from '../submissions/types';
|
||||
import { AndroidSubmitCommandFlags, IosSubmitCommandFlags } from '../submissions/types';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
|
||||
const COMMON_FLAGS = '';
|
||||
@@ -159,14 +156,15 @@ export default class BuildSubmit extends Command {
|
||||
'company-name': companyName,
|
||||
|
||||
// common
|
||||
platform: platformFromParams,
|
||||
...flags
|
||||
},
|
||||
} = this.parse(BuildSubmit);
|
||||
|
||||
await ensureLoggedInAsync();
|
||||
|
||||
const platform = platformFromParams ?? (await promptForPlatformAsync());
|
||||
const platform =
|
||||
(flags.platform?.toUpperCase() as AppPlatform | undefined) ??
|
||||
(await promptForPlatformAsync());
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
@@ -183,7 +181,7 @@ export default class BuildSubmit extends Command {
|
||||
throw new Error("Please run this command inside your project's directory");
|
||||
}
|
||||
|
||||
if (platform === SubmissionPlatform.Android) {
|
||||
if (platform === AppPlatform.Android) {
|
||||
const options: AndroidSubmitCommandFlags = {
|
||||
androidPackage,
|
||||
releaseStatus,
|
||||
@@ -193,7 +191,7 @@ export default class BuildSubmit extends Command {
|
||||
const ctx = AndroidSubmitCommand.createContext(projectDir, projectId, options);
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
} else if (platform === SubmissionPlatform.iOS) {
|
||||
} else if (platform === AppPlatform.Ios) {
|
||||
const options: IosSubmitCommandFlags = {
|
||||
appleId,
|
||||
ascAppId,
|
||||
@@ -213,7 +211,7 @@ export default class BuildSubmit extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForPlatformAsync(): Promise<SubmissionPlatform> {
|
||||
async function promptForPlatformAsync(): Promise<AppPlatform> {
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Submit to platform',
|
||||
@@ -221,11 +219,11 @@ async function promptForPlatformAsync(): Promise<SubmissionPlatform> {
|
||||
choices: [
|
||||
{
|
||||
title: 'iOS',
|
||||
value: SubmissionPlatform.iOS,
|
||||
value: AppPlatform.Ios,
|
||||
},
|
||||
{
|
||||
title: 'Android',
|
||||
value: SubmissionPlatform.Android,
|
||||
value: AppPlatform.Android,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
DeleteUpdateGroupMutation,
|
||||
UpdateMutationDeleteUpdateGroupArgs,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
|
||||
async function deleteUpdateGroupAsync({
|
||||
group,
|
||||
}: {
|
||||
group: string;
|
||||
}): Promise<DeleteUpdateGroupMutation> {
|
||||
return await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<DeleteUpdateGroupMutation, UpdateMutationDeleteUpdateGroupArgs>(
|
||||
gql`
|
||||
mutation DeleteUpdateGroup($group: ID!) {
|
||||
update {
|
||||
deleteUpdateGroup(group: $group) {
|
||||
group
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ group }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
export default class UpdateDelete extends Command {
|
||||
static hidden = true;
|
||||
static description = 'Delete all the updates in an update Group.';
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'groupId',
|
||||
required: true,
|
||||
description: 'The ID of an update group to delete.',
|
||||
},
|
||||
];
|
||||
|
||||
static flags = {
|
||||
json: flags.boolean({
|
||||
description: `Return a json with the group ID of the deleted updates.`,
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const {
|
||||
args: { groupId: group },
|
||||
flags: { json: jsonFlag },
|
||||
} = this.parse(UpdateDelete);
|
||||
|
||||
if (!jsonFlag) {
|
||||
const shouldAbort = await confirmAsync({
|
||||
message:
|
||||
`🚨${chalk.red('CAUTION')}🚨\n\n` +
|
||||
`${chalk.yellow(`This will delete all of the updates in group "${group}".`)} ${chalk.red(
|
||||
'This is a permanent operation.'
|
||||
)}\n\n` +
|
||||
`If you want to revert to a previous publish, you should use 'branch:publish --republish' targeted at the last working update group instead.\n\n` +
|
||||
`An update group should only be deleted in an emergency like an accidental publish of a secret. In this case user 'branch:publish --republish' to revert to the last working update group first and then proceed with the deletion. Deleting an update group when it is the latest publish can lead to inconsistent cacheing behavior by clients.\n\n` +
|
||||
`Would you like to abort?`,
|
||||
});
|
||||
|
||||
if (shouldAbort) {
|
||||
Log.log('Aborted.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await deleteUpdateGroupAsync({ group });
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify({ group }));
|
||||
return;
|
||||
}
|
||||
|
||||
Log.withTick(`Deleted update group ${group}`);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Command } from '@oclif/command';
|
||||
|
||||
export default class UpdateShow extends Command {
|
||||
static hidden = true;
|
||||
static description = 'details about a particular revision';
|
||||
|
||||
async run() {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import Table from 'cli-table3';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import { UpdatesByGroupQuery, UpdatesByGroupQueryVariables } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { UPDATE_COLUMNS, formatUpdate } from '../branch/list';
|
||||
|
||||
export async function viewUpdateAsync({
|
||||
groupId,
|
||||
}: {
|
||||
groupId: string;
|
||||
}): Promise<UpdatesByGroupQuery> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<UpdatesByGroupQuery, UpdatesByGroupQueryVariables>(
|
||||
gql`
|
||||
query UpdatesByGroup($groupId: ID!) {
|
||||
updatesByGroup(group: $groupId) {
|
||||
id
|
||||
group
|
||||
runtimeVersion
|
||||
platform
|
||||
message
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
username
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
}
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
groupId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
if (data.updatesByGroup.length === 0) {
|
||||
throw new Error(`Could not find any updates with group ID: "${groupId}"`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
export default class UpdateView extends Command {
|
||||
static hidden = true;
|
||||
static description = 'Update group details.';
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'groupId',
|
||||
required: true,
|
||||
description: 'The ID of an update group.',
|
||||
},
|
||||
];
|
||||
|
||||
static flags = {
|
||||
json: flags.boolean({
|
||||
description: `Return a json with the updates belonging to the group.`,
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const {
|
||||
args: { groupId },
|
||||
flags: { json: jsonFlag },
|
||||
} = this.parse(UpdateView);
|
||||
|
||||
const { updatesByGroup } = await viewUpdateAsync({ groupId });
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(updatesByGroup));
|
||||
return;
|
||||
}
|
||||
|
||||
const groupTable = new Table({
|
||||
head: [...UPDATE_COLUMNS, 'platforms'],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
// The relevant info of the updates are same except for their platform
|
||||
const platforms = updatesByGroup.map(update => update.platform);
|
||||
const representativeUpdate = updatesByGroup[0];
|
||||
groupTable.push([
|
||||
formatUpdate(representativeUpdate),
|
||||
representativeUpdate.runtimeVersion,
|
||||
representativeUpdate.group,
|
||||
platforms.join(','),
|
||||
]);
|
||||
|
||||
Log.log(groupTable.toString());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
export interface CredentialsProvider {
|
||||
platform: Platform;
|
||||
hasRemoteAsync(): Promise<boolean>;
|
||||
hasLocalAsync(): Promise<boolean>;
|
||||
isLocalSyncedAsync(): Promise<boolean>;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ export const jester: Actor = {
|
||||
id: 'jester-id',
|
||||
username: 'jester',
|
||||
accounts: [{ id: 'jester-account-id', name: 'jester' }],
|
||||
isExpoAdmin: false,
|
||||
};
|
||||
|
||||
export const jester2: Actor = {
|
||||
@@ -12,6 +13,7 @@ export const jester2: Actor = {
|
||||
id: 'jester2-id',
|
||||
username: 'jester2',
|
||||
accounts: [{ id: 'jester2-account-id', name: 'jester2' }],
|
||||
isExpoAdmin: false,
|
||||
};
|
||||
|
||||
export const testUsername = jester.username;
|
||||
|
||||
@@ -14,6 +14,7 @@ export function createCtxMock(mockOverride: Record<string, any> = {}): Context {
|
||||
newIos: getNewIosApiMockWithoutCredentials(),
|
||||
android: getAndroidApiMockWithoutCredentials(),
|
||||
appStore: getAppstoreMock(),
|
||||
bestEffortAppStoreAuthenticateAsync: jest.fn(),
|
||||
ensureAppleCtx: jest.fn(),
|
||||
ensureProjectContext: jest.fn(),
|
||||
user: {
|
||||
@@ -37,5 +38,5 @@ export function createManagerMock(mockOverride: Record<string, any> = {}): Crede
|
||||
}),
|
||||
},
|
||||
mockOverride
|
||||
);
|
||||
) as any;
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
|
||||
import Log from '../../log';
|
||||
import { CredentialsManager } from '../CredentialsManager';
|
||||
import { CredentialsProvider } from '../CredentialsProvider';
|
||||
import { Context } from '../context';
|
||||
import * as credentialsJsonReader from '../credentialsJson/read';
|
||||
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
|
||||
@@ -23,7 +21,7 @@ interface Options {
|
||||
skipCredentialsCheck?: boolean;
|
||||
}
|
||||
|
||||
export default class AndroidCredentialsProvider implements CredentialsProvider {
|
||||
export default class AndroidCredentialsProvider {
|
||||
public readonly platform = Platform.ANDROID;
|
||||
|
||||
constructor(private ctx: Context, private options: Options) {}
|
||||
@@ -33,45 +31,6 @@ export default class AndroidCredentialsProvider implements CredentialsProvider {
|
||||
return `@${accountName}/${projectName}`;
|
||||
}
|
||||
|
||||
public async hasRemoteAsync(): Promise<boolean> {
|
||||
const keystore = await this.ctx.android.fetchKeystoreAsync(this.projectFullName);
|
||||
return !!keystore;
|
||||
}
|
||||
|
||||
public async hasLocalAsync(): Promise<boolean> {
|
||||
if (!(await credentialsJsonReader.fileExistsAsync(this.ctx.projectDir))) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const rawCredentialsJson = await credentialsJsonReader.readRawAsync(this.ctx.projectDir);
|
||||
return !!rawCredentialsJson?.android;
|
||||
} catch (err) {
|
||||
Log.error(err); // malformed json
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async isLocalSyncedAsync(): Promise<boolean> {
|
||||
try {
|
||||
const [remote, local] = await Promise.all([
|
||||
this.ctx.android.fetchKeystoreAsync(this.projectFullName),
|
||||
credentialsJsonReader.readAndroidCredentialsAsync(this.ctx.projectDir),
|
||||
]);
|
||||
const r = remote;
|
||||
const l = local?.keystore;
|
||||
return !!(
|
||||
r &&
|
||||
l &&
|
||||
r.keystore === l.keystore &&
|
||||
r.keystorePassword === l.keystorePassword &&
|
||||
r.keyAlias === l.keyAlias &&
|
||||
r.keyPassword === l.keyPassword
|
||||
);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async getCredentialsAsync(
|
||||
src: CredentialsSource.LOCAL | CredentialsSource.REMOTE
|
||||
): Promise<AndroidCredentials> {
|
||||
@@ -81,7 +40,6 @@ export default class AndroidCredentialsProvider implements CredentialsProvider {
|
||||
case CredentialsSource.REMOTE:
|
||||
return await this.getRemoteAsync();
|
||||
}
|
||||
throw new Error('Unknown');
|
||||
}
|
||||
|
||||
private async getRemoteAsync(): Promise<AndroidCredentials> {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { confirmAsync } from '../prompts';
|
||||
import { Actor, getActorDisplayName } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import AndroidApi from './android/api/Client';
|
||||
import iOSApi from './ios/api/Client';
|
||||
import IosApi from './ios/api/Client';
|
||||
import * as IosGraphqlClient from './ios/api/GraphqlClient';
|
||||
import AppStoreApi from './ios/appstore/AppStoreApi';
|
||||
|
||||
@@ -27,7 +27,7 @@ export interface Context {
|
||||
readonly user: Actor;
|
||||
readonly nonInteractive: boolean;
|
||||
readonly android: AndroidApi;
|
||||
readonly ios: iOSApi;
|
||||
readonly ios: IosApi;
|
||||
readonly newIos: typeof IosGraphqlClient;
|
||||
readonly appStore: AppStoreApi;
|
||||
readonly hasProjectContext: boolean;
|
||||
@@ -56,7 +56,7 @@ export async function createCredentialsContextAsync(
|
||||
|
||||
class CredentialsContext implements Context {
|
||||
public readonly android = new AndroidApi();
|
||||
public readonly ios = new iOSApi();
|
||||
public readonly ios = new IosApi();
|
||||
public readonly newIos = IosGraphqlClient;
|
||||
public readonly appStore: AppStoreApi;
|
||||
public readonly nonInteractive: boolean;
|
||||
|
||||
@@ -1,107 +1,35 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource, iOSDistributionType } from '@expo/eas-json';
|
||||
import { CredentialsSource, IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
|
||||
import { AppleTeamFragment, IosDistributionType } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { CredentialsManager } from '../CredentialsManager';
|
||||
import { CredentialsProvider } from '../CredentialsProvider';
|
||||
import { Context } from '../context';
|
||||
import * as credentialsJsonReader from '../credentialsJson/read';
|
||||
import type { IosCredentials } from '../credentialsJson/read';
|
||||
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
|
||||
import { resolveAppleTeamIfAuthenticatedAsync } from './actions/new/AppleTeamUtils';
|
||||
import { AppleTeamMissingError, MissingCredentialsNonInteractiveError } from './errors';
|
||||
import { AppLookupParams } from './api/GraphqlClient';
|
||||
import { isAdHocProfile } from './utils/provisioningProfile';
|
||||
|
||||
export { IosCredentials };
|
||||
|
||||
interface PartialIosCredentials {
|
||||
provisioningProfile?: string;
|
||||
distributionCertificate?: {
|
||||
certP12?: string;
|
||||
certPassword?: string;
|
||||
};
|
||||
}
|
||||
export type { IosCredentials };
|
||||
|
||||
interface Options {
|
||||
app: AppLookupParams;
|
||||
distribution: iOSDistributionType;
|
||||
distribution: IosDistributionType;
|
||||
enterpriseProvisioning?: IosEnterpriseProvisioning;
|
||||
skipCredentialsCheck?: boolean;
|
||||
}
|
||||
|
||||
interface AppLookupParams {
|
||||
projectName: string;
|
||||
accountName: string;
|
||||
bundleIdentifier: string;
|
||||
}
|
||||
|
||||
export default class IosCredentialsProvider implements CredentialsProvider {
|
||||
export default class IosCredentialsProvider {
|
||||
public readonly platform = Platform.IOS;
|
||||
|
||||
constructor(private ctx: Context, private options: Options) {}
|
||||
|
||||
public async hasRemoteAsync(): Promise<boolean> {
|
||||
// TODO: this is temporary
|
||||
// remove this check when we implement syncing local credentials for internal distribution
|
||||
if (this.options.distribution === 'internal' && (await this.hasLocalAsync())) {
|
||||
return false;
|
||||
}
|
||||
const { distributionCertificate, provisioningProfile } = await this.fetchRemoteAsync();
|
||||
return !!(
|
||||
distributionCertificate?.certP12 ||
|
||||
distributionCertificate?.certPassword ||
|
||||
provisioningProfile
|
||||
);
|
||||
}
|
||||
|
||||
public async hasLocalAsync(): Promise<boolean> {
|
||||
if (!(await credentialsJsonReader.fileExistsAsync(this.ctx.projectDir))) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const rawCredentialsJson = await credentialsJsonReader.readRawAsync(this.ctx.projectDir);
|
||||
return !!rawCredentialsJson?.ios;
|
||||
} catch (err) {
|
||||
Log.error(err); // malformed json
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async isLocalSyncedAsync(): Promise<boolean> {
|
||||
try {
|
||||
const [remote, local] = await Promise.all([this.fetchRemoteAsync(), this.getLocalAsync()]);
|
||||
const r = remote;
|
||||
const l = local;
|
||||
|
||||
// TODO
|
||||
// For now, when credentials.json contains credentials for multi-target project
|
||||
// assume they are synced with the Expo servers.
|
||||
// Change this behavior when we figure out how to store multi-target project credentials in the db.
|
||||
if (credentialsJsonReader.isCredentialsMap(l)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !!(
|
||||
r &&
|
||||
l &&
|
||||
r.provisioningProfile === l.provisioningProfile &&
|
||||
r.distributionCertificate?.certP12 === l.distributionCertificate.certP12 &&
|
||||
r.distributionCertificate?.certPassword === l.distributionCertificate.certPassword
|
||||
);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async getCredentialsAsync(
|
||||
src: CredentialsSource.LOCAL | CredentialsSource.REMOTE
|
||||
): Promise<IosCredentials> {
|
||||
switch (src) {
|
||||
case CredentialsSource.LOCAL:
|
||||
return await this.getLocalAsync();
|
||||
case CredentialsSource.REMOTE:
|
||||
return await this.getRemoteAsync();
|
||||
if (src === CredentialsSource.LOCAL) {
|
||||
return await this.getLocalAsync();
|
||||
} else {
|
||||
return await this.getRemoteAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,98 +63,18 @@ export default class IosCredentialsProvider implements CredentialsProvider {
|
||||
}
|
||||
|
||||
private async getRemoteAsync(): Promise<IosCredentials> {
|
||||
if (this.options.skipCredentialsCheck) {
|
||||
Log.log('Skipping credentials check');
|
||||
} else {
|
||||
await new CredentialsManager(this.ctx).runActionAsync(
|
||||
new SetupBuildCredentials(this.options.app, this.options.distribution)
|
||||
);
|
||||
}
|
||||
|
||||
const { distributionCertificate, provisioningProfile } = await this.fetchRemoteAsync();
|
||||
if (!distributionCertificate?.certP12 || !distributionCertificate?.certPassword) {
|
||||
if (this.options.skipCredentialsCheck) {
|
||||
throw new Error(
|
||||
'Distribution certificate is missing and credentials check was skipped. Run without --skip-credentials-check to set it up.'
|
||||
);
|
||||
} else {
|
||||
throw new Error('Distribution certificate is missing');
|
||||
}
|
||||
}
|
||||
if (!provisioningProfile) {
|
||||
if (this.options.skipCredentialsCheck) {
|
||||
throw new Error(
|
||||
'Provisioning profile is missing and credentials check was skipped. Run without --skip-credentials-check to set it up.'
|
||||
);
|
||||
} else {
|
||||
throw new Error('Provisioning profile is missing');
|
||||
}
|
||||
}
|
||||
const manager = new CredentialsManager(this.ctx);
|
||||
const { provisioningProfile, distributionCertificate } = await new SetupBuildCredentials({
|
||||
app: this.options.app,
|
||||
distribution: this.options.distribution,
|
||||
enterpriseProvisioning: this.options.enterpriseProvisioning,
|
||||
}).runAsync(manager, this.ctx);
|
||||
return {
|
||||
provisioningProfile,
|
||||
distributionCertificate: {
|
||||
certP12: distributionCertificate.certP12,
|
||||
certPassword: distributionCertificate.certPassword,
|
||||
certP12: distributionCertificate.certificateP12,
|
||||
certPassword: distributionCertificate.certificatePassword,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async fetchRemoteAsync(): Promise<PartialIosCredentials> {
|
||||
if (this.options.distribution === 'internal') {
|
||||
const { app } = this.options;
|
||||
const account = findAccountByName(this.ctx.user.accounts, app.accountName);
|
||||
if (!account) {
|
||||
throw new Error(`You do not have access to the ${app.accountName} account`);
|
||||
}
|
||||
|
||||
const appLookupParams = {
|
||||
account,
|
||||
bundleIdentifier: this.options.app.bundleIdentifier,
|
||||
projectName: this.options.app.projectName,
|
||||
};
|
||||
|
||||
let appleTeam: AppleTeamFragment | null = null;
|
||||
if (!this.ctx.nonInteractive) {
|
||||
await this.ctx.appStore.ensureAuthenticatedAsync();
|
||||
appleTeam = await resolveAppleTeamIfAuthenticatedAsync(this.ctx, appLookupParams);
|
||||
}
|
||||
|
||||
try {
|
||||
const [distCert, provisioningProfile] = await Promise.all([
|
||||
this.ctx.newIos.getDistributionCertificateForAppAsync(
|
||||
appLookupParams,
|
||||
IosDistributionType.AdHoc,
|
||||
{ appleTeam }
|
||||
),
|
||||
this.ctx.newIos.getProvisioningProfileAsync(appLookupParams, IosDistributionType.AdHoc, {
|
||||
appleTeam,
|
||||
}),
|
||||
]);
|
||||
return {
|
||||
provisioningProfile: provisioningProfile?.provisioningProfile ?? undefined,
|
||||
distributionCertificate: {
|
||||
certP12: distCert?.certificateP12 ?? undefined,
|
||||
certPassword: distCert?.certificatePassword ?? undefined,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof AppleTeamMissingError && this.ctx.nonInteractive) {
|
||||
throw new MissingCredentialsNonInteractiveError();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
const [distCert, provisioningProfile] = await Promise.all([
|
||||
this.ctx.ios.getDistributionCertificateAsync(this.options.app),
|
||||
this.ctx.ios.getProvisioningProfileAsync(this.options.app),
|
||||
]);
|
||||
return {
|
||||
provisioningProfile: provisioningProfile?.provisioningProfile,
|
||||
distributionCertificate: {
|
||||
certP12: distCert?.certP12,
|
||||
certPassword: distCert?.certPassword,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { IosAppBuildCredentialsFragment } from '../../../graphql/generated';
|
||||
import { getAppstoreMock } from '../../__tests__/fixtures-appstore';
|
||||
import { createCtxMock } from '../../__tests__/fixtures-context';
|
||||
import { testIosAppCredentialsWithBuildCredentialsQueryResult } from '../../__tests__/fixtures-ios';
|
||||
import { getNewIosApiMockWithoutCredentials } from '../../__tests__/fixtures-new-ios';
|
||||
import IosCredentialsProvider from '../IosCredentialsProvider';
|
||||
import { getAppLookupParamsFromContext } from '../actions/new/BuildCredentialsUtils';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('../validators/validateProvisioningProfile', () => ({
|
||||
validateProvisioningProfileAsync: async (
|
||||
_ctx: any,
|
||||
_app: any,
|
||||
buildCredentials: Partial<IosAppBuildCredentialsFragment> | null
|
||||
): Promise<boolean> => {
|
||||
return !!(
|
||||
buildCredentials &&
|
||||
buildCredentials.distributionCertificate &&
|
||||
buildCredentials.provisioningProfile
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleError = console.error;
|
||||
const originalConsoleWarn = console.warn;
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.error = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.log = originalConsoleLog;
|
||||
console.error = originalConsoleError;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
describe(IosCredentialsProvider, () => {
|
||||
describe('#getCredentialsAsync', () => {
|
||||
describe('remote credentials', () => {
|
||||
it('throws an error is credentials do not exist', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
appStore: getAppstoreMock(),
|
||||
projectDir: '/app',
|
||||
newIos: {
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(() => null),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const provider = new IosCredentialsProvider(ctx, {
|
||||
app: {
|
||||
account: {
|
||||
id: `id-${appLookupParams.account.name}`,
|
||||
name: appLookupParams.account.name,
|
||||
},
|
||||
bundleIdentifier: appLookupParams.bundleIdentifier,
|
||||
projectName: appLookupParams.projectName,
|
||||
},
|
||||
distribution: 'store',
|
||||
});
|
||||
|
||||
await expect(provider.getCredentialsAsync(CredentialsSource.REMOTE)).rejects.toThrowError(
|
||||
/Credentials are not set up/
|
||||
);
|
||||
});
|
||||
|
||||
it('returns credentials if they exist', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
appStore: getAppstoreMock(),
|
||||
projectDir: '/app',
|
||||
newIos: {
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
|
||||
() => testIosAppCredentialsWithBuildCredentialsQueryResult
|
||||
),
|
||||
getDistributionCertificateForAppAsync: jest.fn(
|
||||
() =>
|
||||
testIosAppCredentialsWithBuildCredentialsQueryResult.iosAppBuildCredentialsArray[0]
|
||||
.distributionCertificate
|
||||
),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const provider = new IosCredentialsProvider(ctx, {
|
||||
app: {
|
||||
account: {
|
||||
id: `id-${appLookupParams.account.name}`,
|
||||
name: appLookupParams.account.name,
|
||||
},
|
||||
bundleIdentifier: appLookupParams.bundleIdentifier,
|
||||
projectName: appLookupParams.projectName,
|
||||
},
|
||||
distribution: 'store',
|
||||
});
|
||||
|
||||
const buildCredentials =
|
||||
testIosAppCredentialsWithBuildCredentialsQueryResult.iosAppBuildCredentialsArray[0];
|
||||
await expect(provider.getCredentialsAsync(CredentialsSource.REMOTE)).resolves.toMatchObject(
|
||||
{
|
||||
distributionCertificate: {
|
||||
certP12: buildCredentials.distributionCertificate?.certificateP12,
|
||||
certPassword: buildCredentials.distributionCertificate?.certificatePassword,
|
||||
},
|
||||
provisioningProfile: buildCredentials.provisioningProfile?.provisioningProfile,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -31,11 +31,7 @@ export class CreateDistributionCertificate implements Action {
|
||||
}
|
||||
|
||||
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
const distCert = await provideOrGenerateDistributionCertificateAsync(
|
||||
manager,
|
||||
ctx,
|
||||
this.accountName
|
||||
);
|
||||
const distCert = await provideOrGenerateDistributionCertificateAsync(ctx, this.accountName);
|
||||
this._distCredentials = await ctx.ios.createDistributionCertificateAsync(
|
||||
this.accountName,
|
||||
distCert
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import Log from '../../../log';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
|
||||
import { ProvisioningProfile } from '../appstore/Credentials.types';
|
||||
import { AppLookupParams, provisioningProfileSchema } from '../credentials';
|
||||
import { displayIosAppCredentials } from '../utils/printCredentials';
|
||||
import { generateProvisioningProfileAsync } from './ProvisioningProfileUtils';
|
||||
|
||||
export class CreateProvisioningProfileStandaloneManager implements Action {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
await manager.runActionAsync(new CreateProvisioningProfile(this.app));
|
||||
|
||||
const appCredentials = await ctx.ios.getAppCredentialsAsync(this.app);
|
||||
displayIosAppCredentials(appCredentials);
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateProvisioningProfile implements Action {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
const provisioningProfile = await this.provideOrGenerateAsync(ctx);
|
||||
await ctx.ios.updateProvisioningProfileAsync(this.app, provisioningProfile);
|
||||
|
||||
Log.succeed('Created provisioning profile');
|
||||
}
|
||||
|
||||
private async provideOrGenerateAsync(ctx: Context): Promise<ProvisioningProfile> {
|
||||
const userProvided = await askForUserProvidedAsync(provisioningProfileSchema);
|
||||
if (userProvided) {
|
||||
// userProvided profiles don't come with ProvisioningProfileId's (only accessible from Apple Portal API)
|
||||
Log.warn('Provisioning profile: Unable to validate specified profile.');
|
||||
return userProvided;
|
||||
}
|
||||
const distCert = await ctx.ios.getDistributionCertificateAsync(this.app);
|
||||
assert(distCert, 'missing distribution certificate');
|
||||
return await generateProvisioningProfileAsync(ctx, this.app.bundleIdentifier, distCert);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import chalk from 'chalk';
|
||||
|
||||
import Log, { learnMore } from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
|
||||
import { AppleTooManyCertsError } from '../appstore/AppStoreApi';
|
||||
@@ -31,7 +30,6 @@ Please remember that Apple Distribution Certificates are not application specifi
|
||||
`;
|
||||
|
||||
export async function provideOrGenerateDistributionCertificateAsync(
|
||||
manager: CredentialsManager,
|
||||
ctx: Context,
|
||||
accountName: string
|
||||
): Promise<DistributionCertificate> {
|
||||
@@ -50,11 +48,11 @@ export async function provideOrGenerateDistributionCertificateAsync(
|
||||
}
|
||||
return isValid
|
||||
? userProvided
|
||||
: await provideOrGenerateDistributionCertificateAsync(manager, ctx, accountName);
|
||||
: await provideOrGenerateDistributionCertificateAsync(ctx, accountName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return await generateDistributionCertificateAsync(manager, ctx, accountName);
|
||||
return await generateDistributionCertificateAsync(ctx, accountName);
|
||||
}
|
||||
|
||||
async function promptForDistCertAsync(ctx: Context): Promise<DistributionCertificate | null> {
|
||||
@@ -78,7 +76,6 @@ async function promptForDistCertAsync(ctx: Context): Promise<DistributionCertifi
|
||||
}
|
||||
|
||||
async function generateDistributionCertificateAsync(
|
||||
manager: CredentialsManager,
|
||||
ctx: Context,
|
||||
accountName: string
|
||||
): Promise<DistributionCertificate> {
|
||||
@@ -125,7 +122,7 @@ async function generateDistributionCertificateAsync(
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return await generateDistributionCertificateAsync(manager, ctx, accountName);
|
||||
return await generateDistributionCertificateAsync(ctx, accountName);
|
||||
}
|
||||
|
||||
interface SelectOptions {
|
||||
@@ -145,7 +142,7 @@ export async function selectDistributionCertificateAsync(
|
||||
let validDistCredentials: IosDistCredentials[] | null = null;
|
||||
if (ctx.appStore.authCtx) {
|
||||
const certInfoFromApple = await ctx.appStore.listDistributionCertificatesAsync();
|
||||
validDistCredentials = await filterRevokedDistributionCerts(distCredentials, certInfoFromApple);
|
||||
validDistCredentials = filterRevokedDistributionCerts(distCredentials, certInfoFromApple);
|
||||
}
|
||||
distCredentials =
|
||||
options.filterInvalid && validDistCredentials ? validDistCredentials : distCredentials;
|
||||
|
||||
@@ -1,109 +1,99 @@
|
||||
import { iOSDistributionType } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import { IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { AppleDevice, IosAppBuildCredentialsFragment } from '../../../graphql/generated';
|
||||
import {
|
||||
IosDistributionType as GraphQLIosDistributionType,
|
||||
IosAppBuildCredentialsFragment,
|
||||
} from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { findAccountByName } from '../../../user/Account';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
import { isCredentialsMap, readIosCredentialsAsync } from '../../credentialsJson/read';
|
||||
import { AppLookupParams, IosAppCredentials, IosDistCredentials } from '../credentials';
|
||||
import { displayProjectCredentials } from '../utils/printCredentials';
|
||||
import { AppLookupParams as GraphQLAppLookupParams } from '../api/GraphqlClient';
|
||||
import { AppLookupParams, IosAppCredentials } from '../credentials';
|
||||
import { displayProjectCredentials as legacyDisplayProjectCredentials } from '../utils/printCredentials';
|
||||
import { displayProjectCredentials } from '../utils/printCredentialsBeta';
|
||||
import { readAppleTeam } from '../utils/provisioningProfile';
|
||||
import { SetupProvisioningProfile } from './SetupProvisioningProfile';
|
||||
import { SetupAdhocProvisioningProfile } from './new/SetupAdhocProvisioningProfile';
|
||||
import { SetupInternalProvisioningProfile } from './new/SetupInternalProvisioningProfile';
|
||||
import { SetupProvisioningProfile } from './new/SetupProvisioningProfile';
|
||||
|
||||
type AppCredentialsAndDistCert = {
|
||||
appCredentials: IosAppCredentials;
|
||||
distCert: IosDistCredentials | null;
|
||||
};
|
||||
interface Options {
|
||||
app: GraphQLAppLookupParams;
|
||||
distribution: IosDistributionType;
|
||||
enterpriseProvisioning?: IosEnterpriseProvisioning;
|
||||
skipCredentialsCheck?: boolean;
|
||||
}
|
||||
|
||||
export class SetupBuildCredentials implements Action {
|
||||
constructor(private app: AppLookupParams, private distribution: iOSDistributionType) {}
|
||||
interface IosAppBuildCredentials {
|
||||
iosDistributionType: GraphQLIosDistributionType;
|
||||
provisioningProfile: string;
|
||||
distributionCertificate: {
|
||||
certificateP12: string;
|
||||
certificatePassword: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class SetupBuildCredentials implements Action<IosAppBuildCredentials> {
|
||||
constructor(private options: Options) {}
|
||||
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<IosAppBuildCredentials> {
|
||||
const { app } = this.options;
|
||||
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
await ctx.bestEffortAppStoreAuthenticateAsync();
|
||||
|
||||
if (ctx.appStore.authCtx) {
|
||||
await ctx.appStore.ensureBundleIdExistsAsync(this.app, { enablePushNotifications: true });
|
||||
await ctx.appStore.ensureBundleIdExistsAsync(
|
||||
{
|
||||
accountName: app.account.name,
|
||||
bundleIdentifier: app.bundleIdentifier,
|
||||
projectName: app.projectName,
|
||||
},
|
||||
{ enablePushNotifications: true }
|
||||
);
|
||||
}
|
||||
|
||||
let iosAppBuildCredentials: IosAppBuildCredentialsFragment | null = null;
|
||||
try {
|
||||
if (this.distribution === 'internal') {
|
||||
const account = findAccountByName(ctx.user.accounts, this.app.accountName);
|
||||
if (!account) {
|
||||
throw new Error(`You do not have access to the ${this.app.accountName} account`);
|
||||
}
|
||||
const action = new SetupAdhocProvisioningProfile({
|
||||
account,
|
||||
projectName: this.app.projectName,
|
||||
bundleIdentifier: this.app.bundleIdentifier,
|
||||
});
|
||||
await manager.runActionAsync(action);
|
||||
iosAppBuildCredentials = action.iosAppBuildCredentials;
|
||||
} else {
|
||||
await manager.runActionAsync(new SetupProvisioningProfile(this.app));
|
||||
}
|
||||
const buildCredentials = await this.setupBuildCredentials(ctx);
|
||||
const appInfo = `@${app.account.name}/${app.projectName} (${app.bundleIdentifier})`;
|
||||
displayProjectCredentials(app, buildCredentials);
|
||||
Log.newLine();
|
||||
Log.log(chalk.green(`All credentials are ready to build ${appInfo}`));
|
||||
Log.newLine();
|
||||
return {
|
||||
iosDistributionType: buildCredentials.iosDistributionType,
|
||||
provisioningProfile: nullthrows(buildCredentials.provisioningProfile?.provisioningProfile),
|
||||
distributionCertificate: {
|
||||
certificateP12: nullthrows(buildCredentials.distributionCertificate?.certificateP12),
|
||||
certificatePassword: nullthrows(
|
||||
buildCredentials.distributionCertificate?.certificatePassword
|
||||
),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
Log.error('Failed to setup credentials.');
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { appCredentials, distCert } = await this.unifyCredentialsFormatAsync(
|
||||
ctx,
|
||||
iosAppBuildCredentials
|
||||
);
|
||||
const appInfo = `@${this.app.accountName}/${this.app.projectName} (${this.app.bundleIdentifier})`;
|
||||
displayProjectCredentials(this.app, appCredentials, /* pushKey */ null, distCert);
|
||||
Log.newLine();
|
||||
Log.log(chalk.green(`All credentials are ready to build ${appInfo}`));
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
async unifyCredentialsFormatAsync(
|
||||
ctx: Context,
|
||||
iosAppBuildCredentials: IosAppBuildCredentialsFragment | null
|
||||
): Promise<AppCredentialsAndDistCert> {
|
||||
if (!iosAppBuildCredentials) {
|
||||
const [appCredentials, distCert] = await Promise.all([
|
||||
ctx.ios.getAppCredentialsAsync(this.app),
|
||||
ctx.ios.getDistributionCertificateAsync(this.app),
|
||||
]);
|
||||
return {
|
||||
appCredentials,
|
||||
distCert,
|
||||
};
|
||||
async setupBuildCredentials(ctx: Context): Promise<IosAppBuildCredentialsFragment> {
|
||||
const { app, distribution, enterpriseProvisioning } = this.options;
|
||||
if (distribution === 'internal') {
|
||||
if (enterpriseProvisioning === 'adhoc') {
|
||||
return await new SetupAdhocProvisioningProfile(app).runAsync(ctx);
|
||||
} else if (enterpriseProvisioning === 'universal') {
|
||||
return await new SetupProvisioningProfile(
|
||||
app,
|
||||
GraphQLIosDistributionType.Enterprise
|
||||
).runAsync(ctx);
|
||||
} else {
|
||||
return await new SetupInternalProvisioningProfile(app).runAsync(ctx);
|
||||
}
|
||||
} else {
|
||||
const { distributionCertificate, provisioningProfile } = iosAppBuildCredentials;
|
||||
assert(distributionCertificate && provisioningProfile);
|
||||
return {
|
||||
appCredentials: {
|
||||
experienceName: `${this.app.accountName}/${this.app.projectName}`,
|
||||
bundleIdentifier: this.app.bundleIdentifier,
|
||||
credentials: {
|
||||
provisioningProfile: provisioningProfile.provisioningProfile ?? undefined,
|
||||
provisioningProfileId: provisioningProfile.developerPortalIdentifier ?? undefined,
|
||||
teamId: provisioningProfile.appleTeam?.appleTeamIdentifier || '',
|
||||
teamName: provisioningProfile.appleTeam?.appleTeamName ?? undefined,
|
||||
devices: provisioningProfile.appleDevices?.filter(device => device) as
|
||||
| AppleDevice[]
|
||||
| undefined,
|
||||
},
|
||||
},
|
||||
distCert: {
|
||||
// the id doesn't really matter, it's only for displaying credentials
|
||||
id: null as any,
|
||||
type: 'dist-cert',
|
||||
certId: distributionCertificate.developerPortalIdentifier ?? undefined,
|
||||
certP12: distributionCertificate.certificateP12 ?? '',
|
||||
certPassword: distributionCertificate.certificatePassword ?? '',
|
||||
teamId: distributionCertificate.appleTeam?.appleTeamIdentifier || '',
|
||||
teamName: distributionCertificate.appleTeam?.appleTeamName ?? undefined,
|
||||
},
|
||||
};
|
||||
return await new SetupProvisioningProfile(app, GraphQLIosDistributionType.AppStore).runAsync(
|
||||
ctx
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,7 +177,7 @@ export class SetupBuildCredentialsFromCredentialsJson implements Action {
|
||||
await ctx.ios.useDistributionCertificateAsync(this.app, createdDistCert.id);
|
||||
}
|
||||
|
||||
displayProjectCredentials(
|
||||
legacyDisplayProjectCredentials(
|
||||
this.app,
|
||||
await ctx.ios.getAppCredentialsAsync(this.app),
|
||||
undefined,
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync, promptAsync } from '../../../prompts';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
import { AppLookupParams } from '../credentials';
|
||||
import { validateDistributionCertificateAsync } from '../validators/validateDistributionCertificate';
|
||||
import { CreateDistributionCertificate } from './CreateDistributionCertificate';
|
||||
import {
|
||||
formatDistributionCertificate,
|
||||
getValidDistCertsAsync,
|
||||
} from './DistributionCertificateUtils';
|
||||
import {
|
||||
UseExistingDistributionCertificate,
|
||||
UseSpecificDistributionCertificate,
|
||||
} from './UseDistributionCertificate';
|
||||
|
||||
export class SetupDistributionCertificateForApp implements Action {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
if (await this.isCurrentCertificateValidAsync(ctx)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(
|
||||
'Distribution Certificate is not configured correctly. Please run this command again in interactive mode.'
|
||||
);
|
||||
}
|
||||
|
||||
const existingCertificates = await getValidDistCertsAsync(ctx, this.app.accountName);
|
||||
|
||||
if (existingCertificates.length === 0) {
|
||||
await this.createNewCertificateAsync(manager, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
// autoselect credentials if we find valid certs
|
||||
const autoselectedCertificate = existingCertificates[0];
|
||||
|
||||
const credentials = await ctx.ios.getAllCredentialsAsync(this.app.accountName);
|
||||
const usedByApps = credentials.appCredentials.filter(
|
||||
cred => cred.distCredentialsId === autoselectedCertificate.id
|
||||
);
|
||||
const confirm = await confirmAsync({
|
||||
message: `${formatDistributionCertificate(
|
||||
autoselectedCertificate,
|
||||
usedByApps,
|
||||
ctx.appStore.authCtx ? 'VALID' : 'UNKNOWN'
|
||||
)} \n Would you like to use this certificate?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
await this.createOrReuseAsync(manager, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.log(`Using Distribution Certificate: ${autoselectedCertificate.certId || '-----'}`);
|
||||
await manager.runActionAsync(
|
||||
new UseSpecificDistributionCertificate(this.app, autoselectedCertificate.id)
|
||||
);
|
||||
}
|
||||
|
||||
private async isCurrentCertificateValidAsync(ctx: Context): Promise<boolean> {
|
||||
const currentCertificate = await ctx.ios.getDistributionCertificateAsync(this.app);
|
||||
if (!currentCertificate) {
|
||||
return false;
|
||||
}
|
||||
if (ctx.appStore.authCtx) {
|
||||
const isValid = await validateDistributionCertificateAsync(ctx, currentCertificate);
|
||||
if (!isValid) {
|
||||
Log.warn("Current Distribution Certificate is no longer valid on Apple's server");
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async createNewCertificateAsync(
|
||||
manager: CredentialsManager,
|
||||
ctx: Context
|
||||
): Promise<void> {
|
||||
const action = new CreateDistributionCertificate(this.app.accountName);
|
||||
await manager.runActionAsync(action);
|
||||
await manager.runActionAsync(
|
||||
new UseSpecificDistributionCertificate(this.app, action.distCredentials.id)
|
||||
);
|
||||
}
|
||||
|
||||
private async createOrReuseAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
const { action } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'action',
|
||||
message: 'Select an iOS Distribution Certificate to use for code signing:',
|
||||
choices: [
|
||||
{
|
||||
title: '[Choose existing certificate] (Recommended)',
|
||||
value: 'CHOOSE_EXISTING',
|
||||
},
|
||||
{ title: '[Add a new certificate]', value: 'GENERATE' },
|
||||
],
|
||||
});
|
||||
|
||||
if (action === 'GENERATE') {
|
||||
await this.createNewCertificateAsync(manager, ctx);
|
||||
} else if (action === 'CHOOSE_EXISTING') {
|
||||
await manager.runActionAsync(new UseExistingDistributionCertificate(this.app));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
import assert from 'assert';
|
||||
import pick from 'lodash/pick';
|
||||
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync, promptAsync } from '../../../prompts';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
import { ProvisioningProfileStoreInfo } from '../appstore/Credentials.types';
|
||||
import { AppLookupParams, IosDistCredentials } from '../credentials';
|
||||
import {
|
||||
validateProvisioningProfileAsync,
|
||||
validateProvisioningProfileWithoutApple,
|
||||
} from '../validators/validateProvisioningProfile';
|
||||
import {
|
||||
ConfigureProvisioningProfile,
|
||||
UseExistingProvisioningProfile,
|
||||
} from './ConfigureProvisioningProfile';
|
||||
import { CreateProvisioningProfile } from './CreateProvisioningProfile';
|
||||
import { formatProvisioningProfileFromApple } from './ProvisioningProfileUtils';
|
||||
import { SetupDistributionCertificateForApp } from './SetupDistributionCertificate';
|
||||
|
||||
export class SetupProvisioningProfile implements Action {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
await manager.runActionAsync(new SetupDistributionCertificateForApp(this.app));
|
||||
if (await this.isCurrentProfileValidAsync(ctx)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(
|
||||
'Provisioning profile is not configured correctly. Please run this command again in interactive mode.'
|
||||
);
|
||||
}
|
||||
|
||||
if (!ctx.appStore.authCtx) {
|
||||
await manager.runActionAsync(new CreateProvisioningProfile(this.app));
|
||||
return;
|
||||
}
|
||||
|
||||
const existingProfiles = await ctx.appStore.listProvisioningProfilesAsync(
|
||||
this.app.bundleIdentifier
|
||||
);
|
||||
|
||||
if (existingProfiles.length === 0) {
|
||||
await manager.runActionAsync(new CreateProvisioningProfile(this.app));
|
||||
return;
|
||||
}
|
||||
|
||||
const distCert = await ctx.ios.getDistributionCertificateAsync(this.app);
|
||||
assert(distCert, 'missing distribution certificate');
|
||||
|
||||
const autoselectedProfile = this.choosePreferred(existingProfiles, distCert);
|
||||
// autoselect credentials if we find valid certs
|
||||
|
||||
if (!ctx.nonInteractive) {
|
||||
const confirm = await confirmAsync({
|
||||
message: `${formatProvisioningProfileFromApple(
|
||||
autoselectedProfile
|
||||
)} \n Would you like to use this profile?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
await this.createOrReuseAsync(manager, ctx);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Log.log(`Using provisioning profile: ${autoselectedProfile.provisioningProfileId}`);
|
||||
}
|
||||
|
||||
await ctx.ios.updateProvisioningProfileAsync(
|
||||
this.app,
|
||||
pick(autoselectedProfile, [
|
||||
'provisioningProfile',
|
||||
'provisioningProfileId',
|
||||
'teamId',
|
||||
'teamName',
|
||||
])
|
||||
);
|
||||
await manager.runActionAsync(new ConfigureProvisioningProfile(this.app));
|
||||
}
|
||||
|
||||
private async isCurrentProfileValidAsync(ctx: Context): Promise<boolean> {
|
||||
const currentProfile = await ctx.ios.getProvisioningProfileAsync(this.app);
|
||||
const currentCertificate = await ctx.ios.getDistributionCertificateAsync(this.app);
|
||||
if (!currentProfile || !currentCertificate) {
|
||||
return false;
|
||||
}
|
||||
if (!ctx.appStore.authCtx || !currentProfile.provisioningProfileId) {
|
||||
if (!currentProfile.provisioningProfileId) {
|
||||
Log.warn(
|
||||
"The provisioning profile we have on file cannot be validated on Apple's servers."
|
||||
);
|
||||
}
|
||||
const validationResult = validateProvisioningProfileWithoutApple(
|
||||
currentProfile,
|
||||
currentCertificate,
|
||||
this.app.bundleIdentifier
|
||||
);
|
||||
if (!validationResult.ok) {
|
||||
Log.warn(validationResult.error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const validationResult = await validateProvisioningProfileAsync(
|
||||
ctx,
|
||||
currentProfile,
|
||||
currentCertificate,
|
||||
this.app.bundleIdentifier
|
||||
);
|
||||
if (!validationResult.ok) {
|
||||
Log.warn(validationResult.error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private choosePreferred(
|
||||
profiles: ProvisioningProfileStoreInfo[],
|
||||
distCert: IosDistCredentials
|
||||
): ProvisioningProfileStoreInfo {
|
||||
// prefer the profile that already has the same dist cert associated with it
|
||||
const profileWithSameCert = profiles.find(profile =>
|
||||
profile.certificates.some(cert => cert.id === distCert.certId)
|
||||
);
|
||||
|
||||
// if not, just get an arbitrary profile
|
||||
return profileWithSameCert || profiles[0];
|
||||
}
|
||||
|
||||
private async createOrReuseAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
const { action } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'action',
|
||||
message: 'Select a provisioning profile:',
|
||||
choices: [
|
||||
{
|
||||
title: '[Choose existing provisioning profile] (Recommended)',
|
||||
value: 'CHOOSE_EXISTING',
|
||||
},
|
||||
{ title: '[Add a new provisioning profile]', value: 'GENERATE' },
|
||||
],
|
||||
});
|
||||
|
||||
if (action === 'GENERATE') {
|
||||
await manager.runActionAsync(new CreateProvisioningProfile(this.app));
|
||||
} else if (action === 'CHOOSE_EXISTING') {
|
||||
await manager.runActionAsync(new UseExistingProvisioningProfile(this.app));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,11 +62,7 @@ export class UpdateSpecificDistributionCertificate implements Action {
|
||||
cred => cred.distCredentialsId === this.userCredentialsId
|
||||
);
|
||||
|
||||
const newDistCert = await provideOrGenerateDistributionCertificateAsync(
|
||||
manager,
|
||||
ctx,
|
||||
this.accountName
|
||||
);
|
||||
const newDistCert = await provideOrGenerateDistributionCertificateAsync(ctx, this.accountName);
|
||||
await ctx.ios.updateDistributionCertificateAsync(
|
||||
this.userCredentialsId,
|
||||
this.accountName,
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import {
|
||||
AppleDistributionCertificateFragment,
|
||||
AppleProvisioningProfileFragment,
|
||||
AppleTeamFragment,
|
||||
IosDistributionType as GraphQLIosDistributionType,
|
||||
IosAppBuildCredentialsFragment,
|
||||
} from '../../../../graphql/generated';
|
||||
import {
|
||||
getProjectAccountName,
|
||||
getProjectConfigDescription,
|
||||
} from '../../../../project/projectUtils';
|
||||
import { findAccountByName } from '../../../../user/Account';
|
||||
import { Context } from '../../../context';
|
||||
import { AppLookupParams } from '../../api/GraphqlClient';
|
||||
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
|
||||
|
||||
export async function getAllBuildCredentialsAsync(
|
||||
ctx: Context,
|
||||
app: AppLookupParams
|
||||
): Promise<IosAppBuildCredentialsFragment[]> {
|
||||
const appCredentials = await ctx.newIos.getIosAppCredentialsWithBuildCredentialsAsync(app, {});
|
||||
if (!appCredentials) {
|
||||
return [];
|
||||
}
|
||||
return appCredentials.iosAppBuildCredentialsArray;
|
||||
}
|
||||
|
||||
export async function getBuildCredentialsAsync(
|
||||
ctx: Context,
|
||||
app: AppLookupParams,
|
||||
iosDistributionType: GraphQLIosDistributionType
|
||||
): Promise<IosAppBuildCredentialsFragment | null> {
|
||||
const appCredentials = await ctx.newIos.getIosAppCredentialsWithBuildCredentialsAsync(app, {
|
||||
iosDistributionType,
|
||||
});
|
||||
if (!appCredentials || appCredentials.iosAppBuildCredentialsArray.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const [buildCredentials] = appCredentials.iosAppBuildCredentialsArray;
|
||||
return buildCredentials;
|
||||
}
|
||||
|
||||
export async function getProvisioningProfileAsync(
|
||||
ctx: Context,
|
||||
app: AppLookupParams,
|
||||
iosDistributionType: GraphQLIosDistributionType
|
||||
): Promise<AppleProvisioningProfileFragment | null> {
|
||||
const buildCredentials = await getBuildCredentialsAsync(ctx, app, iosDistributionType);
|
||||
return buildCredentials?.provisioningProfile ?? null;
|
||||
}
|
||||
|
||||
export async function getDistributionCertificateAsync(
|
||||
ctx: Context,
|
||||
app: AppLookupParams,
|
||||
iosDistributionType: GraphQLIosDistributionType
|
||||
): Promise<AppleDistributionCertificateFragment | null> {
|
||||
const buildCredentials = await getBuildCredentialsAsync(ctx, app, iosDistributionType);
|
||||
return buildCredentials?.distributionCertificate ?? null;
|
||||
}
|
||||
|
||||
export async function assignBuildCredentialsAsync(
|
||||
ctx: Context,
|
||||
app: AppLookupParams,
|
||||
iosDistributionType: GraphQLIosDistributionType,
|
||||
distCert: AppleDistributionCertificateFragment,
|
||||
provisioningProfile: AppleProvisioningProfileFragment,
|
||||
appleTeam?: AppleTeamFragment
|
||||
): Promise<IosAppBuildCredentialsFragment> {
|
||||
const resolvedAppleTeam = nullthrows(
|
||||
appleTeam ?? (await resolveAppleTeamIfAuthenticatedAsync(ctx, app))
|
||||
);
|
||||
const appleAppIdentifier = await ctx.newIos.createOrGetExistingAppleAppIdentifierAsync(
|
||||
app,
|
||||
resolvedAppleTeam
|
||||
);
|
||||
return await ctx.newIos.createOrUpdateIosAppBuildCredentialsAsync(app, {
|
||||
appleTeam: resolvedAppleTeam,
|
||||
appleAppIdentifierId: appleAppIdentifier.id,
|
||||
appleDistributionCertificateId: distCert.id,
|
||||
appleProvisioningProfileId: provisioningProfile.id,
|
||||
iosDistributionType,
|
||||
});
|
||||
}
|
||||
|
||||
export function getAppLookupParamsFromContext(ctx: Context): AppLookupParams {
|
||||
ctx.ensureProjectContext();
|
||||
const projectName = ctx.exp.slug;
|
||||
const accountName = getProjectAccountName(ctx.exp, ctx.user);
|
||||
const account = findAccountByName(ctx.user.accounts, accountName);
|
||||
if (!account) {
|
||||
throw new Error(`You do not have access to account: ${accountName}`);
|
||||
}
|
||||
|
||||
const bundleIdentifier = ctx.exp.ios?.bundleIdentifier;
|
||||
if (!bundleIdentifier) {
|
||||
throw new Error(
|
||||
`ios.bundleIdentifier needs to be defined in your ${getProjectConfigDescription(
|
||||
ctx.projectDir
|
||||
)} file`
|
||||
);
|
||||
}
|
||||
|
||||
return { account, projectName, bundleIdentifier };
|
||||
}
|
||||
@@ -1,35 +1,19 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import Log from '../../../../log';
|
||||
import { Action, CredentialsManager } from '../../../CredentialsManager';
|
||||
import { Context } from '../../../context';
|
||||
import { AppLookupParams } from '../../api/GraphqlClient';
|
||||
import { AppleDistributionCertificateMutationResult } from '../../api/graphql/mutations/AppleDistributionCertificateMutation';
|
||||
import { provideOrGenerateDistributionCertificateAsync } from '../DistributionCertificateUtils';
|
||||
|
||||
export class CreateDistributionCertificate implements Action {
|
||||
private _distributionCertificate?: AppleDistributionCertificateMutationResult;
|
||||
|
||||
export class CreateDistributionCertificate {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
public get distributionCertificate(): AppleDistributionCertificateMutationResult {
|
||||
assert(
|
||||
this._distributionCertificate,
|
||||
'distributionCertificate can be accessed only after calling .runAsync()'
|
||||
);
|
||||
return this._distributionCertificate;
|
||||
}
|
||||
|
||||
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
public async runAsync(ctx: Context): Promise<AppleDistributionCertificateMutationResult> {
|
||||
const distCert = await provideOrGenerateDistributionCertificateAsync(
|
||||
manager,
|
||||
ctx,
|
||||
this.app.account.name
|
||||
);
|
||||
this._distributionCertificate = await ctx.newIos.createDistributionCertificateAsync(
|
||||
this.app,
|
||||
distCert
|
||||
);
|
||||
const result = await ctx.newIos.createDistributionCertificateAsync(this.app, distCert);
|
||||
Log.succeed('Created distribution certificate');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import { AppleDevice } from '../../../../graphql/generated';
|
||||
import { AppleDevice, AppleDeviceFragment } from '../../../../graphql/generated';
|
||||
import { APPLE_DEVICE_CLASS_LABELS } from '../../../../graphql/types/credentials/AppleDevice';
|
||||
import { promptAsync } from '../../.././../prompts';
|
||||
import { AppleDeviceFragmentWithAppleTeam } from '../../api/graphql/queries/AppleDeviceQuery';
|
||||
|
||||
export async function chooseDevices(
|
||||
allDevices: AppleDeviceFragmentWithAppleTeam[],
|
||||
allDevices: AppleDeviceFragment[],
|
||||
preselectedDeviceIdentifiers: string[] = []
|
||||
): Promise<AppleDevice[]> {
|
||||
const preselectedDeviceIdentifierSet = new Set(preselectedDeviceIdentifiers);
|
||||
const isSelected = (device: AppleDeviceFragmentWithAppleTeam) =>
|
||||
const isSelected = (device: AppleDeviceFragment) =>
|
||||
preselectedDeviceIdentifierSet.size === 0 ||
|
||||
preselectedDeviceIdentifierSet.has(device.identifier);
|
||||
const { devices } = await promptAsync({
|
||||
@@ -27,12 +26,12 @@ export async function chooseDevices(
|
||||
return devices;
|
||||
}
|
||||
|
||||
function formatDeviceLabel(device: AppleDeviceFragmentWithAppleTeam): string {
|
||||
export function formatDeviceLabel(device: AppleDeviceFragment): string {
|
||||
const deviceDetails = formatDeviceDetails(device);
|
||||
return `${device.name ?? device.identifier}${deviceDetails !== '' ? ` ${deviceDetails}` : ''}`;
|
||||
}
|
||||
|
||||
function formatDeviceDetails(device: AppleDeviceFragmentWithAppleTeam): string {
|
||||
function formatDeviceDetails(device: AppleDeviceFragment): string {
|
||||
let details = '';
|
||||
if (device.deviceClass) {
|
||||
details += APPLE_DEVICE_CLASS_LABELS[device.deviceClass];
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
import { AppleDistributionCertificateFragment } from '../../../../graphql/generated';
|
||||
import Log from '../../../../log';
|
||||
import { AppleDeviceFragmentWithAppleTeam } from '../../api/graphql/queries/AppleDeviceQuery';
|
||||
import { ProvisioningProfileStoreInfo } from '../../appstore/Credentials.types';
|
||||
|
||||
export function isDevPortalAdhocProfileValid(
|
||||
profileFromDevPortal: ProvisioningProfileStoreInfo | null,
|
||||
distCert: AppleDistributionCertificateFragment,
|
||||
expectedDevices: AppleDeviceFragmentWithAppleTeam[]
|
||||
): boolean {
|
||||
if (!profileFromDevPortal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (profileFromDevPortal.status === 'Invalid' || profileFromDevPortal.certificates.length === 0) {
|
||||
Log.log('Provisioning Profile is invalid');
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentDistCertId = profileFromDevPortal.certificates[0].id;
|
||||
if (
|
||||
distCert.developerPortalIdentifier &&
|
||||
distCert.developerPortalIdentifier !== currentDistCertId
|
||||
) {
|
||||
Log.log(
|
||||
`Provisioning Profile was generated for Distribution Certificate with ID "${currentDistCertId}", expected ID "${distCert.developerPortalIdentifier}"`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const profileDeviceUdids = (profileFromDevPortal.devices ?? []).map(({ udid }) => udid);
|
||||
const expectedDeviceUdids = expectedDevices.map(({ identifier }) => identifier);
|
||||
if (!doUDIDsMatch(profileDeviceUdids, expectedDeviceUdids)) {
|
||||
Log.log('Provisioning Profile was generated for another set of devices');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function doUDIDsMatch(udidsA: string[], udidsB: string[]): boolean {
|
||||
return isEqual(new Set(udidsA), new Set(udidsB));
|
||||
}
|
||||
@@ -5,16 +5,15 @@ import {
|
||||
import Log from '../../../../log';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import { Account } from '../../../../user/Account';
|
||||
import { Action, CredentialsManager } from '../../../CredentialsManager';
|
||||
import { Context } from '../../../context';
|
||||
import { AppLookupParams } from '../../api/GraphqlClient';
|
||||
import { selectDistributionCertificateWithDependenciesAsync } from './DistributionCertificateUtils';
|
||||
import { RemoveProvisioningProfiles } from './RemoveProvisioningProfile';
|
||||
|
||||
export class SelectAndRemoveDistributionCertificate implements Action {
|
||||
export class SelectAndRemoveDistributionCertificate {
|
||||
constructor(private account: Account) {}
|
||||
|
||||
async runAsync(_manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
async runAsync(ctx: Context): Promise<void> {
|
||||
const selected = await selectDistributionCertificateWithDependenciesAsync(ctx, this.account);
|
||||
if (selected) {
|
||||
await new RemoveDistributionCertificate(this.account, selected).runAsync(ctx);
|
||||
|
||||
+171
-147
@@ -1,199 +1,223 @@
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import differenceBy from 'lodash/differenceBy';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import DeviceCreateAction, { RegistrationMethod } from '../../../../devices/actions/create/action';
|
||||
import {
|
||||
AppleDevice,
|
||||
AppleDeviceFragment,
|
||||
AppleProvisioningProfileFragment,
|
||||
AppleTeamFragment,
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
} from '../../../../graphql/generated';
|
||||
import Log from '../../../../log';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import { Action, CredentialsManager } from '../../../CredentialsManager';
|
||||
import { confirmAsync, pressAnyKeyToContinueAsync } from '../../../../prompts';
|
||||
import { Context } from '../../../context';
|
||||
import { AppLookupParams } from '../../api/GraphqlClient';
|
||||
import { AppleProvisioningProfileMutationResult } from '../../api/graphql/mutations/AppleProvisioningProfileMutation';
|
||||
import { AppleDeviceFragmentWithAppleTeam } from '../../api/graphql/queries/AppleDeviceQuery';
|
||||
import { AppleProvisioningProfileQueryResult } from '../../api/graphql/queries/AppleProvisioningProfileQuery';
|
||||
import { ProvisioningProfileStoreInfo } from '../../appstore/Credentials.types';
|
||||
import { ProfileClass } from '../../appstore/provisioningProfile';
|
||||
import { AppleTeamMissingError, MissingCredentialsNonInteractiveError } from '../../errors';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../errors';
|
||||
import { validateProvisioningProfileAsync } from '../../validators/validateProvisioningProfile';
|
||||
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
|
||||
import { chooseDevices } from './DeviceUtils';
|
||||
import { doUDIDsMatch, isDevPortalAdhocProfileValid } from './ProvisioningProfileUtils';
|
||||
import { assignBuildCredentialsAsync, getBuildCredentialsAsync } from './BuildCredentialsUtils';
|
||||
import { chooseDevices, formatDeviceLabel } from './DeviceUtils';
|
||||
import { SetupDistributionCertificate } from './SetupDistributionCertificate';
|
||||
|
||||
export class SetupAdhocProvisioningProfile implements Action {
|
||||
private _iosAppBuildCredentials?: IosAppBuildCredentialsFragment;
|
||||
|
||||
export class SetupAdhocProvisioningProfile {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
public get iosAppBuildCredentials(): IosAppBuildCredentialsFragment {
|
||||
assert(
|
||||
this._iosAppBuildCredentials,
|
||||
'iosAppBuildCredentials can be accessed only after calling .runAsync()'
|
||||
);
|
||||
return this._iosAppBuildCredentials;
|
||||
}
|
||||
async runAsync(ctx: Context): Promise<IosAppBuildCredentialsFragment> {
|
||||
const distCert = await new SetupDistributionCertificate(
|
||||
this.app,
|
||||
IosDistributionType.AdHoc
|
||||
).runAsync(ctx);
|
||||
|
||||
const areBuildCredentialsSetup = await this.areBuildCredentialsSetupAsync(ctx);
|
||||
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
if (ctx.nonInteractive) {
|
||||
try {
|
||||
await this.runNonInteractiveAsync(manager, ctx);
|
||||
} catch (err) {
|
||||
if (err instanceof AppleTeamMissingError) {
|
||||
throw new MissingCredentialsNonInteractiveError();
|
||||
}
|
||||
throw err;
|
||||
if (areBuildCredentialsSetup) {
|
||||
return nullthrows(await getBuildCredentialsAsync(ctx, this.app, IosDistributionType.AdHoc));
|
||||
} else {
|
||||
throw new MissingCredentialsNonInteractiveError(
|
||||
'Provisioning profile is not configured correctly. Please run this command again in interactive mode.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await this.runInteractiveAsync(manager, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
private async runNonInteractiveAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
// 1. Setup Distribution Certificate
|
||||
const distCertAction = new SetupDistributionCertificate(this.app, IosDistributionType.AdHoc);
|
||||
await manager.runActionAsync(distCertAction);
|
||||
|
||||
// 2. Fetch profile from EAS servers
|
||||
const currentProfile = await ctx.newIos.getProvisioningProfileAsync(
|
||||
const currentBuildCredentials = await getBuildCredentialsAsync(
|
||||
ctx,
|
||||
this.app,
|
||||
IosDistributionType.AdHoc
|
||||
);
|
||||
|
||||
if (!currentProfile) {
|
||||
throw new MissingCredentialsNonInteractiveError();
|
||||
}
|
||||
|
||||
// TODO: implement validation
|
||||
Log.warn(
|
||||
'Provisioning Profile is not validated for non-interactive internal distribution builds.'
|
||||
);
|
||||
|
||||
// app credentials should exist here because the profile exists
|
||||
const appCredentials = nullthrows(
|
||||
await ctx.newIos.getIosAppCredentialsWithBuildCredentialsAsync(this.app, {
|
||||
iosDistributionType: IosDistributionType.AdHoc,
|
||||
})
|
||||
);
|
||||
this._iosAppBuildCredentials = appCredentials.iosAppBuildCredentialsArray[0];
|
||||
}
|
||||
|
||||
private async runInteractiveAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
// 0. Ensure the user is authenticated with Apple and resolve the Apple team object
|
||||
await ctx.appStore.ensureAuthenticatedAsync();
|
||||
const appleTeam = nullthrows(await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app));
|
||||
|
||||
// 1. Fetch devices registered on EAS servers
|
||||
const registeredAppleDevices = await ctx.newIos.getDevicesForAppleTeamAsync(
|
||||
this.app,
|
||||
appleTeam
|
||||
);
|
||||
const registeredAppleDeviceIdentifiers = registeredAppleDevices.map(
|
||||
({ identifier }) => identifier
|
||||
);
|
||||
if (registeredAppleDeviceIdentifiers.length === 0) {
|
||||
throw new Error(`Run 'eas device:create' to register your devices first`);
|
||||
}
|
||||
|
||||
// 2. Setup Distribution Certificate
|
||||
const distCertAction = new SetupDistributionCertificate(this.app, IosDistributionType.AdHoc);
|
||||
await manager.runActionAsync(distCertAction);
|
||||
const distCert = distCertAction.distributionCertificate;
|
||||
|
||||
let profileFromExpoServersToUse:
|
||||
| AppleProvisioningProfileQueryResult
|
||||
| AppleProvisioningProfileMutationResult
|
||||
| null = await ctx.newIos.getProvisioningProfileAsync(this.app, IosDistributionType.AdHoc, {
|
||||
appleTeam,
|
||||
});
|
||||
|
||||
// 4. Choose devices for internal distribution
|
||||
let chosenDevices: AppleDeviceFragmentWithAppleTeam[];
|
||||
if (profileFromExpoServersToUse) {
|
||||
const appleDeviceIdentifiersFromProfile = ((profileFromExpoServersToUse.appleDevices ??
|
||||
[]) as AppleDevice[]).map(({ identifier }) => identifier);
|
||||
|
||||
let shouldAskToChooseDevices = true;
|
||||
if (doUDIDsMatch(appleDeviceIdentifiersFromProfile, registeredAppleDeviceIdentifiers)) {
|
||||
shouldAskToChooseDevices = await confirmAsync({
|
||||
message: `All your registered devices are present in the Provisioning Profile. Would you like to exclude some devices?`,
|
||||
initial: false,
|
||||
});
|
||||
if (areBuildCredentialsSetup) {
|
||||
const buildCredentials = nullthrows(currentBuildCredentials);
|
||||
if (await this.shouldUseExistingProfileAsync(ctx, buildCredentials)) {
|
||||
return buildCredentials;
|
||||
}
|
||||
|
||||
chosenDevices = shouldAskToChooseDevices
|
||||
? await chooseDevices(registeredAppleDevices, appleDeviceIdentifiersFromProfile)
|
||||
: registeredAppleDevices;
|
||||
} else {
|
||||
chosenDevices = await chooseDevices(registeredAppleDevices);
|
||||
}
|
||||
|
||||
// 5. Try to find the profile on Apple Developer Portal
|
||||
const currentProfileFromDevPortal = await this.getProfileFromDevPortalAsync(ctx);
|
||||
// 1. Resolve Apple Team
|
||||
let appleTeam: AppleTeamFragment | null =
|
||||
distCert.appleTeam ?? currentBuildCredentials?.provisioningProfile?.appleTeam ?? null;
|
||||
if (!appleTeam) {
|
||||
await ctx.appStore.ensureAuthenticatedAsync();
|
||||
appleTeam = await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app);
|
||||
}
|
||||
assert(appleTeam, 'Apple Team must be defined here');
|
||||
|
||||
// 6. Validate the profile and possibly recreate it
|
||||
let profileToUse: ProvisioningProfileStoreInfo;
|
||||
let profileWasRecreated = false;
|
||||
if (isDevPortalAdhocProfileValid(currentProfileFromDevPortal, distCert, chosenDevices)) {
|
||||
profileToUse = nullthrows(currentProfileFromDevPortal);
|
||||
} else {
|
||||
const chosenDeviceUDIDs = chosenDevices.map(({ identifier }) => identifier);
|
||||
await ctx.appStore.createOrReuseAdhocProvisioningProfileAsync(
|
||||
chosenDeviceUDIDs,
|
||||
this.app.bundleIdentifier,
|
||||
distCert.serialNumber
|
||||
);
|
||||
profileToUse = nullthrows(await this.getProfileFromDevPortalAsync(ctx));
|
||||
profileWasRecreated = true;
|
||||
// 2. Fetch devices registered on EAS servers
|
||||
let registeredAppleDevices = await ctx.newIos.getDevicesForAppleTeamAsync(this.app, appleTeam);
|
||||
if (registeredAppleDevices.length === 0) {
|
||||
const shouldRegisterDevices = await confirmAsync({
|
||||
message: `You don't have any registered devices yet. Would you like to register them now?`,
|
||||
initial: true,
|
||||
});
|
||||
|
||||
if (shouldRegisterDevices) {
|
||||
registeredAppleDevices = await this.registerDevicesAsync(ctx, appleTeam);
|
||||
} else {
|
||||
throw new Error(`Run 'eas device:create' to register your devices first`);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Send profile to www if new has been created
|
||||
// 3. Choose devices for internal distribution
|
||||
const provisionedDeviceIdentifiers = (
|
||||
currentBuildCredentials?.provisioningProfile?.appleDevices ?? []
|
||||
).map(i => i.identifier);
|
||||
const chosenDevices = await chooseDevices(registeredAppleDevices, provisionedDeviceIdentifiers);
|
||||
|
||||
// 4. Reuse or create the profile on Apple Developer Portal
|
||||
const provisioningProfileStoreInfo = await ctx.appStore.createOrReuseAdhocProvisioningProfileAsync(
|
||||
chosenDevices.map(({ identifier }) => identifier),
|
||||
this.app.bundleIdentifier,
|
||||
distCert.serialNumber
|
||||
);
|
||||
|
||||
// 5. Create or update the profile on servers
|
||||
const appleAppIdentifier = await ctx.newIos.createOrGetExistingAppleAppIdentifierAsync(
|
||||
this.app,
|
||||
appleTeam
|
||||
);
|
||||
if (profileFromExpoServersToUse) {
|
||||
if (profileWasRecreated) {
|
||||
profileFromExpoServersToUse = await ctx.newIos.updateProvisioningProfileAsync(
|
||||
profileFromExpoServersToUse.id,
|
||||
let appleProvisioningProfile: AppleProvisioningProfileFragment | null = null;
|
||||
if (currentBuildCredentials?.provisioningProfile) {
|
||||
if (
|
||||
currentBuildCredentials.provisioningProfile.developerPortalIdentifier !==
|
||||
provisioningProfileStoreInfo.provisioningProfileId
|
||||
) {
|
||||
await ctx.newIos.deleteProvisioningProfilesAsync([
|
||||
currentBuildCredentials.provisioningProfile.id,
|
||||
]);
|
||||
appleProvisioningProfile = await ctx.newIos.createProvisioningProfileAsync(
|
||||
this.app,
|
||||
appleAppIdentifier,
|
||||
{
|
||||
appleProvisioningProfile: profileToUse.provisioningProfile,
|
||||
developerPortalIdentifier: profileToUse.provisioningProfileId,
|
||||
appleProvisioningProfile: provisioningProfileStoreInfo.provisioningProfile,
|
||||
developerPortalIdentifier: provisioningProfileStoreInfo.provisioningProfileId,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
appleProvisioningProfile = currentBuildCredentials.provisioningProfile;
|
||||
}
|
||||
} else {
|
||||
profileFromExpoServersToUse = await ctx.newIos.createProvisioningProfileAsync(
|
||||
appleProvisioningProfile = await ctx.newIos.createProvisioningProfileAsync(
|
||||
this.app,
|
||||
appleAppIdentifier,
|
||||
{
|
||||
appleProvisioningProfile: profileToUse.provisioningProfile,
|
||||
developerPortalIdentifier: profileToUse.provisioningProfileId,
|
||||
appleProvisioningProfile: provisioningProfileStoreInfo.provisioningProfile,
|
||||
developerPortalIdentifier: provisioningProfileStoreInfo.provisioningProfileId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 8. Create (or update) app build credentials
|
||||
this._iosAppBuildCredentials = await ctx.newIos.createOrUpdateIosAppBuildCredentialsAsync(
|
||||
// 6. Create (or update) app build credentials
|
||||
assert(appleProvisioningProfile);
|
||||
return await assignBuildCredentialsAsync(
|
||||
ctx,
|
||||
this.app,
|
||||
{
|
||||
appleTeam,
|
||||
appleAppIdentifierId: appleAppIdentifier.id,
|
||||
appleDistributionCertificateId: distCert.id,
|
||||
appleProvisioningProfileId: profileFromExpoServersToUse.id,
|
||||
iosDistributionType: IosDistributionType.AdHoc,
|
||||
}
|
||||
IosDistributionType.AdHoc,
|
||||
distCert,
|
||||
appleProvisioningProfile,
|
||||
appleTeam
|
||||
);
|
||||
}
|
||||
|
||||
private async getProfileFromDevPortalAsync(
|
||||
ctx: Context
|
||||
): Promise<ProvisioningProfileStoreInfo | null> {
|
||||
const profilesFromDevPortal = await ctx.appStore.listProvisioningProfilesAsync(
|
||||
this.app.bundleIdentifier,
|
||||
ProfileClass.Adhoc
|
||||
private async areBuildCredentialsSetupAsync(ctx: Context): Promise<boolean> {
|
||||
const buildCredentials = await getBuildCredentialsAsync(
|
||||
ctx,
|
||||
this.app,
|
||||
IosDistributionType.AdHoc
|
||||
);
|
||||
return profilesFromDevPortal.length >= 1 ? profilesFromDevPortal[0] : null;
|
||||
return await validateProvisioningProfileAsync(ctx, this.app, buildCredentials);
|
||||
}
|
||||
|
||||
private async shouldUseExistingProfileAsync(
|
||||
ctx: Context,
|
||||
buildCredentials: IosAppBuildCredentialsFragment
|
||||
): Promise<boolean> {
|
||||
const provisioningProfile = nullthrows(buildCredentials.provisioningProfile);
|
||||
|
||||
const appleTeam = nullthrows(provisioningProfile.appleTeam);
|
||||
const registeredAppleDevices = await ctx.newIos.getDevicesForAppleTeamAsync(
|
||||
this.app,
|
||||
appleTeam
|
||||
);
|
||||
|
||||
const provisionedDevices = provisioningProfile.appleDevices;
|
||||
|
||||
const allRegisteredDevicesAreProvisioned = doUDIDsMatch(
|
||||
registeredAppleDevices.map(({ identifier }) => identifier),
|
||||
provisionedDevices.map(({ identifier }) => identifier)
|
||||
);
|
||||
|
||||
if (allRegisteredDevicesAreProvisioned) {
|
||||
return await confirmAsync({
|
||||
message: `All your registered devices are present in the Provisioning Profile. Would you like to reuse it?`,
|
||||
initial: true,
|
||||
});
|
||||
} else {
|
||||
const missingDevices = differenceBy(registeredAppleDevices, provisionedDevices, 'identifier');
|
||||
Log.warn(`The provisioning profile is missing the following devices:`);
|
||||
for (const missingDevice of missingDevices) {
|
||||
Log.warn(`- ${formatDeviceLabel(missingDevice)}`);
|
||||
}
|
||||
return !(await confirmAsync({
|
||||
message: `Would you like to choose the devices to provision again?`,
|
||||
initial: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private async registerDevicesAsync(
|
||||
ctx: Context,
|
||||
appleTeam: AppleTeamFragment
|
||||
): Promise<AppleDeviceFragment[]> {
|
||||
const action = new DeviceCreateAction(this.app.account, appleTeam);
|
||||
const method = await action.runAsync();
|
||||
|
||||
while (true) {
|
||||
if (method !== RegistrationMethod.INPUT) {
|
||||
Log.newLine();
|
||||
Log.log(chalk.bold("Press any key if you've already finished device registration."));
|
||||
await pressAnyKeyToContinueAsync();
|
||||
}
|
||||
Log.newLine();
|
||||
|
||||
const devices = await ctx.newIos.getDevicesForAppleTeamAsync(this.app, appleTeam, {
|
||||
useCache: false,
|
||||
});
|
||||
if (devices.length === 0) {
|
||||
Log.warn('There are still no registered devices.');
|
||||
// if the user used the input method there should be some devices available
|
||||
if (method === RegistrationMethod.INPUT) {
|
||||
throw new Error('Input registration method has failed');
|
||||
}
|
||||
} else {
|
||||
return devices;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function doUDIDsMatch(udidsA: string[], udidsB: string[]): boolean {
|
||||
return isEqual(new Set(udidsA), new Set(udidsB));
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import {
|
||||
AppleDistributionCertificateFragment,
|
||||
AppleProvisioningProfileFragment,
|
||||
AppleTeamFragment,
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
} from '../../../../graphql/generated';
|
||||
import Log from '../../../../log';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import { Context } from '../../../context';
|
||||
import {
|
||||
IosTargetCredentials,
|
||||
isCredentialsMap,
|
||||
readIosCredentialsAsync,
|
||||
} from '../../../credentialsJson/read';
|
||||
import { AppLookupParams } from '../../api/GraphqlClient';
|
||||
import { displayProjectCredentials } from '../../utils/printCredentialsBeta';
|
||||
import { readAppleTeam } from '../../utils/provisioningProfile';
|
||||
import {
|
||||
assignBuildCredentialsAsync,
|
||||
getBuildCredentialsAsync,
|
||||
getDistributionCertificateAsync,
|
||||
getProvisioningProfileAsync,
|
||||
} from './BuildCredentialsUtils';
|
||||
|
||||
export class SetupBuildCredentialsFromCredentialsJson {
|
||||
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
|
||||
|
||||
async getDistributionCertificateToAssignAsync(
|
||||
ctx: Context,
|
||||
targetCredentials: IosTargetCredentials,
|
||||
appleTeam: AppleTeamFragment,
|
||||
currentDistributionCertificate: AppleDistributionCertificateFragment | null
|
||||
): Promise<AppleDistributionCertificateFragment> {
|
||||
const { distributionCertificate } = targetCredentials;
|
||||
const { certP12, certPassword } = distributionCertificate;
|
||||
|
||||
if (!currentDistributionCertificate) {
|
||||
return await ctx.newIos.createDistributionCertificateAsync(this.app, {
|
||||
certP12,
|
||||
certPassword,
|
||||
teamId: appleTeam.appleTeamIdentifier,
|
||||
teamName: appleTeam.appleTeamName ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
const isSameCertificate = currentDistributionCertificate.certificateP12 === certP12;
|
||||
if (!isSameCertificate) {
|
||||
return await ctx.newIos.createDistributionCertificateAsync(this.app, {
|
||||
certP12,
|
||||
certPassword,
|
||||
teamId: appleTeam.appleTeamIdentifier,
|
||||
teamName: appleTeam.appleTeamName ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// If the configured Distribution Certificate is the same as the new one, just use the currently configured certificate
|
||||
return currentDistributionCertificate;
|
||||
}
|
||||
|
||||
async createNewProvisioningProfileAsync(
|
||||
ctx: Context,
|
||||
targetCredentials: IosTargetCredentials,
|
||||
appleTeam: AppleTeamFragment
|
||||
): Promise<AppleProvisioningProfileFragment> {
|
||||
const { provisioningProfile } = targetCredentials;
|
||||
|
||||
const appleAppIdentifier = await ctx.newIos.createOrGetExistingAppleAppIdentifierAsync(
|
||||
this.app,
|
||||
appleTeam
|
||||
);
|
||||
return await ctx.newIos.createProvisioningProfileAsync(this.app, appleAppIdentifier, {
|
||||
appleProvisioningProfile: provisioningProfile,
|
||||
});
|
||||
}
|
||||
|
||||
async getProvisioningProfileToAssignAsync(
|
||||
ctx: Context,
|
||||
targetCredentials: IosTargetCredentials,
|
||||
appleTeam: AppleTeamFragment,
|
||||
currentProvisioningProfile: AppleProvisioningProfileFragment | null
|
||||
): Promise<AppleProvisioningProfileFragment> {
|
||||
const { provisioningProfile } = targetCredentials;
|
||||
|
||||
if (!currentProvisioningProfile) {
|
||||
return await this.createNewProvisioningProfileAsync(ctx, targetCredentials, appleTeam);
|
||||
}
|
||||
|
||||
const isSameProfile = currentProvisioningProfile.provisioningProfile === provisioningProfile;
|
||||
if (!isSameProfile) {
|
||||
return await this.createNewProvisioningProfileAsync(ctx, targetCredentials, appleTeam);
|
||||
}
|
||||
|
||||
// If the configured Provisioning Profile is the same as the new one, just use the currently configured profile
|
||||
return currentProvisioningProfile;
|
||||
}
|
||||
|
||||
async runAsync(ctx: Context): Promise<IosAppBuildCredentialsFragment> {
|
||||
let localCredentials;
|
||||
try {
|
||||
localCredentials = await readIosCredentialsAsync(ctx.projectDir);
|
||||
} catch (error) {
|
||||
Log.error(
|
||||
'Reading credentials from credentials.json failed. Make sure this file is correct and all credentials are present there.'
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// TODO: implement storing multi-target credentials on EAS servers
|
||||
if (isCredentialsMap(localCredentials)) {
|
||||
throw new Error(
|
||||
'Storing multi-target iOS credentials from credentials.json on EAS servers is not yet supported.'
|
||||
);
|
||||
}
|
||||
|
||||
// currently configured credentials
|
||||
const buildCredentials = await getBuildCredentialsAsync(ctx, this.app, this.distributionType);
|
||||
const currentDistributionCertificate = await getDistributionCertificateAsync(
|
||||
ctx,
|
||||
this.app,
|
||||
this.distributionType
|
||||
);
|
||||
const currentProfile = await getProvisioningProfileAsync(ctx, this.app, this.distributionType);
|
||||
const appInfo = `@${this.app.account.name}/${this.app.projectName} (${this.app.bundleIdentifier})`;
|
||||
|
||||
// new credentials from local json
|
||||
const appleTeamFromProvisioningProfile = readAppleTeam(localCredentials.provisioningProfile);
|
||||
const appleTeam = await ctx.newIos.createOrGetExistingAppleTeamAsync(this.app, {
|
||||
appleTeamIdentifier: appleTeamFromProvisioningProfile.teamId,
|
||||
appleTeamName: appleTeamFromProvisioningProfile.teamName,
|
||||
});
|
||||
const distributionCertificateToAssign = await this.getDistributionCertificateToAssignAsync(
|
||||
ctx,
|
||||
localCredentials,
|
||||
appleTeam,
|
||||
currentDistributionCertificate
|
||||
);
|
||||
const provisioningProfileToAssign = await this.getProvisioningProfileToAssignAsync(
|
||||
ctx,
|
||||
localCredentials,
|
||||
appleTeam,
|
||||
currentProfile
|
||||
);
|
||||
|
||||
const didCredentialsChange =
|
||||
!currentDistributionCertificate ||
|
||||
!currentProfile ||
|
||||
currentDistributionCertificate.id !== distributionCertificateToAssign.id ||
|
||||
currentProfile.id !== provisioningProfileToAssign.id;
|
||||
if (!didCredentialsChange) {
|
||||
Log.log('Credentials have already been configured to your credentials json.');
|
||||
return nullthrows(buildCredentials, 'buildCredentials should have been defined.');
|
||||
}
|
||||
|
||||
if (buildCredentials) {
|
||||
Log.log('Currently configured credentials:');
|
||||
displayProjectCredentials(this.app, buildCredentials);
|
||||
if (!ctx.nonInteractive) {
|
||||
const confirm = await confirmAsync({
|
||||
message: `Would you like to replace this configuration with credentials from credentials json?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
throw new Error('Aborting setup of build credentials from credentials json');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.log(`Assigning credentials from credentials json to ${appInfo}...`);
|
||||
const newBuildCredentials = await assignBuildCredentialsAsync(
|
||||
ctx,
|
||||
this.app,
|
||||
this.distributionType,
|
||||
distributionCertificateToAssign,
|
||||
provisioningProfileToAssign,
|
||||
appleTeam
|
||||
);
|
||||
|
||||
Log.log('New credentials configuration:');
|
||||
displayProjectCredentials(this.app, newBuildCredentials);
|
||||
|
||||
Log.newLine();
|
||||
Log.log(chalk.green(`All credentials are ready to build ${appInfo}`));
|
||||
Log.newLine();
|
||||
return newBuildCredentials;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from '../../../../graphql/generated';
|
||||
import Log from '../../../../log';
|
||||
import { confirmAsync, promptAsync } from '../../../../prompts';
|
||||
import { Action, CredentialsManager } from '../../../CredentialsManager';
|
||||
import { Context } from '../../../context';
|
||||
import { AppLookupParams } from '../../api/GraphqlClient';
|
||||
import { AppleDistributionCertificateMutationResult } from '../../api/graphql/mutations/AppleDistributionCertificateMutation';
|
||||
@@ -18,21 +17,12 @@ import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
|
||||
import { CreateDistributionCertificate } from './CreateDistributionCertificate';
|
||||
import { formatDistributionCertificate } from './DistributionCertificateUtils';
|
||||
|
||||
export class SetupDistributionCertificate implements Action {
|
||||
export class SetupDistributionCertificate {
|
||||
private validDistCerts?: AppleDistributionCertificateFragment[];
|
||||
private _distributionCertificate?: AppleDistributionCertificateFragment;
|
||||
|
||||
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
|
||||
|
||||
public get distributionCertificate(): AppleDistributionCertificateFragment {
|
||||
assert(
|
||||
this._distributionCertificate,
|
||||
'distributionCertificate can be accessed only after calling .runAsync()'
|
||||
);
|
||||
return this._distributionCertificate;
|
||||
}
|
||||
|
||||
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
public async runAsync(ctx: Context): Promise<AppleDistributionCertificateFragment> {
|
||||
const appleTeam = await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app);
|
||||
|
||||
try {
|
||||
@@ -43,9 +33,9 @@ export class SetupDistributionCertificate implements Action {
|
||||
);
|
||||
|
||||
if (ctx.nonInteractive) {
|
||||
await this.runNonInteractiveAsync(ctx, currentCertificate);
|
||||
return await this.runNonInteractiveAsync(ctx, currentCertificate);
|
||||
} else {
|
||||
await this.runInteractiveAsync(ctx, manager, currentCertificate);
|
||||
return await this.runInteractiveAsync(ctx, currentCertificate);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof AppleTeamMissingError && ctx.nonInteractive) {
|
||||
@@ -58,32 +48,29 @@ export class SetupDistributionCertificate implements Action {
|
||||
private async runNonInteractiveAsync(
|
||||
ctx: Context,
|
||||
currentCertificate: AppleDistributionCertificateFragment | null
|
||||
): Promise<void> {
|
||||
): Promise<AppleDistributionCertificateFragment> {
|
||||
// TODO: implement validation
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn('Distribution Certificate is not validated for non-interactive builds.');
|
||||
if (!currentCertificate) {
|
||||
throw new MissingCredentialsNonInteractiveError();
|
||||
}
|
||||
this._distributionCertificate = currentCertificate;
|
||||
return currentCertificate;
|
||||
}
|
||||
|
||||
private async runInteractiveAsync(
|
||||
ctx: Context,
|
||||
manager: CredentialsManager,
|
||||
currentCertificate: AppleDistributionCertificateFragment | null
|
||||
): Promise<void> {
|
||||
): Promise<AppleDistributionCertificateFragment> {
|
||||
if (await this.isCurrentCertificateValidAsync(ctx, currentCertificate)) {
|
||||
assert(currentCertificate, 'currentCertificate is defined here');
|
||||
this._distributionCertificate = currentCertificate;
|
||||
return;
|
||||
return currentCertificate;
|
||||
}
|
||||
|
||||
const validDistCertsOnFile = await this.getValidDistCertsAsync(ctx);
|
||||
this._distributionCertificate =
|
||||
validDistCertsOnFile.length === 0
|
||||
? await this.createNewDistCertAsync(manager)
|
||||
: await this.createOrReuseDistCert(manager, ctx);
|
||||
return validDistCertsOnFile.length === 0
|
||||
? await this.createNewDistCertAsync(ctx)
|
||||
: await this.createOrReuseDistCert(ctx);
|
||||
}
|
||||
|
||||
private async isCurrentCertificateValidAsync(
|
||||
@@ -93,6 +80,22 @@ export class SetupDistributionCertificate implements Action {
|
||||
if (!currentCertificate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
if (
|
||||
now < new Date(currentCertificate.validityNotBefore) ||
|
||||
now > new Date(currentCertificate.validityNotAfter)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ctx.appStore.authCtx) {
|
||||
Log.warn(
|
||||
"Skipping Distribution Certificate validation on Apple Servers because we aren't authenticated."
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
const validCertSerialNumbers = (await this.getValidDistCertsAsync(ctx)).map(
|
||||
i => i.serialNumber
|
||||
);
|
||||
@@ -103,10 +106,7 @@ export class SetupDistributionCertificate implements Action {
|
||||
return isValid;
|
||||
}
|
||||
|
||||
private async createOrReuseDistCert(
|
||||
manager: CredentialsManager,
|
||||
ctx: Context
|
||||
): Promise<AppleDistributionCertificateFragment> {
|
||||
private async createOrReuseDistCert(ctx: Context): Promise<AppleDistributionCertificateFragment> {
|
||||
const validDistCerts = await this.getValidDistCertsAsync(ctx);
|
||||
const autoselectedDistCert = validDistCerts[0];
|
||||
|
||||
@@ -137,18 +137,16 @@ export class SetupDistributionCertificate implements Action {
|
||||
});
|
||||
|
||||
if (action === 'GENERATE') {
|
||||
return await this.createNewDistCertAsync(manager);
|
||||
return await this.createNewDistCertAsync(ctx);
|
||||
} else {
|
||||
return await this.reuseDistCertAsync(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
private async createNewDistCertAsync(
|
||||
manager: CredentialsManager
|
||||
ctx: Context
|
||||
): Promise<AppleDistributionCertificateMutationResult> {
|
||||
const action = new CreateDistributionCertificate(this.app);
|
||||
await manager.runActionAsync(action);
|
||||
return action.distributionCertificate;
|
||||
return new CreateDistributionCertificate(this.app).runAsync(ctx);
|
||||
}
|
||||
|
||||
private async reuseDistCertAsync(ctx: Context): Promise<AppleDistributionCertificate> {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { IosAppBuildCredentialsFragment, IosDistributionType } from '../../../../graphql/generated';
|
||||
import Log from '../../../../log';
|
||||
import { promptAsync } from '../../../../prompts';
|
||||
import { Context } from '../../../context';
|
||||
import { AppLookupParams } from '../../api/GraphqlClient';
|
||||
import { getAllBuildCredentialsAsync } from './BuildCredentialsUtils';
|
||||
import { SetupAdhocProvisioningProfile } from './SetupAdhocProvisioningProfile';
|
||||
import { SetupProvisioningProfile } from './SetupProvisioningProfile';
|
||||
|
||||
/**
|
||||
* It's used when setting up credentials for internal distribution but `enterpriseProvisioning` is not set.
|
||||
*
|
||||
* TLDR: If the user authenticates with an account with Apple Developer Enterprise Program membership we ask them
|
||||
* to choose if they want to set up an adhoc or universal distribution provisioning profile. Otherwise, always
|
||||
* set up an adhoc provisioning profile.
|
||||
*/
|
||||
export class SetupInternalProvisioningProfile {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
async runAsync(ctx: Context): Promise<IosAppBuildCredentialsFragment> {
|
||||
const buildCredentials = await getAllBuildCredentialsAsync(ctx, this.app);
|
||||
|
||||
const adhocBuildCredentialsExist =
|
||||
buildCredentials.filter(
|
||||
({ iosDistributionType }) => iosDistributionType === IosDistributionType.AdHoc
|
||||
).length > 0;
|
||||
const enterpriseBuildCredentialsExist =
|
||||
buildCredentials.filter(
|
||||
({ iosDistributionType }) => iosDistributionType === IosDistributionType.Enterprise
|
||||
).length > 0;
|
||||
|
||||
if (!ctx.nonInteractive) {
|
||||
if (ctx.appStore.authCtx) {
|
||||
if (ctx.appStore.authCtx.team.inHouse) {
|
||||
return await this.askForDistributionTypeAndSetupAsync(
|
||||
ctx,
|
||||
'Which credentials would you like to set up?'
|
||||
);
|
||||
} else {
|
||||
return await this.setupAdhocProvisioningProfileAsync(ctx);
|
||||
}
|
||||
} else {
|
||||
if (adhocBuildCredentialsExist && enterpriseBuildCredentialsExist) {
|
||||
Log.log('You have set up both adhoc and universal distribution credentials.');
|
||||
return await this.askForDistributionTypeAndSetupAsync(
|
||||
ctx,
|
||||
'Which credentials would you like to use?'
|
||||
);
|
||||
} else if (adhocBuildCredentialsExist) {
|
||||
return await this.setupAdhocProvisioningProfileAsync(ctx);
|
||||
} else if (enterpriseBuildCredentialsExist) {
|
||||
return await this.setupUniversalProvisioningProfileAsync(ctx);
|
||||
} else {
|
||||
const { team } = await ctx.appStore.ensureAuthenticatedAsync();
|
||||
if (team.inHouse) {
|
||||
return await this.askForDistributionTypeAndSetupAsync(
|
||||
ctx,
|
||||
'Which credentials would you like to set up?'
|
||||
);
|
||||
} else {
|
||||
return await this.setupAdhocProvisioningProfileAsync(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (adhocBuildCredentialsExist && enterpriseBuildCredentialsExist) {
|
||||
throw new Error(
|
||||
`You're in non-interactive mode. You have set up both adhoc and universal distribution credentials. Please set the 'enterpriseProvisioning' property (to 'adhoc' or 'universal') in eas.json to choose the credentials to use.`
|
||||
);
|
||||
} else if (adhocBuildCredentialsExist) {
|
||||
return await this.setupAdhocProvisioningProfileAsync(ctx);
|
||||
} else if (enterpriseBuildCredentialsExist) {
|
||||
return await this.setupUniversalProvisioningProfileAsync(ctx);
|
||||
} else {
|
||||
throw new Error(
|
||||
`You're in non-interactive mode. EAS CLI couldn't find any credentials suitable for internal distribution. Please run again in interactive mode.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async setupAdhocProvisioningProfileAsync(
|
||||
ctx: Context
|
||||
): Promise<IosAppBuildCredentialsFragment> {
|
||||
return await new SetupAdhocProvisioningProfile(this.app).runAsync(ctx);
|
||||
}
|
||||
|
||||
private async setupUniversalProvisioningProfileAsync(
|
||||
ctx: Context
|
||||
): Promise<IosAppBuildCredentialsFragment> {
|
||||
return await new SetupProvisioningProfile(this.app, IosDistributionType.Enterprise).runAsync(
|
||||
ctx
|
||||
);
|
||||
}
|
||||
|
||||
private async askForDistributionTypeAndSetupAsync(
|
||||
ctx: Context,
|
||||
message: string
|
||||
): Promise<IosAppBuildCredentialsFragment> {
|
||||
const { distributionType } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'distributionType',
|
||||
message,
|
||||
choices: [
|
||||
{ title: 'Universal Distribution', value: IosDistributionType.Enterprise },
|
||||
{ title: 'Adhoc Distribution', value: IosDistributionType.AdHoc },
|
||||
],
|
||||
});
|
||||
if (distributionType === IosDistributionType.Enterprise) {
|
||||
return await this.setupUniversalProvisioningProfileAsync(ctx);
|
||||
} else {
|
||||
return await this.setupAdhocProvisioningProfileAsync(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,47 +6,28 @@ import {
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
} from '../../../../graphql/generated';
|
||||
import Log from '../../../../log';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import { CredentialsManager } from '../../../CredentialsManager';
|
||||
import { Context } from '../../../context';
|
||||
import { AppLookupParams } from '../../api/GraphqlClient';
|
||||
import { ProvisioningProfileStoreInfo } from '../../appstore/Credentials.types';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../errors';
|
||||
import { validateProvisioningProfileAsync } from '../../validators/validateProvisioningProfile';
|
||||
import { formatProvisioningProfileFromApple } from '../ProvisioningProfileUtils';
|
||||
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
|
||||
import {
|
||||
assignBuildCredentialsAsync,
|
||||
getBuildCredentialsAsync,
|
||||
getProvisioningProfileAsync,
|
||||
} from './BuildCredentialsUtils';
|
||||
import { ConfigureProvisioningProfile } from './ConfigureProvisioningProfile';
|
||||
import { CreateProvisioningProfile } from './CreateProvisioningProfile';
|
||||
import { SetupDistributionCertificate } from './SetupDistributionCertificate';
|
||||
|
||||
export class SetupProvisioningProfile {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
|
||||
|
||||
async areBuildCredentialsSetupAsync(ctx: Context): Promise<boolean> {
|
||||
const buildCredentials = await this.getBuildCredentialsAsync(ctx);
|
||||
if (!buildCredentials) {
|
||||
return false;
|
||||
}
|
||||
const { distributionCertificate, provisioningProfile } = buildCredentials;
|
||||
if (!distributionCertificate || !provisioningProfile) {
|
||||
return false;
|
||||
}
|
||||
return await this.isCurrentProfileValidAsync(ctx, provisioningProfile, distributionCertificate);
|
||||
}
|
||||
|
||||
async getBuildCredentialsAsync(ctx: Context): Promise<IosAppBuildCredentialsFragment | null> {
|
||||
const appCredentials = await ctx.newIos.getIosAppCredentialsWithBuildCredentialsAsync(
|
||||
this.app,
|
||||
{
|
||||
iosDistributionType: IosDistributionType.AppStore,
|
||||
}
|
||||
);
|
||||
if (!appCredentials || appCredentials.iosAppBuildCredentialsArray.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const [buildCredentials] = appCredentials.iosAppBuildCredentialsArray;
|
||||
return buildCredentials;
|
||||
const buildCredentials = await getBuildCredentialsAsync(ctx, this.app, this.distributionType);
|
||||
return await validateProvisioningProfileAsync(ctx, this.app, buildCredentials);
|
||||
}
|
||||
|
||||
async assignNewAndDeleteOldProfileAsync(
|
||||
@@ -67,7 +48,13 @@ export class SetupProvisioningProfile {
|
||||
const provisioningProfile = await new CreateProvisioningProfile(this.app, distCert).runAsync(
|
||||
ctx
|
||||
);
|
||||
return await this.assignBuildCredentialsAsync(ctx, distCert, provisioningProfile);
|
||||
return await assignBuildCredentialsAsync(
|
||||
ctx,
|
||||
this.app,
|
||||
this.distributionType,
|
||||
distCert,
|
||||
provisioningProfile
|
||||
);
|
||||
}
|
||||
|
||||
async configureAndAssignProfileAsync(
|
||||
@@ -84,45 +71,24 @@ export class SetupProvisioningProfile {
|
||||
if (!updatedProvisioningProfile) {
|
||||
return null;
|
||||
}
|
||||
return await this.assignBuildCredentialsAsync(ctx, distCert, updatedProvisioningProfile);
|
||||
}
|
||||
|
||||
async assignBuildCredentialsAsync(
|
||||
ctx: Context,
|
||||
distCert: AppleDistributionCertificateFragment,
|
||||
provisioningProfile: AppleProvisioningProfileFragment
|
||||
): Promise<IosAppBuildCredentialsFragment> {
|
||||
const appleTeam = nullthrows(await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app));
|
||||
const appleAppIdentifier = await ctx.newIos.createOrGetExistingAppleAppIdentifierAsync(
|
||||
return await assignBuildCredentialsAsync(
|
||||
ctx,
|
||||
this.app,
|
||||
appleTeam
|
||||
this.distributionType,
|
||||
distCert,
|
||||
updatedProvisioningProfile
|
||||
);
|
||||
return await ctx.newIos.createOrUpdateIosAppBuildCredentialsAsync(this.app, {
|
||||
appleTeam,
|
||||
appleAppIdentifierId: appleAppIdentifier.id,
|
||||
appleDistributionCertificateId: distCert.id,
|
||||
appleProvisioningProfileId: provisioningProfile.id,
|
||||
iosDistributionType: IosDistributionType.AppStore,
|
||||
});
|
||||
}
|
||||
|
||||
async getProvisioningProfileAsync(
|
||||
ctx: Context
|
||||
): Promise<AppleProvisioningProfileFragment | null> {
|
||||
const buildCredentials = await this.getBuildCredentialsAsync(ctx);
|
||||
if (!buildCredentials) {
|
||||
return null;
|
||||
}
|
||||
return buildCredentials.provisioningProfile ?? null;
|
||||
}
|
||||
async runAsync(ctx: Context): Promise<IosAppBuildCredentialsFragment> {
|
||||
const distCert = await new SetupDistributionCertificate(
|
||||
this.app,
|
||||
this.distributionType
|
||||
).runAsync(ctx);
|
||||
|
||||
async runAsync(
|
||||
manager: CredentialsManager,
|
||||
ctx: Context
|
||||
): Promise<IosAppBuildCredentialsFragment> {
|
||||
const areBuildCredentialsSetup = await this.areBuildCredentialsSetupAsync(ctx);
|
||||
if (areBuildCredentialsSetup) {
|
||||
return nullthrows(await this.getBuildCredentialsAsync(ctx));
|
||||
return nullthrows(await getBuildCredentialsAsync(ctx, this.app, this.distributionType));
|
||||
}
|
||||
if (ctx.nonInteractive) {
|
||||
throw new MissingCredentialsNonInteractiveError(
|
||||
@@ -130,11 +96,7 @@ export class SetupProvisioningProfile {
|
||||
);
|
||||
}
|
||||
|
||||
const distCertAction = new SetupDistributionCertificate(this.app, IosDistributionType.AppStore);
|
||||
await distCertAction.runAsync(manager, ctx);
|
||||
const distCert = distCertAction.distributionCertificate;
|
||||
|
||||
const currentProfile = await this.getProvisioningProfileAsync(ctx);
|
||||
const currentProfile = await getProvisioningProfileAsync(ctx, this.app, this.distributionType);
|
||||
if (!currentProfile) {
|
||||
return await this.createAndAssignProfileAsync(ctx, distCert);
|
||||
}
|
||||
@@ -170,37 +132,6 @@ export class SetupProvisioningProfile {
|
||||
return updatedProfile;
|
||||
}
|
||||
|
||||
private async isCurrentProfileValidAsync(
|
||||
ctx: Context,
|
||||
currentProfile: AppleProvisioningProfileFragment,
|
||||
currentCertificate: AppleDistributionCertificateFragment
|
||||
): Promise<boolean> {
|
||||
const { provisioningProfile, developerPortalIdentifier } = currentProfile;
|
||||
const { certificateP12, certificatePassword } = currentCertificate;
|
||||
if (!provisioningProfile || !certificateP12 || !certificatePassword) {
|
||||
return false;
|
||||
}
|
||||
const validationResult = await validateProvisioningProfileAsync(
|
||||
ctx,
|
||||
{
|
||||
provisioningProfile,
|
||||
...(developerPortalIdentifier
|
||||
? { provisioningProfileId: developerPortalIdentifier }
|
||||
: null),
|
||||
},
|
||||
{
|
||||
certP12: certificateP12,
|
||||
certPassword: certificatePassword,
|
||||
},
|
||||
this.app.bundleIdentifier
|
||||
);
|
||||
if (!validationResult.ok) {
|
||||
Log.warn(validationResult.error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private getCurrentProfileStoreInfo(
|
||||
profiles: ProvisioningProfileStoreInfo[],
|
||||
currentProfile: AppleProvisioningProfileFragment
|
||||
|
||||
+2
-5
@@ -2,16 +2,13 @@ import {
|
||||
AppleDistributionCertificateFragment,
|
||||
IosDistributionType,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { Action, CredentialsManager } from '../../../../CredentialsManager';
|
||||
import { testDistCertFragmentOneDependency } from '../../../../__tests__/fixtures-ios';
|
||||
import { Context } from '../../../../context';
|
||||
import { AppLookupParams } from '../../../api/GraphqlClient';
|
||||
export class SetupDistributionCertificate implements Action {
|
||||
export class SetupDistributionCertificate {
|
||||
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
|
||||
|
||||
public get distributionCertificate(): AppleDistributionCertificateFragment {
|
||||
public async runAsync(_ctx: Context): Promise<AppleDistributionCertificateFragment> {
|
||||
return testDistCertFragmentOneDependency;
|
||||
}
|
||||
|
||||
public async runAsync(_manager: CredentialsManager, _ctx: Context): Promise<void> {}
|
||||
}
|
||||
+3
-3
@@ -6,8 +6,8 @@ import {
|
||||
testProvisioningProfile,
|
||||
testProvisioningProfileFragment,
|
||||
} from '../../../../__tests__/fixtures-ios';
|
||||
import { ManageIosBeta } from '../../../../manager/ManageIosBeta';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../../errors';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { ConfigureProvisioningProfile } from '../ConfigureProvisioningProfile';
|
||||
jest.mock('../../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
@@ -29,7 +29,7 @@ describe('ConfigureProvisioningProfile', () => {
|
||||
useExistingProvisioningProfileAsync: jest.fn(() => mockProvisioningProfileFromApple),
|
||||
},
|
||||
});
|
||||
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const provProfConfigurator = new ConfigureProvisioningProfile(
|
||||
appLookupParams,
|
||||
testDistCertFragmentNoDependencies,
|
||||
@@ -46,7 +46,7 @@ describe('ConfigureProvisioningProfile', () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
});
|
||||
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const provProfConfigurator = new ConfigureProvisioningProfile(
|
||||
appLookupParams,
|
||||
testDistCertFragmentNoDependencies,
|
||||
|
||||
+3
-3
@@ -5,8 +5,8 @@ import {
|
||||
testDistCertFragmentNoDependencies,
|
||||
testProvisioningProfile,
|
||||
} from '../../../../__tests__/fixtures-ios';
|
||||
import { ManageIosBeta } from '../../../../manager/ManageIosBeta';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../../errors';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { CreateProvisioningProfile } from '../CreateProvisioningProfile';
|
||||
jest.mock('../../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
@@ -22,7 +22,7 @@ describe('CreateProvisioningProfile', () => {
|
||||
createProvisioningProfileAsync: jest.fn(() => testProvisioningProfile),
|
||||
},
|
||||
});
|
||||
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const createProvProfAction = new CreateProvisioningProfile(
|
||||
appLookupParams,
|
||||
testDistCertFragmentNoDependencies
|
||||
@@ -38,7 +38,7 @@ describe('CreateProvisioningProfile', () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
});
|
||||
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const createProvProfAction = new CreateProvisioningProfile(
|
||||
appLookupParams,
|
||||
testDistCertFragmentNoDependencies
|
||||
|
||||
+5
-5
@@ -4,7 +4,7 @@ import {
|
||||
testDistCertFragmentNoDependencies,
|
||||
testDistCertFragmentOneDependency,
|
||||
} from '../../../../__tests__/fixtures-ios';
|
||||
import { ManageIosBeta } from '../../../../manager/ManageIosBeta';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { RemoveDistributionCertificate } from '../RemoveDistributionCertificate';
|
||||
|
||||
jest.mock('../../../../../prompts');
|
||||
@@ -13,7 +13,7 @@ jest.mock('../../../../../prompts');
|
||||
describe('RemoveDistributionCertificate', () => {
|
||||
it('deletes the distribution certificate on Expo and Apple servers when there are no App Dependencies in Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: false });
|
||||
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const removeDistCertAction = new RemoveDistributionCertificate(
|
||||
appLookupParams.account,
|
||||
testDistCertFragmentNoDependencies
|
||||
@@ -29,7 +29,7 @@ describe('RemoveDistributionCertificate', () => {
|
||||
});
|
||||
it('deletes the distribution certificate on Expo servers when there are no App Dependencies in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: true });
|
||||
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const removeDistCertAction = new RemoveDistributionCertificate(
|
||||
appLookupParams.account,
|
||||
testDistCertFragmentNoDependencies
|
||||
@@ -45,7 +45,7 @@ describe('RemoveDistributionCertificate', () => {
|
||||
});
|
||||
it('deletes the distribution certificate and its provisioning profile on Expo and Apple servers when there are App Dependencies in Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: false });
|
||||
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const removeDistCertAction = new RemoveDistributionCertificate(
|
||||
appLookupParams.account,
|
||||
testDistCertFragmentOneDependency
|
||||
@@ -61,7 +61,7 @@ describe('RemoveDistributionCertificate', () => {
|
||||
});
|
||||
it('errors when the distribution certificate has App Dependencies in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: true });
|
||||
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const removeDistCertAction = new RemoveDistributionCertificate(
|
||||
appLookupParams.account,
|
||||
testDistCertFragmentOneDependency
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
import { createCtxMock } from '../../../../__tests__/fixtures-context';
|
||||
import { ManageIosBeta } from '../../../../manager/ManageIosBeta';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { RemoveProvisioningProfiles } from '../RemoveProvisioningProfile';
|
||||
|
||||
describe('RemoveProvisioningProfile', () => {
|
||||
it('Basic Case', async () => {
|
||||
const ctx = createCtxMock();
|
||||
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const testProvisioningProfile = {
|
||||
id: 'test-id',
|
||||
developerPortalIdentifier: 'test-developer-portal-identifier',
|
||||
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import { asMock } from '../../../../../__tests__/utils';
|
||||
import { IosDistributionType } from '../../../../../graphql/generated';
|
||||
import { confirmAsync } from '../../../../../prompts';
|
||||
import { createCtxMock } from '../../../../__tests__/fixtures-context';
|
||||
import {
|
||||
testAppleAppIdentifierFragment,
|
||||
testDistCert,
|
||||
testDistCertFragmentNoDependencies,
|
||||
testIosAppCredentialsWithBuildCredentialsQueryResult,
|
||||
testProvisioningProfile,
|
||||
testProvisioningProfileFragment,
|
||||
} from '../../../../__tests__/fixtures-ios';
|
||||
import { getNewIosApiMockWithoutCredentials } from '../../../../__tests__/fixtures-new-ios';
|
||||
import { readIosCredentialsAsync } from '../../../../credentialsJson/read';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { SetupBuildCredentialsFromCredentialsJson } from '../SetupBuildCredentialsFromCredentialsJson';
|
||||
jest.mock('../../../../../prompts');
|
||||
jest.mock('../../../../credentialsJson/read');
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
asMock(confirmAsync).mockReset();
|
||||
asMock(readIosCredentialsAsync).mockReset();
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
(readIosCredentialsAsync as jest.Mock).mockImplementation(() => ({
|
||||
provisioningProfile: testProvisioningProfile.provisioningProfile,
|
||||
distributionCertificate: {
|
||||
certP12: testDistCert.certP12,
|
||||
certPassword: testDistCert.certPassword,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
describe('SetupBuildCredentialsFromCredentialsJson', () => {
|
||||
it('sets up build credentials with same prior configuration in Interactive Mode', async () => {
|
||||
// configure to be the same creds that are returned by readIosCredentialsAsync in the mock above
|
||||
// create deep clone the quick and dirty way
|
||||
const testBuildCreds = JSON.parse(
|
||||
JSON.stringify(testIosAppCredentialsWithBuildCredentialsQueryResult)
|
||||
);
|
||||
testBuildCreds.iosAppBuildCredentialsArray[0].distributionCertificate.certificateP12 =
|
||||
testDistCert.certP12;
|
||||
testBuildCreds.iosAppBuildCredentialsArray[0].provisioningProfile.provisioningProfile =
|
||||
testProvisioningProfile.provisioningProfile;
|
||||
const ctx = createCtxMock({
|
||||
newIos: {
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(() => testBuildCreds),
|
||||
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
|
||||
createDistributionCertificateAsync: jest.fn(() => ({ testDistCertFragmentNoDependencies })),
|
||||
createProvisioningProfileAsync: jest.fn(() => testProvisioningProfileFragment),
|
||||
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(() => testBuildCreds),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsFromCredentialsJsonAction = new SetupBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore
|
||||
);
|
||||
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
|
||||
|
||||
// expect build credentials not to be created or updated on expo servers
|
||||
expect((ctx.newIos.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(0);
|
||||
// expect distribution certificate not to be uploaded on expo servers
|
||||
expect((ctx.newIos.createDistributionCertificateAsync as any).mock.calls.length).toBe(0);
|
||||
// expect provisioning profile not to be uploaded on expo servers
|
||||
expect((ctx.newIos.createProvisioningProfileAsync as any).mock.calls.length).toBe(0);
|
||||
});
|
||||
it('sets up build credentials with no prior configuration in Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
newIos: {
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(() => null),
|
||||
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
|
||||
createDistributionCertificateAsync: jest.fn(() => ({ testDistCertFragmentNoDependencies })),
|
||||
createProvisioningProfileAsync: jest.fn(() => testProvisioningProfileFragment),
|
||||
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(
|
||||
() => testIosAppCredentialsWithBuildCredentialsQueryResult
|
||||
),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsFromCredentialsJsonAction = new SetupBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore
|
||||
);
|
||||
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
|
||||
|
||||
// expect build credentials to be created or updated on expo servers
|
||||
expect((ctx.newIos.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(1);
|
||||
// expect distribution certificate to be uploaded on expo servers
|
||||
expect((ctx.newIos.createDistributionCertificateAsync as any).mock.calls.length).toBe(1);
|
||||
// expect provisioning profile to be uploaded on expo servers
|
||||
expect((ctx.newIos.createProvisioningProfileAsync as any).mock.calls.length).toBe(1);
|
||||
});
|
||||
it('sets up build credentials with different prior configuration in Interactive Mode', async () => {
|
||||
// create deep clone the quick and dirty way
|
||||
const testBuildCreds = JSON.parse(
|
||||
JSON.stringify(testIosAppCredentialsWithBuildCredentialsQueryResult)
|
||||
);
|
||||
testBuildCreds.iosAppBuildCredentialsArray[0].distributionCertificate.certificateP12 =
|
||||
'something different so SetupBuildCredentialsFromCredentialsJson doesnt think we want to exact the same cert and skip upload to expo servers';
|
||||
const ctx = createCtxMock({
|
||||
newIos: {
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(() => testBuildCreds),
|
||||
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
|
||||
createDistributionCertificateAsync: jest.fn(() => ({ testDistCertFragmentNoDependencies })),
|
||||
createProvisioningProfileAsync: jest.fn(() => testProvisioningProfileFragment),
|
||||
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(() => testBuildCreds),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsFromCredentialsJsonAction = new SetupBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore
|
||||
);
|
||||
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
|
||||
|
||||
// expect build credentials to be created or updated on expo servers
|
||||
expect((ctx.newIos.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(1);
|
||||
// expect distribution certificate to be uploaded on expo servers
|
||||
expect((ctx.newIos.createDistributionCertificateAsync as any).mock.calls.length).toBe(1);
|
||||
// expect provisioning profile to be uploaded on expo servers
|
||||
expect((ctx.newIos.createProvisioningProfileAsync as any).mock.calls.length).toBe(1);
|
||||
});
|
||||
it('works in Non Interactive Mode - sets up build credentials with different prior configuration', async () => {
|
||||
// create deep clone the quick and dirty way
|
||||
const testBuildCreds = JSON.parse(
|
||||
JSON.stringify(testIosAppCredentialsWithBuildCredentialsQueryResult)
|
||||
);
|
||||
testBuildCreds.iosAppBuildCredentialsArray[0].distributionCertificate.certificateP12 =
|
||||
'something different so SetupBuildCredentialsFromCredentialsJson doesnt think we want to exact the same cert and skip upload to expo servers';
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
newIos: {
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(() => testBuildCreds),
|
||||
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
|
||||
createDistributionCertificateAsync: jest.fn(() => ({ testDistCertFragmentNoDependencies })),
|
||||
createProvisioningProfileAsync: jest.fn(() => testProvisioningProfileFragment),
|
||||
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(() => testBuildCreds),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsFromCredentialsJsonAction = new SetupBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore
|
||||
);
|
||||
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
|
||||
|
||||
// expect no prompts to be called
|
||||
expect((confirmAsync as any).mock.calls.length).toBe(0);
|
||||
// expect build credentials to be created or updated on expo servers
|
||||
expect((ctx.newIos.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(1);
|
||||
// expect distribution certificate to be uploaded on expo servers
|
||||
expect((ctx.newIos.createDistributionCertificateAsync as any).mock.calls.length).toBe(1);
|
||||
// expect provisioning profile to be uploaded on expo servers
|
||||
expect((ctx.newIos.createProvisioningProfileAsync as any).mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
import { asMock } from '../../../../../__tests__/utils';
|
||||
import {
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { promptAsync } from '../../../../../prompts';
|
||||
import { getAppstoreMock, testAuthCtx } from '../../../../__tests__/fixtures-appstore';
|
||||
import { createCtxMock } from '../../../../__tests__/fixtures-context';
|
||||
import { getAllBuildCredentialsAsync } from '../BuildCredentialsUtils';
|
||||
import { SetupAdhocProvisioningProfile } from '../SetupAdhocProvisioningProfile';
|
||||
import { SetupInternalProvisioningProfile } from '../SetupInternalProvisioningProfile';
|
||||
import { SetupProvisioningProfile } from '../SetupProvisioningProfile';
|
||||
|
||||
jest.mock('../../../../../prompts');
|
||||
jest.mock('../SetupAdhocProvisioningProfile');
|
||||
jest.mock('../SetupProvisioningProfile');
|
||||
jest.mock('../BuildCredentialsUtils', () => ({ getAllBuildCredentialsAsync: jest.fn() }));
|
||||
|
||||
beforeEach(() => {
|
||||
asMock(promptAsync).mockReset();
|
||||
|
||||
asMock(getAllBuildCredentialsAsync).mockReset();
|
||||
asMock(getAllBuildCredentialsAsync).mockImplementation(() => {
|
||||
throw new Error(
|
||||
`unhandled getAllBuildCredentialsAsync call - this shouldn't happen - fix tests!`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const testAdhocBuildCredentials: IosAppBuildCredentialsFragment = {
|
||||
id: 'test-app-build-credentials-id-1',
|
||||
iosDistributionType: IosDistributionType.AdHoc,
|
||||
};
|
||||
const testEnterpriseBuildCredentials: IosAppBuildCredentialsFragment = {
|
||||
id: 'test-app-build-credentials-id-2',
|
||||
iosDistributionType: IosDistributionType.Enterprise,
|
||||
};
|
||||
|
||||
describe(SetupInternalProvisioningProfile, () => {
|
||||
describe('interactive mode', () => {
|
||||
describe('when authenticated with apple', () => {
|
||||
it('runs the SetupAdhocProvisioningProfile action for non-enterprise team', async () => {
|
||||
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
|
||||
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
|
||||
return buildCredentials;
|
||||
});
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
});
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
|
||||
authCtx: testAuthCtx,
|
||||
},
|
||||
});
|
||||
|
||||
const runAsync = jest.fn();
|
||||
SetupAdhocProvisioningProfile.prototype.runAsync = runAsync;
|
||||
|
||||
await action.runAsync(ctx);
|
||||
|
||||
expect(runAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('asks the user for an action to run when they have access to an enterprise team', async () => {
|
||||
asMock(promptAsync).mockImplementationOnce(() => ({
|
||||
distributionType: IosDistributionType.Enterprise,
|
||||
}));
|
||||
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
|
||||
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
|
||||
return buildCredentials;
|
||||
});
|
||||
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
});
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
ensureAuthenticatedAsync: jest.fn(() => ({
|
||||
...testAuthCtx,
|
||||
team: { ...testAuthCtx.team, inHouse: true },
|
||||
})),
|
||||
authCtx: { ...testAuthCtx, team: { ...testAuthCtx.team, inHouse: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const runAsync = jest.fn();
|
||||
SetupProvisioningProfile.prototype.runAsync = runAsync;
|
||||
|
||||
await action.runAsync(ctx);
|
||||
|
||||
expect(runAsync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('when not authenticated with apple', () => {
|
||||
it('asks the user for an action to run when both adhoc and universal distribution credentials exist', async () => {
|
||||
asMock(promptAsync).mockImplementationOnce(() => ({
|
||||
distributionType: IosDistributionType.Enterprise,
|
||||
}));
|
||||
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
|
||||
const buildCredentials: IosAppBuildCredentialsFragment[] = [
|
||||
testAdhocBuildCredentials,
|
||||
testEnterpriseBuildCredentials,
|
||||
];
|
||||
return buildCredentials;
|
||||
});
|
||||
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
});
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
ensureAuthenticatedAsync: jest.fn(() => null),
|
||||
authCtx: null,
|
||||
},
|
||||
});
|
||||
|
||||
const runAsync = jest.fn();
|
||||
SetupProvisioningProfile.prototype.runAsync = runAsync;
|
||||
|
||||
await action.runAsync(ctx);
|
||||
|
||||
expect(runAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('runs the SetupAdhocProvisioningProfile action when adhoc credentials exist', async () => {
|
||||
asMock(promptAsync).mockImplementationOnce(() => ({
|
||||
distributionType: IosDistributionType.Enterprise,
|
||||
}));
|
||||
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
|
||||
const buildCredentials: IosAppBuildCredentialsFragment[] = [testAdhocBuildCredentials];
|
||||
return buildCredentials;
|
||||
});
|
||||
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
});
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
ensureAuthenticatedAsync: jest.fn(() => null),
|
||||
authCtx: null,
|
||||
},
|
||||
});
|
||||
|
||||
const runAsync = jest.fn();
|
||||
SetupAdhocProvisioningProfile.prototype.runAsync = runAsync;
|
||||
|
||||
await action.runAsync(ctx);
|
||||
|
||||
expect(runAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('runs the SetupProvisioningProfile action when enterprise credentials exist', async () => {
|
||||
asMock(promptAsync).mockImplementationOnce(() => ({
|
||||
distributionType: IosDistributionType.Enterprise,
|
||||
}));
|
||||
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
|
||||
const buildCredentials: IosAppBuildCredentialsFragment[] = [
|
||||
testEnterpriseBuildCredentials,
|
||||
];
|
||||
return buildCredentials;
|
||||
});
|
||||
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
});
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
ensureAuthenticatedAsync: jest.fn(() => null),
|
||||
authCtx: null,
|
||||
},
|
||||
});
|
||||
|
||||
const runAsync = jest.fn();
|
||||
SetupProvisioningProfile.prototype.runAsync = runAsync;
|
||||
|
||||
await action.runAsync(ctx);
|
||||
|
||||
expect(runAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('forces the apple authentication when neither adhoc nor enterprise credentials exist', async () => {
|
||||
asMock(promptAsync).mockImplementationOnce(() => ({
|
||||
distributionType: IosDistributionType.Enterprise,
|
||||
}));
|
||||
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
|
||||
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
|
||||
return buildCredentials;
|
||||
});
|
||||
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
});
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
|
||||
authCtx: null,
|
||||
},
|
||||
});
|
||||
|
||||
await action.runAsync(ctx);
|
||||
|
||||
expect(ctx.appStore.ensureAuthenticatedAsync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('non-interactive mode', () => {
|
||||
it('throws an error when both adhoc and enterprise credentials are set up', async () => {
|
||||
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
|
||||
const buildCredentials: IosAppBuildCredentialsFragment[] = [
|
||||
testAdhocBuildCredentials,
|
||||
testEnterpriseBuildCredentials,
|
||||
];
|
||||
return buildCredentials;
|
||||
});
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
});
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
});
|
||||
await expect(action.runAsync(ctx)).rejects.toThrow(
|
||||
/You have set up both adhoc and universal distribution credentials/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when neither adhoc nor enterprise credentials are set up', async () => {
|
||||
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
|
||||
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
|
||||
return buildCredentials;
|
||||
});
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
});
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
});
|
||||
await expect(action.runAsync(ctx)).rejects.toThrow(/couldn't find any credentials/);
|
||||
});
|
||||
|
||||
it('runs the SetupAdhocProvisioningProfile action when adhoc credentials exist', async () => {
|
||||
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
|
||||
const buildCredentials: IosAppBuildCredentialsFragment[] = [testAdhocBuildCredentials];
|
||||
return buildCredentials;
|
||||
});
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
});
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
const runAsync = jest.fn();
|
||||
SetupAdhocProvisioningProfile.prototype.runAsync = runAsync;
|
||||
|
||||
await action.runAsync(ctx);
|
||||
|
||||
expect(runAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('runs the SetupProvisioningProfile action when enterprise credentials exist', async () => {
|
||||
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
|
||||
const buildCredentials: IosAppBuildCredentialsFragment[] = [testEnterpriseBuildCredentials];
|
||||
return buildCredentials;
|
||||
});
|
||||
const action = new SetupInternalProvisioningProfile({
|
||||
account: { id: 'account-id', name: 'account-name' },
|
||||
bundleIdentifier: 'com.expo.test',
|
||||
projectName: 'testproject',
|
||||
});
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
const runAsync = jest.fn();
|
||||
SetupProvisioningProfile.prototype.runAsync = runAsync;
|
||||
|
||||
await action.runAsync(ctx);
|
||||
|
||||
expect(runAsync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
+43
-35
@@ -1,15 +1,18 @@
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { IosDistributionType } from '../../../../../graphql/generated';
|
||||
import { confirmAsync } from '../../../../../prompts';
|
||||
import { getAppstoreMock, testAuthCtx } from '../../../../__tests__/fixtures-appstore';
|
||||
import { createCtxMock, createManagerMock } from '../../../../__tests__/fixtures-context';
|
||||
import { createCtxMock } from '../../../../__tests__/fixtures-context';
|
||||
import {
|
||||
testAppleAppIdentifierFragment,
|
||||
testIosAppBuildCredentialsFragment,
|
||||
testIosAppCredentialsWithBuildCredentialsQueryResult,
|
||||
} from '../../../../__tests__/fixtures-ios';
|
||||
import { getNewIosApiMockWithoutCredentials } from '../../../../__tests__/fixtures-new-ios';
|
||||
import { ManageIosBeta } from '../../../../manager/ManageIosBeta';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../../errors';
|
||||
import { validateProvisioningProfileAsync } from '../../../validators/validateProvisioningProfile';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { SetupProvisioningProfile } from '../SetupProvisioningProfile';
|
||||
jest.mock('../../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
@@ -20,11 +23,7 @@ jest.mock('../../../validators/validateProvisioningProfile');
|
||||
|
||||
describe('SetupProvisioningProfile', () => {
|
||||
it('repairs existing Provisioning Profile with bad build credentials in Interactive Mode', async () => {
|
||||
(validateProvisioningProfileAsync as jest.Mock).mockImplementation(() => ({
|
||||
error: 'testing: everything is fine, ignore this',
|
||||
ok: false,
|
||||
}));
|
||||
const manager = createManagerMock();
|
||||
(validateProvisioningProfileAsync as jest.Mock).mockImplementation(() => false);
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
@@ -33,9 +32,10 @@ describe('SetupProvisioningProfile', () => {
|
||||
authCtx: testAuthCtx,
|
||||
listProvisioningProfilesAsync: jest.fn(() => [
|
||||
{
|
||||
provisioningProfileId:
|
||||
provisioningProfileId: nullthrows(
|
||||
testIosAppCredentialsWithBuildCredentialsQueryResult.iosAppBuildCredentialsArray[0]
|
||||
.provisioningProfile.developerPortalIdentifier,
|
||||
.provisioningProfile
|
||||
).developerPortalIdentifier,
|
||||
},
|
||||
]),
|
||||
},
|
||||
@@ -50,9 +50,12 @@ describe('SetupProvisioningProfile', () => {
|
||||
),
|
||||
},
|
||||
});
|
||||
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
|
||||
const setupProvisioningProfileAction = new SetupProvisioningProfile(appLookupParams);
|
||||
await setupProvisioningProfileAction.runAsync(manager, ctx);
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupProvisioningProfileAction = new SetupProvisioningProfile(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore
|
||||
);
|
||||
await setupProvisioningProfileAction.runAsync(ctx);
|
||||
|
||||
// expect build credentials to be created or updated on expo servers
|
||||
expect((ctx.newIos.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(1);
|
||||
@@ -60,11 +63,7 @@ describe('SetupProvisioningProfile', () => {
|
||||
expect((ctx.newIos.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(0);
|
||||
});
|
||||
it('sets up a new Provisioning Profile with bad build credentials in Interactive Mode', async () => {
|
||||
(validateProvisioningProfileAsync as jest.Mock).mockImplementation(() => ({
|
||||
error: 'testing: everything is fine, ignore this',
|
||||
ok: false,
|
||||
}));
|
||||
const manager = createManagerMock();
|
||||
(validateProvisioningProfileAsync as jest.Mock).mockImplementation(() => false);
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
@@ -81,9 +80,12 @@ describe('SetupProvisioningProfile', () => {
|
||||
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
|
||||
},
|
||||
});
|
||||
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
|
||||
const setupProvisioningProfileAction = new SetupProvisioningProfile(appLookupParams);
|
||||
await setupProvisioningProfileAction.runAsync(manager, ctx);
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupProvisioningProfileAction = new SetupProvisioningProfile(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore
|
||||
);
|
||||
await setupProvisioningProfileAction.runAsync(ctx);
|
||||
|
||||
// expect build credentials to be created or updated on expo servers
|
||||
expect((ctx.newIos.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(1);
|
||||
@@ -91,10 +93,7 @@ describe('SetupProvisioningProfile', () => {
|
||||
expect((ctx.newIos.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(1);
|
||||
});
|
||||
it('skips setting up a Provisioning Profile with prior build credentials configured properly in Interactive Mode', async () => {
|
||||
(validateProvisioningProfileAsync as jest.Mock).mockImplementation(() => ({
|
||||
ok: true,
|
||||
}));
|
||||
const manager = createManagerMock();
|
||||
(validateProvisioningProfileAsync as jest.Mock).mockImplementation(() => true);
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
@@ -111,9 +110,12 @@ describe('SetupProvisioningProfile', () => {
|
||||
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
|
||||
},
|
||||
});
|
||||
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
|
||||
const setupProvisioningProfileAction = new SetupProvisioningProfile(appLookupParams);
|
||||
await setupProvisioningProfileAction.runAsync(manager, ctx);
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupProvisioningProfileAction = new SetupProvisioningProfile(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore
|
||||
);
|
||||
await setupProvisioningProfileAction.runAsync(ctx);
|
||||
|
||||
// expect build credentials not to be created or updated on expo servers
|
||||
expect((ctx.newIos.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(0);
|
||||
@@ -121,7 +123,7 @@ describe('SetupProvisioningProfile', () => {
|
||||
expect((ctx.newIos.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(0);
|
||||
});
|
||||
it('sets up a Provisioning Profile with no prior build credentials configured in Interactive Mode', async () => {
|
||||
const manager = createManagerMock();
|
||||
(validateProvisioningProfileAsync as jest.Mock).mockImplementation(() => false);
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
@@ -134,9 +136,12 @@ describe('SetupProvisioningProfile', () => {
|
||||
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
|
||||
},
|
||||
});
|
||||
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
|
||||
const setupProvisioningProfileAction = new SetupProvisioningProfile(appLookupParams);
|
||||
await setupProvisioningProfileAction.runAsync(manager, ctx);
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupProvisioningProfileAction = new SetupProvisioningProfile(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore
|
||||
);
|
||||
await setupProvisioningProfileAction.runAsync(ctx);
|
||||
|
||||
// expect build credentials to be created or updated on expo servers
|
||||
expect((ctx.newIos.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(1);
|
||||
@@ -144,13 +149,16 @@ describe('SetupProvisioningProfile', () => {
|
||||
expect((ctx.newIos.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(0);
|
||||
});
|
||||
it('errors in Non Interactive Mode', async () => {
|
||||
const manager = createManagerMock();
|
||||
(validateProvisioningProfileAsync as jest.Mock).mockImplementation(() => false);
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
});
|
||||
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
|
||||
const setupProvisioningProfileAction = new SetupProvisioningProfile(appLookupParams);
|
||||
await expect(setupProvisioningProfileAction.runAsync(manager, ctx)).rejects.toThrowError(
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupProvisioningProfileAction = new SetupProvisioningProfile(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore
|
||||
);
|
||||
await expect(setupProvisioningProfileAction.runAsync(ctx)).rejects.toThrowError(
|
||||
MissingCredentialsNonInteractiveError
|
||||
);
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ interface CredentialsCache {
|
||||
};
|
||||
}
|
||||
|
||||
export default class iOSApi {
|
||||
export default class IosApi {
|
||||
client = new ApiClient();
|
||||
credentials: CredentialsCache = {};
|
||||
isPrefetched: { [accountName: string]: boolean } = {};
|
||||
|
||||
@@ -3,6 +3,7 @@ import nullthrows from 'nullthrows';
|
||||
import {
|
||||
AppFragment,
|
||||
AppleAppIdentifierFragment,
|
||||
AppleDeviceFragment,
|
||||
AppleDistributionCertificateFragment,
|
||||
AppleTeamFragment,
|
||||
CommonIosAppCredentialsFragment,
|
||||
@@ -26,10 +27,7 @@ import { IosAppBuildCredentialsMutation } from './graphql/mutations/IosAppBuildC
|
||||
import { IosAppCredentialsMutation } from './graphql/mutations/IosAppCredentialsMutation';
|
||||
import { AppQuery } from './graphql/queries/AppQuery';
|
||||
import { AppleAppIdentifierQuery } from './graphql/queries/AppleAppIdentifierQuery';
|
||||
import {
|
||||
AppleDeviceFragmentWithAppleTeam,
|
||||
AppleDeviceQuery,
|
||||
} from './graphql/queries/AppleDeviceQuery';
|
||||
import { AppleDeviceQuery } from './graphql/queries/AppleDeviceQuery';
|
||||
import { AppleDistributionCertificateQuery } from './graphql/queries/AppleDistributionCertificateQuery';
|
||||
import {
|
||||
AppleProvisioningProfileQuery,
|
||||
@@ -100,7 +98,7 @@ export async function createOrUpdateIosAppBuildCredentialsAsync(
|
||||
|
||||
export async function getIosAppCredentialsWithBuildCredentialsAsync(
|
||||
appLookupParams: AppLookupParams,
|
||||
{ iosDistributionType }: { iosDistributionType: IosDistributionType }
|
||||
{ iosDistributionType }: { iosDistributionType?: IosDistributionType }
|
||||
): Promise<IosAppCredentialsWithBuildCredentialsQueryResult | null> {
|
||||
const { account, bundleIdentifier } = appLookupParams;
|
||||
const appleAppIdentifier = await AppleAppIdentifierQuery.byBundleIdentifierAsync(
|
||||
@@ -215,9 +213,12 @@ export async function createOrGetExistingAppleAppIdentifierAsync(
|
||||
|
||||
export async function getDevicesForAppleTeamAsync(
|
||||
{ account }: AppLookupParams,
|
||||
{ appleTeamIdentifier }: AppleTeamFragment
|
||||
): Promise<AppleDeviceFragmentWithAppleTeam[]> {
|
||||
return await AppleDeviceQuery.getAllByAppleTeamIdentifierAsync(account.id, appleTeamIdentifier);
|
||||
{ appleTeamIdentifier }: AppleTeamFragment,
|
||||
{ useCache = true }: { useCache?: boolean } = {}
|
||||
): Promise<AppleDeviceFragment[]> {
|
||||
return await AppleDeviceQuery.getAllByAppleTeamIdentifierAsync(account.id, appleTeamIdentifier, {
|
||||
useCache,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createProvisioningProfileAsync(
|
||||
|
||||
@@ -34,7 +34,8 @@ export type AppleDevicesByIdentifierQueryResult = AppleDeviceQueryResult & {
|
||||
const AppleDeviceQuery = {
|
||||
async getAllByAppleTeamIdentifierAsync(
|
||||
accountId: string,
|
||||
appleTeamIdentifier: string
|
||||
appleTeamIdentifier: string,
|
||||
{ useCache = true }: { useCache?: boolean } = {}
|
||||
): Promise<AppleDeviceFragmentWithAppleTeam[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
@@ -63,7 +64,10 @@ const AppleDeviceQuery = {
|
||||
accountId,
|
||||
appleTeamIdentifier,
|
||||
},
|
||||
{ additionalTypenames: ['AppleDevice'] }
|
||||
{
|
||||
additionalTypenames: ['AppleDevice'],
|
||||
requestPolicy: useCache ? 'cache-first' : 'network-only',
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
@@ -66,7 +66,7 @@ const IosAppCredentialsQuery = {
|
||||
iosDistributionType,
|
||||
}: {
|
||||
appleAppIdentifierId: string;
|
||||
iosDistributionType: IosDistributionType;
|
||||
iosDistributionType?: IosDistributionType;
|
||||
}
|
||||
): Promise<IosAppCredentialsWithBuildCredentialsQueryResult | null> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
@@ -76,7 +76,7 @@ const IosAppCredentialsQuery = {
|
||||
query IosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery(
|
||||
$projectFullName: String!
|
||||
$appleAppIdentifierId: String!
|
||||
$iosDistributionType: IosDistributionType!
|
||||
$iosDistributionType: IosDistributionType
|
||||
) {
|
||||
app {
|
||||
byFullName(fullName: $projectFullName) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { syncCapabilities } from '../ensureAppExists';
|
||||
|
||||
describe(syncCapabilities, () => {
|
||||
test("bundleId.updateBundleIdCapabilityAsync should not be called if there's noting to update", async () => {
|
||||
const bundleId = {
|
||||
hasCapabilityAsync: jest.fn().mockImplementation(() => {
|
||||
return {};
|
||||
}),
|
||||
updateBundleIdCapabilityAsync: jest.fn(),
|
||||
} as any;
|
||||
await syncCapabilities(bundleId, { enablePushNotifications: true });
|
||||
expect(bundleId.updateBundleIdCapabilityAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
test('bundleId.updateBundleIdCapabilityAsync should be called if there are some changes to be made', async () => {
|
||||
const bundleId = {
|
||||
hasCapabilityAsync: jest.fn().mockImplementation(() => {
|
||||
return null;
|
||||
}),
|
||||
updateBundleIdCapabilityAsync: jest.fn(),
|
||||
} as any;
|
||||
await syncCapabilities(bundleId, { enablePushNotifications: true });
|
||||
expect(bundleId.updateBundleIdCapabilityAsync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -36,7 +36,7 @@ export async function ensureBundleIdExistsWithNameAsync(
|
||||
options?: EnsureAppExistsOptions
|
||||
) {
|
||||
const context = getRequestContext(authCtx);
|
||||
let spinner = ora(`Linking bundle identifier ${chalk.dim(bundleIdentifier)}`).start();
|
||||
const spinner = ora(`Linking bundle identifier ${chalk.dim(bundleIdentifier)}`).start();
|
||||
|
||||
let bundleId: BundleId | null;
|
||||
try {
|
||||
@@ -72,23 +72,33 @@ export async function ensureBundleIdExistsWithNameAsync(
|
||||
}
|
||||
|
||||
if (options) {
|
||||
try {
|
||||
spinner = ora(`Syncing capabilities`).start();
|
||||
await syncCapabilities(bundleId, options);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the capabilities
|
||||
export async function syncCapabilities(
|
||||
bundleId: BundleId,
|
||||
options: EnsureAppExistsOptions
|
||||
): Promise<void> {
|
||||
const spinner = ora(`Syncing capabilities`).start();
|
||||
|
||||
try {
|
||||
const capability = await bundleId.hasCapabilityAsync(CapabilityType.PUSH_NOTIFICATIONS);
|
||||
if (!capability && options.enablePushNotifications) {
|
||||
await bundleId.updateBundleIdCapabilityAsync({
|
||||
capabilityType: CapabilityType.PUSH_NOTIFICATIONS,
|
||||
option: options.enablePushNotifications
|
||||
? CapabilityTypeOption.ON
|
||||
: CapabilityTypeOption.OFF,
|
||||
// TODO: Add more capabilities
|
||||
option: CapabilityTypeOption.ON,
|
||||
});
|
||||
} else if (capability && !options.enablePushNotifications) {
|
||||
await bundleId.updateBundleIdCapabilityAsync({
|
||||
capabilityType: CapabilityType.PUSH_NOTIFICATIONS,
|
||||
option: CapabilityTypeOption.OFF,
|
||||
});
|
||||
spinner.succeed(`Synced capabilities`);
|
||||
} catch (err) {
|
||||
spinner.fail(`Failed to sync capabilities ${chalk.dim(bundleIdentifier)}`);
|
||||
|
||||
throw err;
|
||||
}
|
||||
spinner.succeed(`Synced capabilities`);
|
||||
} catch (err) {
|
||||
spinner.fail(`Failed to sync capabilities ${chalk.dim(bundleId.attributes.identifier)}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import mockdate from 'mockdate';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import Log from '../../../../log';
|
||||
import { IosAppCredentialsQuery } from '../../api/graphql/queries/IosAppCredentialsQuery';
|
||||
@@ -18,7 +19,7 @@ describe('print credentials', () => {
|
||||
appleAppIdentifierId: 'test-id',
|
||||
}
|
||||
);
|
||||
displayIosAppCredentials(testIosAppCredentialsData);
|
||||
displayIosAppCredentials(nullthrows(testIosAppCredentialsData));
|
||||
const loggedSoFar = (Log.log as jest.Mock).mock.calls.reduce(
|
||||
(acc, mockValue) => acc + mockValue
|
||||
);
|
||||
|
||||
@@ -2,10 +2,12 @@ import chalk from 'chalk';
|
||||
import dateformat from 'dateformat';
|
||||
|
||||
import {
|
||||
AppleDeviceFragment,
|
||||
CommonIosAppCredentialsFragment,
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
} from '../../../graphql/generated';
|
||||
import { APPLE_DEVICE_CLASS_LABELS } from '../../../graphql/types/credentials/AppleDevice';
|
||||
import Log from '../../../log';
|
||||
import { fromNow } from '../../../utils/date';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
@@ -79,12 +81,25 @@ export function displayIosAppCredentials(credentials: CommonIosAppCredentialsFra
|
||||
}
|
||||
}
|
||||
|
||||
export function displayProjectCredentials(
|
||||
appLookupParams: AppLookupParams,
|
||||
buildCredentials: IosAppBuildCredentialsFragment
|
||||
): void {
|
||||
const experienceName = `@${appLookupParams.account.name}/${appLookupParams.projectName}`;
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Project Credentials Configuration:'));
|
||||
Log.log(` Project: ${chalk.bold(experienceName)}`);
|
||||
Log.log(` Bundle Identifier: ${appLookupParams.bundleIdentifier}`);
|
||||
displayIosAppBuildCredentials(buildCredentials);
|
||||
}
|
||||
|
||||
function displayIosAppBuildCredentials(buildCredentials: IosAppBuildCredentialsFragment): void {
|
||||
Log.log(
|
||||
chalk.bold(
|
||||
` Configuration: ${prettyIosDistributionType(buildCredentials.iosDistributionType)}`
|
||||
)
|
||||
);
|
||||
Log.newLine();
|
||||
const maybeDistCert = buildCredentials.distributionCertificate;
|
||||
Log.log(` Distribution Certificate:`);
|
||||
if (maybeDistCert) {
|
||||
@@ -106,7 +121,14 @@ function displayIosAppBuildCredentials(buildCredentials: IosAppBuildCredentialsF
|
||||
const maybeProvProf = buildCredentials.provisioningProfile;
|
||||
Log.log(` Provisioning Profile:`);
|
||||
if (maybeProvProf) {
|
||||
const { expiration, updatedAt, status, developerPortalIdentifier, appleTeam } = maybeProvProf;
|
||||
const {
|
||||
expiration,
|
||||
updatedAt,
|
||||
status,
|
||||
developerPortalIdentifier,
|
||||
appleTeam,
|
||||
appleDevices,
|
||||
} = maybeProvProf;
|
||||
if (developerPortalIdentifier) {
|
||||
Log.log(` Developer Portal ID: ${developerPortalIdentifier}`);
|
||||
}
|
||||
@@ -118,9 +140,42 @@ function displayIosAppBuildCredentials(buildCredentials: IosAppBuildCredentialsF
|
||||
` Apple Team: ${appleTeamIdentifier} ${appleTeamName ? `(${appleTeamName})` : ''}`
|
||||
);
|
||||
}
|
||||
if (appleDevices && appleDevices.length > 0) {
|
||||
Log.log(` Provisioned devices:`);
|
||||
for (const appleDevice of appleDevices) {
|
||||
Log.log(` - ${formatAppleDevice(appleDevice)}`);
|
||||
}
|
||||
}
|
||||
Log.log(` Updated ${fromNow(new Date(updatedAt))} ago`);
|
||||
} else {
|
||||
Log.log(` None assigned yet`);
|
||||
}
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
function formatAppleDevice(device: AppleDeviceFragment): string {
|
||||
let deviceString = '';
|
||||
if (device.name) {
|
||||
deviceString += device.name;
|
||||
}
|
||||
if (device.deviceClass || device.model) {
|
||||
const deviceDetails = [
|
||||
device.deviceClass && APPLE_DEVICE_CLASS_LABELS[device.deviceClass],
|
||||
device.model,
|
||||
]
|
||||
.filter(i => i)
|
||||
.join(' ');
|
||||
if (deviceString === '') {
|
||||
deviceString += deviceDetails;
|
||||
} else {
|
||||
deviceString += ` - ${deviceDetails}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceString === '') {
|
||||
deviceString += device.identifier;
|
||||
} else {
|
||||
deviceString += ` (UDID: ${device.identifier})`;
|
||||
}
|
||||
return deviceString;
|
||||
}
|
||||
@@ -1,121 +1,128 @@
|
||||
import { PlistArray, PlistObject } from '@expo/plist';
|
||||
import assert from 'assert';
|
||||
import crypto from 'crypto';
|
||||
import minimatch from 'minimatch';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { IosAppBuildCredentialsFragment, IosDistributionType } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Context } from '../../context';
|
||||
import { DistributionCertificate, ProvisioningProfile } from '../appstore/Credentials.types';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { ProfileClass } from '../appstore/provisioningProfile';
|
||||
import { getP12CertFingerprint } from '../utils/p12Certificate';
|
||||
import { parse as parseProvisioningProfile } from '../utils/provisioningProfile';
|
||||
|
||||
interface ValidationResult {
|
||||
error?: string;
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
export async function validateProvisioningProfileAsync(
|
||||
ctx: Context,
|
||||
profile: Pick<ProvisioningProfile, 'provisioningProfile' | 'provisioningProfileId'>,
|
||||
distCert: Pick<DistributionCertificate, 'certP12' | 'certPassword'>,
|
||||
bundleIdentifier: string
|
||||
): Promise<ValidationResult> {
|
||||
const resultWithoutApple = validateProvisioningProfileWithoutApple(
|
||||
profile,
|
||||
distCert,
|
||||
bundleIdentifier
|
||||
);
|
||||
app: AppLookupParams,
|
||||
buildCredentials: Partial<IosAppBuildCredentialsFragment> | null
|
||||
): Promise<boolean> {
|
||||
if (
|
||||
!buildCredentials ||
|
||||
!buildCredentials.distributionCertificate ||
|
||||
!buildCredentials.provisioningProfile
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// already failed, return early
|
||||
if (!resultWithoutApple.ok) {
|
||||
return resultWithoutApple;
|
||||
const resultWithoutApple = validateProvisioningProfileWithoutApple(app, buildCredentials);
|
||||
if (!resultWithoutApple) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ctx.appStore.authCtx) {
|
||||
Log.warn(
|
||||
"Skipping Provisioning Profile validation on Apple Servers because we aren't authenticated."
|
||||
);
|
||||
return resultWithoutApple;
|
||||
return true;
|
||||
}
|
||||
|
||||
return await validateProvisioningProfileWithAppleAsync(ctx, profile, bundleIdentifier);
|
||||
return await validateProvisioningProfileWithAppleAsync(ctx, app, buildCredentials);
|
||||
}
|
||||
|
||||
async function validateProvisioningProfileWithAppleAsync(
|
||||
ctx: Context,
|
||||
profile: Pick<ProvisioningProfile, 'provisioningProfile' | 'provisioningProfileId'>,
|
||||
bundleIdentifier: string
|
||||
): Promise<ValidationResult> {
|
||||
const profilesFromApple = await ctx.appStore.listProvisioningProfilesAsync(bundleIdentifier);
|
||||
|
||||
const configuredProfileFromApple = profilesFromApple.find(appleProfile =>
|
||||
profile.provisioningProfileId
|
||||
? appleProfile.provisioningProfileId === profile.provisioningProfileId
|
||||
: appleProfile.provisioningProfile === profile.provisioningProfile
|
||||
);
|
||||
if (!configuredProfileFromApple) {
|
||||
return {
|
||||
error: `Provisioning profile (id: ${profile.provisioningProfileId}) does not exist in Apple Dev Portal`,
|
||||
ok: false,
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export function validateProvisioningProfileWithoutApple(
|
||||
provisioningProfile: Pick<ProvisioningProfile, 'provisioningProfile' | 'provisioningProfileId'>,
|
||||
distCert: Pick<DistributionCertificate, 'certP12' | 'certPassword'>,
|
||||
bundleIdentifier: string
|
||||
): ValidationResult {
|
||||
function validateProvisioningProfileWithoutApple(
|
||||
app: AppLookupParams,
|
||||
{ provisioningProfile, distributionCertificate }: Partial<IosAppBuildCredentialsFragment>
|
||||
): boolean {
|
||||
try {
|
||||
const profilePlist = parseProvisioningProfile(provisioningProfile.provisioningProfile);
|
||||
const profilePlist = parseProvisioningProfile(
|
||||
nullthrows(provisioningProfile?.provisioningProfile)
|
||||
);
|
||||
|
||||
let distCertFingerprint: string;
|
||||
try {
|
||||
distCertFingerprint = getP12CertFingerprint(distCert.certP12, distCert.certPassword);
|
||||
distCertFingerprint = getP12CertFingerprint(
|
||||
nullthrows(distributionCertificate?.certificateP12),
|
||||
nullthrows(distributionCertificate?.certificatePassword)
|
||||
);
|
||||
} catch (e) {
|
||||
return {
|
||||
error: `Failed to calculate fingerprint for Distribution Certificate: ${e.toString()}`,
|
||||
ok: false,
|
||||
};
|
||||
Log.warn(`Failed to calculate fingerprint for Distribution Certificate: ${e.toString()}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const devCertStatus = validateDeveloperCertificate(profilePlist, distCertFingerprint);
|
||||
if (!devCertStatus.ok) {
|
||||
return devCertStatus;
|
||||
if (!devCertStatus) {
|
||||
return false;
|
||||
}
|
||||
const bundleIdentifierStatus = validateBundleIdentifier(profilePlist, bundleIdentifier);
|
||||
if (!bundleIdentifierStatus.ok) {
|
||||
return bundleIdentifierStatus;
|
||||
|
||||
const bundleIdentifierStatus = validateBundleIdentifier(profilePlist, app.bundleIdentifier);
|
||||
if (!bundleIdentifierStatus) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isExpired = new Date(profilePlist['ExpirationDate'] as string) <= new Date();
|
||||
if (isExpired) {
|
||||
return {
|
||||
error: 'Provisioning Profile has expired.',
|
||||
ok: false,
|
||||
};
|
||||
Log.warn('Provisioning Profile has expired.');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
error: 'Provisioning Profile is malformed.',
|
||||
ok: false,
|
||||
};
|
||||
Log.warn('Provisioning Profile is malformed.');
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
async function validateProvisioningProfileWithAppleAsync(
|
||||
ctx: Context,
|
||||
app: AppLookupParams,
|
||||
buildCredentials: Partial<IosAppBuildCredentialsFragment>
|
||||
): Promise<boolean> {
|
||||
assert(buildCredentials.provisioningProfile, 'Provisioning Profile must be defined');
|
||||
const { developerPortalIdentifier, provisioningProfile } = buildCredentials.provisioningProfile;
|
||||
|
||||
const profilesFromApple = await ctx.appStore.listProvisioningProfilesAsync(
|
||||
app.bundleIdentifier,
|
||||
buildCredentials.iosDistributionType === IosDistributionType.AdHoc
|
||||
? ProfileClass.Adhoc
|
||||
: ProfileClass.General
|
||||
);
|
||||
|
||||
const configuredProfileFromApple = profilesFromApple.find(appleProfile =>
|
||||
developerPortalIdentifier
|
||||
? appleProfile.provisioningProfileId === developerPortalIdentifier
|
||||
: appleProfile.provisioningProfile === provisioningProfile
|
||||
);
|
||||
if (!configuredProfileFromApple) {
|
||||
Log.warn(
|
||||
`Provisioning profile (id: ${developerPortalIdentifier}) does not exist in Apple Developer Portal`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (configuredProfileFromApple.status !== 'ACTIVE') {
|
||||
Log.warn(`Provisioning profile (id: ${developerPortalIdentifier}) is no longer valid`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateDeveloperCertificate(
|
||||
plistData: PlistObject,
|
||||
distCertFingerprint: string
|
||||
): ValidationResult {
|
||||
): boolean {
|
||||
const devCertBase64 = (plistData?.DeveloperCertificates as PlistArray)?.[0] as string;
|
||||
if (!devCertBase64) {
|
||||
return {
|
||||
error: 'Missing certificate fingerprint in provisioning profile.',
|
||||
ok: false,
|
||||
};
|
||||
Log.warn('Missing certificate fingerprint in provisioning profile.');
|
||||
return false;
|
||||
}
|
||||
const devCertBuffer = Buffer.from(devCertBase64, 'base64');
|
||||
const devCertFingerprint = crypto
|
||||
@@ -125,40 +132,34 @@ function validateDeveloperCertificate(
|
||||
.toUpperCase();
|
||||
|
||||
if (devCertFingerprint !== distCertFingerprint) {
|
||||
return {
|
||||
error: 'Provisioning profile is not associated with uploaded Distribution Certificate.',
|
||||
ok: false,
|
||||
};
|
||||
Log.warn('Provisioning profile is not associated with uploaded Distribution Certificate.');
|
||||
return false;
|
||||
}
|
||||
return { ok: true };
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateBundleIdentifier(
|
||||
plistData: PlistObject,
|
||||
expectedBundleIdentifier: string
|
||||
): ValidationResult {
|
||||
): boolean {
|
||||
const actualApplicationIdentifier = (plistData.Entitlements as PlistObject)?.[
|
||||
'application-identifier'
|
||||
] as string;
|
||||
if (!actualApplicationIdentifier) {
|
||||
return {
|
||||
error: 'Missing application-identifier in provisioning profile entitlements',
|
||||
ok: false,
|
||||
};
|
||||
Log.warn('Missing application-identifier in provisioning profile entitlements');
|
||||
return false;
|
||||
}
|
||||
const actualBundleIdentifier = /\.(.+)/.exec(actualApplicationIdentifier)?.[1] as string;
|
||||
if (!actualBundleIdentifier) {
|
||||
return {
|
||||
error: 'Malformed application-identifier field in provisioning profile',
|
||||
ok: false,
|
||||
};
|
||||
Log.warn('Malformed application-identifier field in provisioning profile');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!minimatch(expectedBundleIdentifier, actualBundleIdentifier)) {
|
||||
return {
|
||||
error: `Wrong bundleIdentifier found in provisioning profile; expected: ${expectedBundleIdentifier}, found (in provisioning profile): ${actualBundleIdentifier}`,
|
||||
ok: false,
|
||||
};
|
||||
Log.warn(
|
||||
`Wrong bundleIdentifier found in provisioning profile; expected: ${expectedBundleIdentifier}, found (in provisioning profile): ${actualBundleIdentifier}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return { ok: true };
|
||||
return true;
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { UpdateCredentialsJson } from '../ios/actions/UpdateCredentialsJson';
|
||||
import { UpdateDistributionCertificate } from '../ios/actions/UpdateDistributionCertificate';
|
||||
import { UseExistingDistributionCertificate } from '../ios/actions/UseDistributionCertificate';
|
||||
import { getAppLookupParamsFromContext } from '../ios/actions/new/BuildCredentialsUtils';
|
||||
import { AppLookupParams } from '../ios/credentials';
|
||||
import { displayAllIosCredentials } from '../ios/utils/printCredentials';
|
||||
import { PressAnyKeyToContinue } from './HelperActions';
|
||||
@@ -91,7 +92,7 @@ export class ManageIos implements Action {
|
||||
});
|
||||
|
||||
try {
|
||||
await manager.runActionAsync(this.getAction(manager, ctx, accountName, action));
|
||||
await this.getAction(ctx, accountName, action).runAsync(manager, ctx);
|
||||
} catch (err) {
|
||||
Log.error(err);
|
||||
}
|
||||
@@ -114,12 +115,7 @@ export class ManageIos implements Action {
|
||||
return { accountName, projectName, bundleIdentifier };
|
||||
}
|
||||
|
||||
private getAction(
|
||||
manager: CredentialsManager,
|
||||
ctx: Context,
|
||||
accountName: string,
|
||||
action: ActionType
|
||||
): Action {
|
||||
private getAction(ctx: Context, accountName: string, action: ActionType): Action<unknown> {
|
||||
switch (action) {
|
||||
case ActionType.CreateDistributionCertificate:
|
||||
return new CreateDistributionCertificateStandaloneManager(accountName);
|
||||
@@ -132,8 +128,8 @@ export class ManageIos implements Action {
|
||||
return new UseExistingDistributionCertificate(app);
|
||||
}
|
||||
case ActionType.SetupBuildCredentials: {
|
||||
const app = this.getAppLookupParamsFromContext(ctx);
|
||||
return new SetupBuildCredentials(app, 'store');
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
return new SetupBuildCredentials({ app: appLookupParams, distribution: 'store' });
|
||||
}
|
||||
case ActionType.RemoveSpecificProvisioningProfile: {
|
||||
const app = this.getAppLookupParamsFromContext(ctx);
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import { EasConfig, EasJsonReader } from '@expo/eas-json';
|
||||
|
||||
import Log from '../../log';
|
||||
import { getProjectAccountName, getProjectConfigDescription } from '../../project/projectUtils';
|
||||
import { getProjectAccountName } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { Account, findAccountByName } from '../../user/Account';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { ensureActorHasUsername } from '../../user/actions';
|
||||
import { Action, CredentialsManager } from '../CredentialsManager';
|
||||
import { Context } from '../context';
|
||||
import { SetupBuildCredentials } from '../ios/actions/SetupBuildCredentials';
|
||||
import { getAppLookupParamsFromContext } from '../ios/actions/new/BuildCredentialsUtils';
|
||||
import { SelectAndRemoveDistributionCertificate } from '../ios/actions/new/RemoveDistributionCertificate';
|
||||
import { SetupBuildCredentialsFromCredentialsJson } from '../ios/actions/new/SetupBuildCredentialsFromCredentialsJson';
|
||||
import { AppLookupParams } from '../ios/api/GraphqlClient';
|
||||
import {
|
||||
displayEmptyIosCredentials,
|
||||
displayIosAppCredentials,
|
||||
} from '../ios/utils/printCredentialsBeta';
|
||||
import { PressAnyKeyToContinue } from './HelperActions';
|
||||
import { SelectBuildProfileFromEasJson } from './SelectBuildProfileFromEasJson';
|
||||
import { SelectIosDistributionTypeGraphqlFromBuildProfile } from './SelectIosDistributionTypeGraphqlFromBuildProfile';
|
||||
|
||||
enum ActionType {
|
||||
SetupBuildCredentials,
|
||||
@@ -39,7 +46,7 @@ export class ManageIosBeta implements Action {
|
||||
throw new Error(`You do not have access to account: ${accountName}`);
|
||||
}
|
||||
if (ctx.hasProjectContext) {
|
||||
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const iosAppCredentials = await ctx.newIos.getIosAppCredentialsWithCommonFieldsAsync(
|
||||
appLookupParams
|
||||
);
|
||||
@@ -51,9 +58,19 @@ export class ManageIosBeta implements Action {
|
||||
}
|
||||
|
||||
const projectSpecificActions: { value: ActionType; title: string }[] = ctx.hasProjectContext
|
||||
? []
|
||||
? [
|
||||
{
|
||||
// This command will be triggered during build to ensure all credentials are ready
|
||||
// I'm leaving it here for now to simplify testing
|
||||
value: ActionType.SetupBuildCredentials,
|
||||
title: 'Ensure all credentials for project are valid',
|
||||
},
|
||||
{
|
||||
value: ActionType.SetupBuildCredentialsFromCredentialsJson,
|
||||
title: 'Update credentials on EAS servers with values from credentials.json',
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const { action } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'action',
|
||||
@@ -67,7 +84,25 @@ export class ManageIosBeta implements Action {
|
||||
],
|
||||
});
|
||||
try {
|
||||
await manager.runActionAsync(this.getAction(ctx, account, action));
|
||||
const isProjectSpecific = projectSpecificActions.find(
|
||||
actionItem => actionItem.value === action
|
||||
);
|
||||
if (isProjectSpecific) {
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const easJsonReader = await new EasJsonReader(ctx.projectDir, 'ios');
|
||||
const easConfig = await new SelectBuildProfileFromEasJson(easJsonReader).runAsync(ctx);
|
||||
await this.runProjectSpecificActionAsync(
|
||||
manager,
|
||||
ctx,
|
||||
appLookupParams,
|
||||
easConfig,
|
||||
action
|
||||
);
|
||||
} else if (action === ActionType.RemoveDistributionCertificate) {
|
||||
await new SelectAndRemoveDistributionCertificate(account).runAsync(ctx);
|
||||
} else {
|
||||
throw new Error('Unknown action selected');
|
||||
}
|
||||
} catch (err) {
|
||||
Log.error(err);
|
||||
}
|
||||
@@ -75,38 +110,42 @@ export class ManageIosBeta implements Action {
|
||||
} catch (err) {
|
||||
Log.error(err);
|
||||
await manager.runActionAsync(new PressAnyKeyToContinue());
|
||||
} finally {
|
||||
// TODO remove finally block before committing!
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static getAppLookupParamsFromContext(ctx: Context): AppLookupParams {
|
||||
ctx.ensureProjectContext();
|
||||
const projectName = ctx.exp.slug;
|
||||
const accountName = getProjectAccountName(ctx.exp, ctx.user);
|
||||
const account = findAccountByName(ctx.user.accounts, accountName);
|
||||
if (!account) {
|
||||
throw new Error(`You do not have access to account: ${accountName}`);
|
||||
}
|
||||
|
||||
const bundleIdentifier = ctx.exp.ios?.bundleIdentifier;
|
||||
if (!bundleIdentifier) {
|
||||
throw new Error(
|
||||
`ios.bundleIdentifier needs to be defined in your ${getProjectConfigDescription(
|
||||
ctx.projectDir
|
||||
)} file`
|
||||
);
|
||||
}
|
||||
|
||||
return { account, projectName, bundleIdentifier };
|
||||
}
|
||||
|
||||
private getAction(ctx: Context, account: Account, action: ActionType): Action {
|
||||
private async runProjectSpecificActionAsync(
|
||||
manager: CredentialsManager,
|
||||
ctx: Context,
|
||||
appLookupParams: AppLookupParams,
|
||||
easConfig: EasConfig,
|
||||
action: ActionType
|
||||
): Promise<void> {
|
||||
switch (action) {
|
||||
case ActionType.RemoveDistributionCertificate:
|
||||
return new SelectAndRemoveDistributionCertificate(account);
|
||||
case ActionType.SetupBuildCredentials: {
|
||||
const iosDistributionTypeEasConfig = easConfig.builds.ios?.distribution;
|
||||
if (!iosDistributionTypeEasConfig) {
|
||||
throw new Error(`The distributionType field is required in your iOS build profile`);
|
||||
}
|
||||
await new SetupBuildCredentials({
|
||||
app: appLookupParams,
|
||||
distribution: iosDistributionTypeEasConfig,
|
||||
}).runAsync(manager, ctx);
|
||||
return;
|
||||
}
|
||||
case ActionType.SetupBuildCredentialsFromCredentialsJson: {
|
||||
const iosAppCredentials = await ctx.newIos.getIosAppCredentialsWithCommonFieldsAsync(
|
||||
appLookupParams
|
||||
);
|
||||
const iosDistributionTypeGraphql = await new SelectIosDistributionTypeGraphqlFromBuildProfile(
|
||||
easConfig
|
||||
).runAsync(ctx, iosAppCredentials);
|
||||
await new SetupBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams,
|
||||
iosDistributionTypeGraphql
|
||||
).runAsync(ctx);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new Error('Unknown action selected');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { EasConfig, EasJsonReader } from '@expo/eas-json';
|
||||
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { Context } from '../context';
|
||||
|
||||
export class SelectBuildProfileFromEasJson {
|
||||
constructor(private easJsonReader: EasJsonReader) {}
|
||||
async runAsync(ctx: Context): Promise<EasConfig> {
|
||||
const profileName = await this.getProfileNameFromEasConfigAsync(ctx);
|
||||
const easConfig = await this.easJsonReader.readAsync(profileName);
|
||||
Log.succeed(`Using build profile: ${profileName}`);
|
||||
return easConfig;
|
||||
}
|
||||
|
||||
async getProfileNameFromEasConfigAsync(ctx: Context): Promise<string> {
|
||||
const easJson = await this.easJsonReader.readRawAsync();
|
||||
const buildProfileNames = Object.keys(easJson.builds?.ios || {});
|
||||
if (buildProfileNames.length === 0) {
|
||||
throw new Error(
|
||||
'You need at least one iOS build profile declared in eas.json. Go to https://docs.expo.io/build/eas-json/ for more details'
|
||||
);
|
||||
} else if (buildProfileNames.length === 1) {
|
||||
return buildProfileNames[0];
|
||||
}
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(
|
||||
'You have multiple build profiles. Please run this command in interactive mode.'
|
||||
);
|
||||
}
|
||||
const { profileName } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'profileName',
|
||||
message: 'Which build profile do you want to configure?',
|
||||
choices: buildProfileNames.map(profileName => ({ value: profileName, title: profileName })),
|
||||
});
|
||||
return profileName;
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { EasConfig } from '@expo/eas-json';
|
||||
import uniq from 'lodash/uniq';
|
||||
|
||||
import {
|
||||
CommonIosAppCredentialsFragment,
|
||||
IosDistributionType as IosDistributionTypeGraphql,
|
||||
} from '../../graphql/generated';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { Context } from '../context';
|
||||
|
||||
export class SelectIosDistributionTypeGraphqlFromBuildProfile {
|
||||
constructor(private easConfig: EasConfig) {}
|
||||
async runAsync(
|
||||
ctx: Context,
|
||||
iosAppCredentials: CommonIosAppCredentialsFragment | null
|
||||
): Promise<IosDistributionTypeGraphql> {
|
||||
const iosDistributionTypeEasConfig = this.easConfig.builds.ios?.distribution;
|
||||
if (!iosDistributionTypeEasConfig) {
|
||||
throw new Error(`The distributionType field is required in your iOS build profile`);
|
||||
}
|
||||
if (iosDistributionTypeEasConfig === 'simulator') {
|
||||
throw new Error('A simulator distribution does not require credentials to be configured.');
|
||||
} else if (iosDistributionTypeEasConfig === 'store') {
|
||||
return IosDistributionTypeGraphql.AppStore;
|
||||
} else return this.resolveInternalDistributionAsync(ctx, iosAppCredentials);
|
||||
}
|
||||
|
||||
async resolveInternalDistributionAsync(
|
||||
ctx: Context,
|
||||
iosAppCredentials: CommonIosAppCredentialsFragment | null
|
||||
): Promise<IosDistributionTypeGraphql> {
|
||||
// check if the type is specified in eas config
|
||||
const maybeEnterpriseProvisioning = this.easConfig.builds.ios?.enterpriseProvisioning;
|
||||
if (maybeEnterpriseProvisioning) {
|
||||
switch (maybeEnterpriseProvisioning) {
|
||||
case 'universal':
|
||||
return IosDistributionTypeGraphql.Enterprise;
|
||||
case 'adhoc':
|
||||
return IosDistributionTypeGraphql.AdHoc;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unsupported Enterprise Provisioning type: ${maybeEnterpriseProvisioning}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// only enterprise accounts support enterprise distributions
|
||||
const isDefinitelyNotAnEnterpriseAccount = ctx.appStore.authCtx?.team.inHouse === false;
|
||||
if (isDefinitelyNotAnEnterpriseAccount) {
|
||||
return IosDistributionTypeGraphql.AdHoc;
|
||||
}
|
||||
|
||||
// extrapolate type from existing app credentials
|
||||
const iosAppBuildCredentialsArray = iosAppCredentials?.iosAppBuildCredentialsArray ?? [];
|
||||
const existingInternalIosDistributionTypes = uniq(
|
||||
iosAppBuildCredentialsArray
|
||||
.map(buildCredentials => buildCredentials.iosDistributionType)
|
||||
.filter(
|
||||
distributionTypeGraphql =>
|
||||
distributionTypeGraphql === IosDistributionTypeGraphql.AdHoc ||
|
||||
distributionTypeGraphql === IosDistributionTypeGraphql.Enterprise
|
||||
)
|
||||
);
|
||||
if (existingInternalIosDistributionTypes.length === 1) {
|
||||
return existingInternalIosDistributionTypes[0];
|
||||
}
|
||||
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(
|
||||
'Unable to determine type of internal distribution. Please run this command in interactive mode.'
|
||||
);
|
||||
}
|
||||
|
||||
// ask the user as a last resort
|
||||
const { iosDistributionTypeGraphql } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'iosDistributionTypeGraphql',
|
||||
message: 'Which type of internal distribution would you like to configure?',
|
||||
choices: [
|
||||
{
|
||||
value: IosDistributionTypeGraphql.AdHoc,
|
||||
title: 'Adhoc: distribution to a small number of test users',
|
||||
},
|
||||
{
|
||||
value: IosDistributionTypeGraphql.Enterprise,
|
||||
title:
|
||||
'Enterprise: distribution to members of your company. Restricted to Apple Enterprise Accounts.',
|
||||
},
|
||||
],
|
||||
});
|
||||
return iosDistributionTypeGraphql;
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
import { EasConfig } from '@expo/eas-json';
|
||||
|
||||
import {
|
||||
CommonIosAppCredentialsFragment,
|
||||
IosDistributionType as IosDistributionTypeGraphql,
|
||||
} from '../../../graphql/generated';
|
||||
import { createCtxMock } from '../../__tests__/fixtures-context';
|
||||
import { testIosAppCredentialsWithBuildCredentialsQueryResult } from '../../__tests__/fixtures-ios';
|
||||
import { SelectIosDistributionTypeGraphqlFromBuildProfile } from '../SelectIosDistributionTypeGraphqlFromBuildProfile';
|
||||
|
||||
describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
|
||||
it('errors with a simulator distribution', async () => {
|
||||
const easConfig = {
|
||||
builds: {
|
||||
ios: {
|
||||
distribution: 'simulator',
|
||||
},
|
||||
},
|
||||
} as EasConfig;
|
||||
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
});
|
||||
const selectIosDistributionTypeGraphqlFromBuildProfileAction = new SelectIosDistributionTypeGraphqlFromBuildProfile(
|
||||
easConfig
|
||||
);
|
||||
await expect(
|
||||
selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(
|
||||
ctx,
|
||||
/* iosAppCredentials */ null
|
||||
)
|
||||
).rejects.toThrowError();
|
||||
});
|
||||
it('returns APP_STORE type with a store distribution', async () => {
|
||||
const easConfig = {
|
||||
builds: {
|
||||
ios: {
|
||||
distribution: 'store',
|
||||
},
|
||||
},
|
||||
} as EasConfig;
|
||||
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
});
|
||||
const selectIosDistributionTypeGraphqlFromBuildProfileAction = new SelectIosDistributionTypeGraphqlFromBuildProfile(
|
||||
easConfig
|
||||
);
|
||||
const iosDistributionTypeGraphql = await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(
|
||||
ctx,
|
||||
/* iosAppCredentials */ null
|
||||
);
|
||||
expect(iosDistributionTypeGraphql).toBe(IosDistributionTypeGraphql.AppStore);
|
||||
});
|
||||
it('returns ENTERPRISE type with an internal distribution with the universal provisioningEnterprise configuration', async () => {
|
||||
const easConfig = {
|
||||
builds: {
|
||||
ios: {
|
||||
distribution: 'internal',
|
||||
enterpriseProvisioning: 'universal',
|
||||
},
|
||||
},
|
||||
} as EasConfig;
|
||||
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
});
|
||||
const selectIosDistributionTypeGraphqlFromBuildProfileAction = new SelectIosDistributionTypeGraphqlFromBuildProfile(
|
||||
easConfig
|
||||
);
|
||||
const iosDistributionTypeGraphql = await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(
|
||||
ctx,
|
||||
/* iosAppCredentials */ null
|
||||
);
|
||||
expect(iosDistributionTypeGraphql).toBe(IosDistributionTypeGraphql.Enterprise);
|
||||
});
|
||||
it('returns ADHOC type with an internal distribution with the adhoc provisioningEnterprise configuration', async () => {
|
||||
const easConfig = {
|
||||
builds: {
|
||||
ios: {
|
||||
distribution: 'internal',
|
||||
enterpriseProvisioning: 'adhoc',
|
||||
},
|
||||
},
|
||||
} as EasConfig;
|
||||
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
});
|
||||
const selectIosDistributionTypeGraphqlFromBuildProfileAction = new SelectIosDistributionTypeGraphqlFromBuildProfile(
|
||||
easConfig
|
||||
);
|
||||
const iosDistributionTypeGraphql = await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(
|
||||
ctx,
|
||||
/* iosAppCredentials */ null
|
||||
);
|
||||
expect(iosDistributionTypeGraphql).toBe(IosDistributionTypeGraphql.AdHoc);
|
||||
});
|
||||
it('extrapolates type for an internal distribution with ambiguous configuration', async () => {
|
||||
const easConfig = {
|
||||
builds: {
|
||||
ios: {
|
||||
distribution: 'internal',
|
||||
},
|
||||
},
|
||||
} as EasConfig;
|
||||
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
});
|
||||
const selectIosDistributionTypeGraphqlFromBuildProfileAction = new SelectIosDistributionTypeGraphqlFromBuildProfile(
|
||||
easConfig
|
||||
);
|
||||
|
||||
// create deep clone the quick and dirty way
|
||||
const testAppCredentials = JSON.parse(
|
||||
JSON.stringify(testIosAppCredentialsWithBuildCredentialsQueryResult)
|
||||
) as CommonIosAppCredentialsFragment;
|
||||
testAppCredentials.iosAppBuildCredentialsArray[0].iosDistributionType =
|
||||
IosDistributionTypeGraphql.AdHoc;
|
||||
const iosDistributionTypeGraphql = await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(
|
||||
ctx,
|
||||
testAppCredentials
|
||||
);
|
||||
expect(iosDistributionTypeGraphql).toBe(IosDistributionTypeGraphql.AdHoc);
|
||||
});
|
||||
});
|
||||
@@ -30,17 +30,18 @@ describe(AccountResolver, () => {
|
||||
{ id: 'account_id_777', name: 'dominik' },
|
||||
{ id: 'account_id_888', name: 'foo' },
|
||||
],
|
||||
isExpoAdmin: false,
|
||||
};
|
||||
|
||||
describe('when inside project dir', () => {
|
||||
const projectDir = '/app';
|
||||
const exp = {} as any;
|
||||
|
||||
it('returns the account defined in app.json/app.config.js if user confirms', async () => {
|
||||
asMock(prompts).mockImplementationOnce(() => ({
|
||||
value: true,
|
||||
}));
|
||||
|
||||
const resolver = new AccountResolver(projectDir, user);
|
||||
const resolver = new AccountResolver(exp, user);
|
||||
const account = await resolver.resolveAccountAsync();
|
||||
expect(account).toEqual(user.accounts[1]);
|
||||
});
|
||||
@@ -53,7 +54,7 @@ describe(AccountResolver, () => {
|
||||
account: user.accounts[0],
|
||||
}));
|
||||
|
||||
const resolver = new AccountResolver(projectDir, user);
|
||||
const resolver = new AccountResolver(exp, user);
|
||||
const account = await resolver.resolveAccountAsync();
|
||||
expect(account).toEqual(user.accounts[0]);
|
||||
});
|
||||
@@ -71,7 +72,7 @@ describe(AccountResolver, () => {
|
||||
const originalConsoleWarn = console.warn;
|
||||
console.warn = jest.fn();
|
||||
|
||||
const resolver = new AccountResolver(projectDir, userWithAccessToProjectAccount);
|
||||
const resolver = new AccountResolver(exp, userWithAccessToProjectAccount);
|
||||
const account = await resolver.resolveAccountAsync();
|
||||
expect(account).toEqual(user.accounts[0]);
|
||||
expect(console.warn).toBeCalledWith(expect.stringMatching(/doesn't have access to the/));
|
||||
|
||||
@@ -19,7 +19,7 @@ export default class DeviceCreateAction {
|
||||
private appleTeam: Pick<AppleTeam, 'appleTeamIdentifier' | 'appleTeamName' | 'id'>
|
||||
) {}
|
||||
|
||||
public async runAsync(): Promise<void> {
|
||||
public async runAsync(): Promise<RegistrationMethod> {
|
||||
const method = await this.askForRegistrationMethodAsync();
|
||||
if (method === RegistrationMethod.WEBSITE) {
|
||||
await runRegistrationUrlMethodAsync(this.account.id, this.appleTeam);
|
||||
@@ -29,6 +29,7 @@ export default class DeviceCreateAction {
|
||||
Log.log('Bye!');
|
||||
process.exit(0);
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
private async askForRegistrationMethodAsync(): Promise<RegistrationMethod> {
|
||||
|
||||
@@ -19,7 +19,7 @@ Your app archive (.ipa) will be installable on your equipment as long as you sig
|
||||
The provisioning profile needs to contain the UDIDs (unique identifiers) of your iPhones and iPads.
|
||||
|
||||
First of all, please choose the Expo account under which you want to register your devices.
|
||||
Later, authenticate with Apple and choose your desired Apple Team (if you're Apple ID has access to multiple teams).`;
|
||||
Later, authenticate with Apple and choose your desired Apple Team (if your Apple ID has access to multiple teams).`;
|
||||
|
||||
export default class DeviceManager {
|
||||
constructor(private ctx: DeviceManagerContext) {}
|
||||
|
||||
@@ -66,6 +66,7 @@ export type RootQuery = {
|
||||
/** @deprecated Use 'search' root field. */
|
||||
searchUsersAndApps: Array<Maybe<SearchResult>>;
|
||||
snack: SnackQuery;
|
||||
submissions: SubmissionQuery;
|
||||
/** Top-level query object for querying UserInvitationPublicData publicly. */
|
||||
userInvitationPublicData: UserInvitationPublicDataQuery;
|
||||
/** Top-level query object for querying Users. */
|
||||
@@ -413,6 +414,8 @@ export type App = Project & {
|
||||
/** (EAS Build) Builds associated with this app */
|
||||
builds: Array<Build>;
|
||||
buildJobs: Array<BuildJob>;
|
||||
/** EAS Submissions associated with this app */
|
||||
submissions: Array<Submission>;
|
||||
/** iOS app credentials for the project */
|
||||
iosAppCredentials: Array<IosAppCredentials>;
|
||||
/** Android app credentials for the project */
|
||||
@@ -420,11 +423,11 @@ export type App = Project & {
|
||||
/** EAS channels owned by an app */
|
||||
updateChannels: Array<UpdateChannel>;
|
||||
/** get an EAS channel owned by the app by name */
|
||||
updateChannelByName: UpdateChannel;
|
||||
updateChannelByName?: Maybe<UpdateChannel>;
|
||||
/** EAS branches owned by an app */
|
||||
updateBranches: Array<UpdateBranch>;
|
||||
/** get an EAS branch owned by the app by name */
|
||||
updateBranchByName: UpdateBranch;
|
||||
updateBranchByName?: Maybe<UpdateBranch>;
|
||||
/** Coalesced project activity for an app. Use "createdBefore" to offset a query. */
|
||||
activityTimelineProjectActivities: Array<ActivityTimelineProjectActivity>;
|
||||
/** Environment secrets for an app */
|
||||
@@ -479,6 +482,14 @@ export type AppBuildJobsArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppSubmissionsArgs = {
|
||||
filter: SubmissionFilter;
|
||||
offset: Scalars['Int'];
|
||||
limit: Scalars['Int'];
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppIosAppCredentialsArgs = {
|
||||
filter?: Maybe<IosAppCredentialsFilter>;
|
||||
@@ -840,6 +851,40 @@ export enum BuildJobStatus {
|
||||
SentToQueue = 'SENT_TO_QUEUE'
|
||||
}
|
||||
|
||||
export type SubmissionFilter = {
|
||||
platform?: Maybe<AppPlatform>;
|
||||
status?: Maybe<SubmissionStatus>;
|
||||
};
|
||||
|
||||
export enum SubmissionStatus {
|
||||
InQueue = 'IN_QUEUE',
|
||||
InProgress = 'IN_PROGRESS',
|
||||
Finished = 'FINISHED',
|
||||
Errored = 'ERRORED'
|
||||
}
|
||||
|
||||
/** Represents an EAS Submission */
|
||||
export type Submission = ActivityTimelineProjectActivity & {
|
||||
__typename?: 'Submission';
|
||||
id: Scalars['ID'];
|
||||
actor?: Maybe<Actor>;
|
||||
activityTimestamp: Scalars['DateTime'];
|
||||
app?: Maybe<App>;
|
||||
initiatingActor?: Maybe<Actor>;
|
||||
platform: AppPlatform;
|
||||
status: SubmissionStatus;
|
||||
logsUrl?: Maybe<Scalars['String']>;
|
||||
error?: Maybe<SubmissionError>;
|
||||
createdAt: Scalars['DateTime'];
|
||||
updatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export type SubmissionError = {
|
||||
__typename?: 'SubmissionError';
|
||||
errorCode?: Maybe<Scalars['String']>;
|
||||
message?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type IosAppCredentialsFilter = {
|
||||
appleAppIdentifierId?: Maybe<Scalars['String']>;
|
||||
};
|
||||
@@ -1376,6 +1421,7 @@ export type BuildQuery = {
|
||||
/**
|
||||
* Get all builds for a specific app.
|
||||
* They are sorted from latest to oldest.
|
||||
* @deprecated Use App.builds instead
|
||||
*/
|
||||
allForApp: Array<Maybe<Build>>;
|
||||
};
|
||||
@@ -1427,11 +1473,22 @@ export type ExperimentationQuery = {
|
||||
|
||||
export type ProjectQuery = {
|
||||
__typename?: 'ProjectQuery';
|
||||
byAccountNameAndSlug: Project;
|
||||
/** @deprecated See byAccountNameAndSlug */
|
||||
byUsernameAndSlug: Project;
|
||||
/** @deprecated Field no longer supported */
|
||||
byPaths: Array<Maybe<Project>>;
|
||||
};
|
||||
|
||||
|
||||
export type ProjectQueryByAccountNameAndSlugArgs = {
|
||||
accountName: Scalars['String'];
|
||||
slug: Scalars['String'];
|
||||
platform?: Maybe<AppPlatform>;
|
||||
sdkVersions?: Maybe<Array<Maybe<Scalars['String']>>>;
|
||||
};
|
||||
|
||||
|
||||
export type ProjectQueryByUsernameAndSlugArgs = {
|
||||
username: Scalars['String'];
|
||||
slug: Scalars['String'];
|
||||
@@ -1478,6 +1535,17 @@ export type SnackQueryByHashIdArgs = {
|
||||
hashId: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type SubmissionQuery = {
|
||||
__typename?: 'SubmissionQuery';
|
||||
/** Look up EAS Submission by submission ID */
|
||||
byId: Submission;
|
||||
};
|
||||
|
||||
|
||||
export type SubmissionQueryByIdArgs = {
|
||||
submissionId: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type UserInvitationPublicDataQuery = {
|
||||
__typename?: 'UserInvitationPublicDataQuery';
|
||||
/** Get UserInvitationPublicData by token */
|
||||
@@ -1573,6 +1641,8 @@ export type RootMutation = {
|
||||
iosAppCredentials: IosAppCredentialsMutation;
|
||||
/** Mutations that create, update, and delete Robots */
|
||||
robot: RobotMutation;
|
||||
/** Mutations that modify an EAS Submit submission */
|
||||
submission: SubmissionMutation;
|
||||
updateChannel: UpdateChannelMutation;
|
||||
update: UpdateMutation;
|
||||
updateBranch: UpdateBranchMutation;
|
||||
@@ -1675,11 +1745,13 @@ export type AccountMutation = {
|
||||
setBuildAutoRenew?: Maybe<Account>;
|
||||
/** Set payment details */
|
||||
setPaymentSource?: Maybe<Account>;
|
||||
/** Extend offer to account */
|
||||
extendOffer?: Maybe<Account>;
|
||||
/** Send an email to primary account email */
|
||||
sendEmail?: Maybe<Account>;
|
||||
/** Require authorization to send push notifications for experiences owned by this account */
|
||||
setPushSecurityEnabled?: Maybe<Account>;
|
||||
/** Rename this account */
|
||||
/** Rename this account and the primary user's username if this account is a personal account */
|
||||
rename: Account;
|
||||
};
|
||||
|
||||
@@ -1730,6 +1802,13 @@ export type AccountMutationSetPaymentSourceArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type AccountMutationExtendOfferArgs = {
|
||||
accountName: Scalars['ID'];
|
||||
offer: StandardOffer;
|
||||
suppressMessage?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
|
||||
export type AccountMutationSendEmailArgs = {
|
||||
accountName: Scalars['ID'];
|
||||
emailTemplate: EmailTemplate;
|
||||
@@ -1747,6 +1826,17 @@ export type AccountMutationRenameArgs = {
|
||||
newName: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum StandardOffer {
|
||||
/** $29 USD per month, 30 day trial */
|
||||
Default = 'DEFAULT',
|
||||
/** $348 USD per year, 30 day trial */
|
||||
YearlySub = 'YEARLY_SUB',
|
||||
/** $29 USD per month, 1 year trial */
|
||||
YcDeals = 'YC_DEALS',
|
||||
/** $800 USD per month */
|
||||
Support = 'SUPPORT'
|
||||
}
|
||||
|
||||
export enum EmailTemplate {
|
||||
/** Able to purchase Developer Services */
|
||||
DevServicesOfferExtended = 'DEV_SERVICES_OFFER_EXTENDED',
|
||||
@@ -2296,9 +2386,16 @@ export type AndroidManagedJobInput = {
|
||||
secrets?: Maybe<AndroidJobSecretsInput>;
|
||||
builderEnvironment?: Maybe<AndroidBuilderEnvironmentInput>;
|
||||
cache?: Maybe<BuildCacheInput>;
|
||||
buildType?: Maybe<AndroidManagedBuildType>;
|
||||
username?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export enum AndroidManagedBuildType {
|
||||
Apk = 'APK',
|
||||
AppBundle = 'APP_BUNDLE',
|
||||
DevelopmentClient = 'DEVELOPMENT_CLIENT'
|
||||
}
|
||||
|
||||
export type IosGenericJobInput = {
|
||||
projectArchive: ProjectArchiveSourceInput;
|
||||
projectRootDirectory: Scalars['String'];
|
||||
@@ -2481,6 +2578,29 @@ export type DeleteRobotResult = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type SubmissionMutation = {
|
||||
__typename?: 'SubmissionMutation';
|
||||
/** Create an EAS Submit submission */
|
||||
createSubmission: CreateSubmissionResult;
|
||||
};
|
||||
|
||||
|
||||
export type SubmissionMutationCreateSubmissionArgs = {
|
||||
input: CreateSubmissionInput;
|
||||
};
|
||||
|
||||
export type CreateSubmissionInput = {
|
||||
appId: Scalars['ID'];
|
||||
platform: AppPlatform;
|
||||
config: Scalars['JSONObject'];
|
||||
};
|
||||
|
||||
export type CreateSubmissionResult = {
|
||||
__typename?: 'CreateSubmissionResult';
|
||||
/** Created submission */
|
||||
submission: Submission;
|
||||
};
|
||||
|
||||
export type UpdateChannelMutation = {
|
||||
__typename?: 'UpdateChannelMutation';
|
||||
/**
|
||||
@@ -2526,18 +2646,18 @@ export type DeleteUpdateChannelResult = {
|
||||
|
||||
export type UpdateMutation = {
|
||||
__typename?: 'UpdateMutation';
|
||||
/** Delete an EAS update */
|
||||
deleteUpdate: DeleteUpdateResult;
|
||||
/** Delete an EAS update group */
|
||||
deleteUpdateGroup: DeleteUpdateGroupResult;
|
||||
};
|
||||
|
||||
|
||||
export type UpdateMutationDeleteUpdateArgs = {
|
||||
updateId: Scalars['ID'];
|
||||
export type UpdateMutationDeleteUpdateGroupArgs = {
|
||||
group: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type DeleteUpdateResult = {
|
||||
__typename?: 'DeleteUpdateResult';
|
||||
id: Scalars['ID'];
|
||||
export type DeleteUpdateGroupResult = {
|
||||
__typename?: 'DeleteUpdateGroupResult';
|
||||
group: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type UpdateBranchMutation = {
|
||||
@@ -3002,15 +3122,6 @@ export type DeleteWebhookResult = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
export enum StandardOffer {
|
||||
/** $29 USD per month, 30 day trial */
|
||||
Default = 'DEFAULT',
|
||||
/** $29 USD per month, 1 year trial */
|
||||
YcDeals = 'YC_DEALS',
|
||||
/** $800 USD per month */
|
||||
Support = 'SUPPORT'
|
||||
}
|
||||
|
||||
export type BaseSearchResult = SearchResult & {
|
||||
__typename?: 'BaseSearchResult';
|
||||
/** @deprecated Use SearchResult instead */
|
||||
@@ -3085,10 +3196,10 @@ export type GetBranchInfoQuery = (
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
& Pick<App, 'id'>
|
||||
& { updateBranchByName: (
|
||||
& { updateBranchByName?: Maybe<(
|
||||
{ __typename?: 'UpdateBranch' }
|
||||
& Pick<UpdateBranch, 'id' | 'name'>
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
);
|
||||
@@ -3127,7 +3238,7 @@ export type BranchesByAppQuery = (
|
||||
& Pick<UpdateBranch, 'id' | 'name'>
|
||||
& { updates: Array<(
|
||||
{ __typename?: 'Update' }
|
||||
& Pick<Update, 'id' | 'updatedAt' | 'message'>
|
||||
& Pick<Update, 'id' | 'createdAt' | 'message' | 'runtimeVersion' | 'group'>
|
||||
& { actor?: Maybe<(
|
||||
{ __typename: 'User' }
|
||||
& Pick<User, 'username' | 'id'>
|
||||
@@ -3184,7 +3295,7 @@ export type ViewBranchQuery = (
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
& Pick<App, 'id'>
|
||||
& { updateBranchByName: (
|
||||
& { updateBranchByName?: Maybe<(
|
||||
{ __typename?: 'UpdateBranch' }
|
||||
& Pick<UpdateBranch, 'id' | 'name'>
|
||||
& { updates: Array<(
|
||||
@@ -3192,13 +3303,13 @@ export type ViewBranchQuery = (
|
||||
& Pick<Update, 'id' | 'group' | 'message' | 'createdAt' | 'runtimeVersion' | 'platform' | 'manifestFragment'>
|
||||
& { actor?: Maybe<(
|
||||
{ __typename?: 'User' }
|
||||
& Pick<User, 'firstName' | 'id'>
|
||||
& Pick<User, 'username' | 'id'>
|
||||
) | (
|
||||
{ __typename?: 'Robot' }
|
||||
& Pick<Robot, 'firstName' | 'id'>
|
||||
)> }
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
);
|
||||
@@ -3250,14 +3361,14 @@ export type GetChannelByNameToEditQuery = (
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
& Pick<App, 'id'>
|
||||
& { updateChannelByName: (
|
||||
& { updateChannelByName?: Maybe<(
|
||||
{ __typename?: 'UpdateChannel' }
|
||||
& Pick<UpdateChannel, 'id' | 'name'>
|
||||
& { updateBranches: Array<(
|
||||
{ __typename?: 'UpdateBranch' }
|
||||
& Pick<UpdateBranch, 'id' | 'name'>
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
);
|
||||
@@ -3295,16 +3406,16 @@ export type GetAllChannelsForAppQuery = (
|
||||
& Pick<App, 'id'>
|
||||
& { updateChannels: Array<(
|
||||
{ __typename?: 'UpdateChannel' }
|
||||
& Pick<UpdateChannel, 'id' | 'name'>
|
||||
& Pick<UpdateChannel, 'id' | 'name' | 'branchMapping'>
|
||||
& { updateBranches: Array<(
|
||||
{ __typename?: 'UpdateBranch' }
|
||||
& Pick<UpdateBranch, 'id' | 'name'>
|
||||
& { updates: Array<(
|
||||
{ __typename?: 'Update' }
|
||||
& Pick<Update, 'id' | 'group' | 'message' | 'createdAt'>
|
||||
& Pick<Update, 'id' | 'group' | 'message' | 'runtimeVersion' | 'createdAt'>
|
||||
& { actor?: Maybe<(
|
||||
{ __typename?: 'User' }
|
||||
& Pick<User, 'firstName' | 'id'>
|
||||
& Pick<User, 'username' | 'id'>
|
||||
) | (
|
||||
{ __typename?: 'Robot' }
|
||||
& Pick<Robot, 'firstName' | 'id'>
|
||||
@@ -3329,7 +3440,7 @@ export type GetChannelByNameForAppQuery = (
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
& Pick<App, 'id'>
|
||||
& { updateChannelByName: (
|
||||
& { updateChannelByName?: Maybe<(
|
||||
{ __typename?: 'UpdateChannel' }
|
||||
& Pick<UpdateChannel, 'id' | 'name' | 'createdAt' | 'branchMapping'>
|
||||
& { updateBranches: Array<(
|
||||
@@ -3337,21 +3448,57 @@ export type GetChannelByNameForAppQuery = (
|
||||
& Pick<UpdateBranch, 'id' | 'name'>
|
||||
& { updates: Array<(
|
||||
{ __typename?: 'Update' }
|
||||
& Pick<Update, 'id' | 'group' | 'message' | 'createdAt'>
|
||||
& Pick<Update, 'id' | 'group' | 'message' | 'runtimeVersion' | 'createdAt'>
|
||||
& { actor?: Maybe<(
|
||||
{ __typename?: 'User' }
|
||||
& Pick<User, 'firstName' | 'id'>
|
||||
& Pick<User, 'username' | 'id'>
|
||||
) | (
|
||||
{ __typename?: 'Robot' }
|
||||
& Pick<Robot, 'firstName' | 'id'>
|
||||
)> }
|
||||
)> }
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
);
|
||||
|
||||
export type DeleteUpdateGroupMutationVariables = Exact<{
|
||||
group: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteUpdateGroupMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { update: (
|
||||
{ __typename?: 'UpdateMutation' }
|
||||
& { deleteUpdateGroup: (
|
||||
{ __typename?: 'DeleteUpdateGroupResult' }
|
||||
& Pick<DeleteUpdateGroupResult, 'group'>
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type UpdatesByGroupQueryVariables = Exact<{
|
||||
groupId: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdatesByGroupQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { updatesByGroup: Array<(
|
||||
{ __typename?: 'Update' }
|
||||
& Pick<Update, 'id' | 'group' | 'runtimeVersion' | 'platform' | 'message' | 'createdAt'>
|
||||
& { actor?: Maybe<(
|
||||
{ __typename?: 'User' }
|
||||
& Pick<User, 'username' | 'id'>
|
||||
) | (
|
||||
{ __typename?: 'Robot' }
|
||||
& Pick<Robot, 'firstName' | 'id'>
|
||||
)> }
|
||||
)> }
|
||||
);
|
||||
|
||||
export type CreateAppleAppIdentifierMutationVariables = Exact<{
|
||||
appleAppIdentifierInput: AppleAppIdentifierInput;
|
||||
accountId: Scalars['ID'];
|
||||
@@ -3911,7 +4058,7 @@ export type IosAppCredentialsByAppIdentifierIdQuery = (
|
||||
export type IosAppCredentialsWithBuildCredentialsByAppIdentifierIdQueryVariables = Exact<{
|
||||
projectFullName: Scalars['String'];
|
||||
appleAppIdentifierId: Scalars['String'];
|
||||
iosDistributionType: IosDistributionType;
|
||||
iosDistributionType?: Maybe<IosDistributionType>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -4154,6 +4301,27 @@ export type UpdatePublishMutation = (
|
||||
) }
|
||||
);
|
||||
|
||||
export type CreateSubmissionMutationVariables = Exact<{
|
||||
appId: Scalars['ID'];
|
||||
platform: AppPlatform;
|
||||
config: Scalars['JSONObject'];
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateSubmissionMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { submission: (
|
||||
{ __typename?: 'SubmissionMutation' }
|
||||
& { createSubmission: (
|
||||
{ __typename?: 'CreateSubmissionResult' }
|
||||
& { submission: (
|
||||
{ __typename?: 'Submission' }
|
||||
& Pick<Submission, 'id'>
|
||||
) }
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type CreateUploadSessionMutationVariables = Exact<{
|
||||
type: UploadSessionType;
|
||||
}>;
|
||||
@@ -4363,6 +4531,48 @@ export type GetAssetMetadataQuery = (
|
||||
) }
|
||||
);
|
||||
|
||||
export type SubmissionsByIdQueryVariables = Exact<{
|
||||
submissionId: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SubmissionsByIdQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { submissions: (
|
||||
{ __typename?: 'SubmissionQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'Submission' }
|
||||
& Pick<Submission, 'id'>
|
||||
& SubmissionFragment
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type GetAllSubmissionsForAppQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
offset: Scalars['Int'];
|
||||
limit: Scalars['Int'];
|
||||
status?: Maybe<SubmissionStatus>;
|
||||
platform?: Maybe<AppPlatform>;
|
||||
}>;
|
||||
|
||||
|
||||
export type GetAllSubmissionsForAppQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
& Pick<App, 'id'>
|
||||
& { submissions: Array<(
|
||||
{ __typename?: 'Submission' }
|
||||
& Pick<Submission, 'id'>
|
||||
& SubmissionFragment
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
);
|
||||
|
||||
export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
@@ -4462,6 +4672,15 @@ export type EnvironmentSecretFragment = (
|
||||
& Pick<EnvironmentSecret, 'id' | 'name' | 'createdAt'>
|
||||
);
|
||||
|
||||
export type SubmissionFragment = (
|
||||
{ __typename?: 'Submission' }
|
||||
& Pick<Submission, 'id' | 'status' | 'platform' | 'logsUrl'>
|
||||
& { error?: Maybe<(
|
||||
{ __typename?: 'SubmissionError' }
|
||||
& Pick<SubmissionError, 'errorCode' | 'message'>
|
||||
)> }
|
||||
);
|
||||
|
||||
export type WebhookFragment = (
|
||||
{ __typename?: 'Webhook' }
|
||||
& Pick<Webhook, 'id' | 'event' | 'url' | 'createdAt' | 'updatedAt'>
|
||||
@@ -4517,10 +4736,12 @@ export type AppleProvisioningProfileFragment = (
|
||||
& Pick<AppleProvisioningProfile, 'id' | 'expiration' | 'developerPortalIdentifier' | 'provisioningProfile' | 'updatedAt' | 'status'>
|
||||
& { appleTeam?: Maybe<(
|
||||
{ __typename?: 'AppleTeam' }
|
||||
& Pick<AppleTeam, 'id' | 'appleTeamIdentifier' | 'appleTeamName'>
|
||||
& Pick<AppleTeam, 'id'>
|
||||
& AppleTeamFragment
|
||||
)>, appleDevices: Array<(
|
||||
{ __typename?: 'AppleDevice' }
|
||||
& Pick<AppleDevice, 'id' | 'identifier' | 'name' | 'model' | 'deviceClass'>
|
||||
& Pick<AppleDevice, 'id'>
|
||||
& AppleDeviceFragment
|
||||
)> }
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { JSONObject } from '@expo/json-file';
|
||||
import gql from 'graphql-tag';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
AppPlatform,
|
||||
CreateSubmissionMutation,
|
||||
CreateSubmissionMutationVariables,
|
||||
Submission,
|
||||
} from '../generated';
|
||||
|
||||
interface CreateSubmissionResult {
|
||||
submission: Pick<Submission, 'id'>;
|
||||
}
|
||||
|
||||
export const SubmissionMutation = {
|
||||
async createSubmissionAsync(input: {
|
||||
appId: string;
|
||||
platform: AppPlatform;
|
||||
config: JSONObject;
|
||||
}): Promise<CreateSubmissionResult> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateSubmissionMutation, CreateSubmissionMutationVariables>(
|
||||
gql`
|
||||
mutation CreateSubmissionMutation(
|
||||
$appId: ID!
|
||||
$platform: AppPlatform!
|
||||
$config: JSONObject!
|
||||
) {
|
||||
submission {
|
||||
createSubmission(input: { appId: $appId, platform: $platform, config: $config }) {
|
||||
submission {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
input
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return nullthrows(data.submission?.createSubmission);
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { EnvironmentSecretScope } from '../../commands/secrets/create';
|
||||
import { EnvironmentSecretScope } from '../../commands/secret/create';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
EnvironmentSecretFragment,
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
AppPlatform,
|
||||
GetAllSubmissionsForAppQuery,
|
||||
GetAllSubmissionsForAppQueryVariables,
|
||||
SubmissionFragment,
|
||||
SubmissionStatus,
|
||||
SubmissionsByIdQuery,
|
||||
SubmissionsByIdQueryVariables,
|
||||
} from '../generated';
|
||||
import { SubmissionFragmentNode } from '../types/Submission';
|
||||
|
||||
type Filters = {
|
||||
platform?: AppPlatform;
|
||||
status?: SubmissionStatus;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export const SubmissionQuery = {
|
||||
async byIdAsync(
|
||||
submissionId: string,
|
||||
{ useCache = true }: { useCache?: boolean } = {}
|
||||
): Promise<SubmissionFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<SubmissionsByIdQuery, SubmissionsByIdQueryVariables>(
|
||||
gql`
|
||||
query SubmissionsByIdQuery($submissionId: ID!) {
|
||||
submissions {
|
||||
byId(submissionId: $submissionId) {
|
||||
id
|
||||
...SubmissionFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(SubmissionFragmentNode)}
|
||||
`,
|
||||
{ submissionId },
|
||||
{ requestPolicy: useCache ? 'cache-first' : 'network-only' }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.submissions.byId;
|
||||
},
|
||||
|
||||
async allForAppAsync(
|
||||
appId: string,
|
||||
{ limit = 10, offset = 0, status, platform }: Filters
|
||||
): Promise<SubmissionFragment[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<GetAllSubmissionsForAppQuery, GetAllSubmissionsForAppQueryVariables>(
|
||||
gql`
|
||||
query GetAllSubmissionsForApp(
|
||||
$appId: String!
|
||||
$offset: Int!
|
||||
$limit: Int!
|
||||
$status: SubmissionStatus
|
||||
$platform: AppPlatform
|
||||
) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
submissions(
|
||||
filter: { status: $status, platform: $platform }
|
||||
offset: $offset
|
||||
limit: $limit
|
||||
) {
|
||||
id
|
||||
...SubmissionFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(SubmissionFragmentNode)}
|
||||
`,
|
||||
{ appId, offset, limit, status, platform }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.app?.byId.submissions ?? [];
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const SubmissionFragmentNode = gql`
|
||||
fragment SubmissionFragment on Submission {
|
||||
id
|
||||
status
|
||||
platform
|
||||
error {
|
||||
errorCode
|
||||
message
|
||||
}
|
||||
logsUrl
|
||||
}
|
||||
`;
|
||||
@@ -1,5 +1,9 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { AppleDeviceFragmentNode } from './AppleDevice';
|
||||
import { AppleTeamFragmentNode } from './AppleTeam';
|
||||
|
||||
export const AppleProvisioningProfileFragmentNode = gql`
|
||||
fragment AppleProvisioningProfileFragment on AppleProvisioningProfile {
|
||||
id
|
||||
@@ -10,17 +14,15 @@ export const AppleProvisioningProfileFragmentNode = gql`
|
||||
status
|
||||
appleTeam {
|
||||
id
|
||||
appleTeamIdentifier
|
||||
appleTeamName
|
||||
...AppleTeamFragment
|
||||
}
|
||||
appleDevices {
|
||||
id
|
||||
identifier
|
||||
name
|
||||
model
|
||||
deviceClass
|
||||
...AppleDeviceFragment
|
||||
}
|
||||
}
|
||||
${print(AppleDeviceFragmentNode)}
|
||||
${print(AppleTeamFragmentNode)}
|
||||
`;
|
||||
export const AppleProvisioningProfileIdentifiersFragmentNode = gql`
|
||||
fragment AppleProvisioningProfileIdentifiersFragment on AppleProvisioningProfile {
|
||||
|
||||
Loaded 100 of 135 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user