mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9da78946b | ||
|
|
b6d9accea4 | ||
|
|
4611a65bce | ||
|
|
db29b3fc18 | ||
|
|
2534a46bd3 | ||
|
|
7def4b502e | ||
|
|
4ea26d7ca3 | ||
|
|
c4c8075b1f | ||
|
|
266545d2a0 | ||
|
|
ddd7046513 | ||
|
|
7a44eeb22b | ||
|
|
5bcf8263cf | ||
|
|
f380ba0106 | ||
|
|
1a6d81f68e | ||
|
|
522818113b | ||
|
|
f93d347665 | ||
|
|
e96cee1c3b | ||
|
|
4d46e0a8ac | ||
|
|
80e35c7524 | ||
|
|
186c26bc99 | ||
|
|
be6a7f54f9 | ||
|
|
fff7aac5b6 | ||
|
|
0d90fbd3ff | ||
|
|
e71779049e | ||
|
|
7da909e3c1 | ||
|
|
e439fd14a9 | ||
|
|
ffed1b446a | ||
|
|
7b65412ec6 | ||
|
|
f1cbdcc094 | ||
|
|
ec5573efa6 | ||
|
|
242da62271 | ||
|
|
23d1221ef9 | ||
|
|
5402d8c74f | ||
|
|
176026645c | ||
|
|
b0d4ed6c0e | ||
|
|
2f7ac71921 | ||
|
|
b599315a85 | ||
|
|
e86d8aa77a | ||
|
|
ab4ac87d52 | ||
|
|
af98fb1621 | ||
|
|
0ce7c1b3b4 | ||
|
|
2bdaa0d428 | ||
|
|
2486a4de97 | ||
|
|
6137b215e9 | ||
|
|
265ba16594 | ||
|
|
a7bc3d0c98 | ||
|
|
ee618880d7 | ||
|
|
2c95c8c310 | ||
|
|
b90e37d9fc | ||
|
|
34563070f4 | ||
|
|
66b269f4bf | ||
|
|
168cb4d4a0 | ||
|
|
91b531da43 | ||
|
|
2e0d567361 | ||
|
|
81e6280cff | ||
|
|
e2fda7b9d3 | ||
|
|
ede30807f5 | ||
|
|
9f354013fa | ||
|
|
30552f134c | ||
|
|
e4beba9a21 | ||
|
|
b71ab7ec84 | ||
|
|
596cfc372b | ||
|
|
d5f6c5680c | ||
|
|
28c508ff62 | ||
|
|
97ff6f406d | ||
|
|
bd39b296fa | ||
|
|
bfbe1e930b | ||
|
|
3fe45176cc | ||
|
|
bffa6fbf03 |
No files matched your search
+82
-3
@@ -6,6 +6,87 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Change default iOS image for projects with Expo SDK <= 41 (SDK 41 won't build with Xcode 12.5). ([#479](https://github.com/expo/eas-cli/pull/479) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix inverted conditional so we actually only prompt about commit when index is dirty. ([#481](https://github.com/expo/eas-cli/pull/481) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [0.18.1](https://github.com/expo/eas-cli/releases/tag/v0.18.1) - 2021-06-24
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add basic printing support for Apple App Specific Password. ([#474](https://github.com/expo/eas-cli/pull/474) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix CLI UI getting blocked on credentials migration, leading to partially migrated state. ([#477](https://github.com/expo/eas-cli/pull/477) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.18.0](https://github.com/expo/eas-cli/releases/tag/v0.18.0) - 2021-06-24
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Drop support for `experimental.npmToken` in credentials.json, EAS Secrets can be used instead. ([#444](https://github.com/expo/eas-cli/pull/444) by [@dsokal](https://github.com/dsokal))
|
||||
- Remove `--allow-experimental` flag from `eas build:configure` as it has no effect now. ([#444](https://github.com/expo/eas-cli/pull/444) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Make credentials manager work with multi-target iOS projects. ([#441](https://github.com/expo/eas-cli/pull/441) by [@dsokal](https://github.com/dsokal))
|
||||
- Copy over credentials from Expo Classic to EAS. ([#445](https://github.com/expo/eas-cli/pull/445) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add Big Sur image for iOS builds. ([#457](https://github.com/expo/eas-cli/pull/457) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- New version of Android credentials manager. ([#460](https://github.com/expo/eas-cli/pull/460) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add an internal distribution with dev client to `build:configure` defaults. ([#465](https://github.com/expo/eas-cli/pull/465) by [@fson](https://github.com/fson))
|
||||
- Add `updates.channel` to build metadata. ([#461](https://github.com/expo/eas-cli/pull/461) by [@jkhales](https://github.com/jkhales))
|
||||
- iOS push key setup and management now available in `eas-cli credentials`. ([#469](https://github.com/expo/eas-cli/pull/469) [#470](https://github.com/expo/eas-cli/pull/470) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Support new build status: `new`. ([#475](https://github.com/expo/eas-cli/pull/475) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Deprecate `--skip-credentials-check` flag because it doesn't do anything and is no longer needed. ([#442](https://github.com/expo/eas-cli/pull/442) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Move android credentials code to new Graphql API. ([#439](https://github.com/expo/eas-cli/pull/439) [#440](https://github.com/expo/eas-cli/pull/440) [#438](https://github.com/expo/eas-cli/pull/438) [#443](https://github.com/expo/eas-cli/pull/443) [#447](https://github.com/expo/eas-cli/pull/447) [#451](https://github.com/expo/eas-cli/pull/451) [#455](https://github.com/expo/eas-cli/pull/455) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Prepare Graphql infra to support iOS push keys. ([#456](https://github.com/expo/eas-cli/pull/456) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Improve credentials DX ([#448](https://github.com/expo/eas-cli/pull/448) [#449](https://github.com/expo/eas-cli/pull/449) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add analytics on dev client builds. ([#454](https://github.com/expo/eas-cli/pull/454) by [@fson](https://github.com/fson))
|
||||
- Support non-git projects. ([#462](https://github.com/expo/eas-cli/pull/462) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.17.0](https://github.com/expo/eas-cli/releases/tag/v0.17.0) - 2021-06-02
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix bundle identifier resolution when native target is not provided. ([#434](https://github.com/expo/eas-cli/pull/434) by [@dsokal](https://github.com/dsokal))
|
||||
- Fix git repo root path getter on Windows. ([#429](https://github.com/expo/eas-cli/pull/429) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Fix resolving Android application identifier. ([#431](https://github.com/expo/eas-cli/pull/431) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Android credentials setup now on Graphql API. ([#434](https://github.com/expo/eas-cli/pull/427) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.16.0](https://github.com/expo/eas-cli/releases/tag/v0.16.0) - 2021-05-26
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Opt out of capability syncing with `EXPO_NO_CAPABILITY_SYNC=1`. ([#426](https://github.com/expo/eas-cli/pull/426) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Add more verbose logging around capability syncing to help debug reported issues. ([#426](https://github.com/expo/eas-cli/pull/426) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Add managed credentials support for multi-target iOS projects. ([#414](https://github.com/expo/eas-cli/pull/414) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.15.1](https://github.com/expo/eas-cli/releases/tag/v0.15.1) - 2021-05-25
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support auto capabilities in managed workflow using Expo config plugin introspection. ([#419](https://github.com/expo/eas-cli/pull/419) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix sending `appBuildVersion` as part of the build metadata. ([#423](https://github.com/expo/eas-cli/pull/423) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.15.0](https://github.com/expo/eas-cli/releases/tag/v0.15.0) - 2021-05-20
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- `[EAS BUILD API]` Remove "Auto" option for `schemeBuildConfiguration` and make the old "Auto" behaviour the default. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
|
||||
- Remove `experimental.disableIosBundleIdentifierValidation` flag from eas.json. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
|
||||
- Deprecate `android.package` and `ios.bundleIdentifier` in app config for generic projects. EAS CLI depends on the values in native code now. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
|
||||
@@ -23,13 +104,11 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix failure when publishing without the platform flag. ([#415](https://github.com/expo/eas-cli/pull/415) by [@jkhales](https://github.com/jkhales))
|
||||
- Fix failure when publishing without the platform flag. ([#415](https://github.com/expo/eas-cli/pull/415) by [@jkhales](https://github.com/jkhales))
|
||||
- Pin versions in package.json. ([#399](https://github.com/expo/eas-cli/pull/399) by [@dsokal](https://github.com/dsokal))
|
||||
- Revert [0ac2f](https://github.com/expo/eas-cli/commit/0ac2fb77a118df609381c4e350aa68609340c3cd) as the root cause of the issue has been fixed in [#399](https://github.com/expo/eas-cli/pull/399).
|
||||
- Include development-client in valid buildType for internal distribution. ([#410](https://github.com/expo/eas-cli/pull/410) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [0.14.1](https://github.com/expo/eas-cli/releases/tag/v0.14.1) - 2021-05-11
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
+4
-4
@@ -3,10 +3,6 @@
|
||||
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`.
|
||||
- Before hitting the submit button, make sure there are 5 assets available under the release entry.
|
||||

|
||||
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:
|
||||
@@ -31,6 +27,10 @@
|
||||
1. Update `CHANGELOG.md`.
|
||||
- Add the new section with the release version in the title.
|
||||
- Leave the `main` section empty (without any changelog entries).
|
||||
1. Once the release is ready to go, you can go to https://github.com/expo/eas-cli/releases and mark it as not draft.
|
||||
- Remember to copy the changelog entries from `CHANGELOG.md`.
|
||||
- Before hitting the submit button, make sure there are 5 assets available under the release entry.
|
||||

|
||||
1. If any of the changelog entries have been tagged with `[EAS BUILD API]`
|
||||
- Open `package.json` and note the version of `@expo/eas-build-job` used in the eas-cli.
|
||||
- Find the `Publish` commit in [expo/eas-build](https://github.com/expo/eas-build/commit/) for that version. In most cases, it will be the latest `Publish` commit.
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.15.0"
|
||||
"version": "0.18.2"
|
||||
}
|
||||
+23
-25
@@ -69,7 +69,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/account/login.ts)_
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/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.15.0/build/commands/account/logout.ts)_
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/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.15.0/build/commands/account/view.ts)_
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -108,7 +108,7 @@ USAGE
|
||||
$ eas analytics [STATUS]
|
||||
```
|
||||
|
||||
_See code: [build/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/analytics.ts)_
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -124,12 +124,11 @@ OPTIONS
|
||||
--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
|
||||
--skip-project-configuration Skip project configuration
|
||||
--[no-]wait Wait for build(s) to complete
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/build/index.ts)_
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -140,7 +139,7 @@ USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/build/cancel.ts)_
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -152,10 +151,9 @@ USAGE
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios|all) Platform to configure
|
||||
--allow-experimental Enable experimental configuration steps.
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/build/configure.ts)_
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -168,10 +166,10 @@ USAGE
|
||||
OPTIONS
|
||||
--limit=limit
|
||||
--platform=(all|android|ios)
|
||||
--status=(in-queue|in-progress|errored|finished|canceled)
|
||||
--status=(new|in-queue|in-progress|errored|finished|canceled)
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/build/list.ts)_
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -182,7 +180,7 @@ USAGE
|
||||
$ eas build:view [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/build/view.ts)_
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -193,7 +191,7 @@ USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/credentials.ts)_
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -204,7 +202,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/device/create.ts)_
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -218,7 +216,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/device/list.ts)_
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -229,7 +227,7 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/device/view.ts)_
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -262,7 +260,7 @@ OPTIONS
|
||||
--value=value Value of the secret
|
||||
```
|
||||
|
||||
_See code: [build/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/secret/create.ts)_
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -279,7 +277,7 @@ DESCRIPTION
|
||||
Unsure where to find the secret's ID? Run eas secrets:list
|
||||
```
|
||||
|
||||
_See code: [build/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/secret/delete.ts)_
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -290,7 +288,7 @@ USAGE
|
||||
$ eas secret:list
|
||||
```
|
||||
|
||||
_See code: [build/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/secret/list.ts)_
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
|
||||
## `eas submit --platform=(android|ios)`
|
||||
|
||||
@@ -369,7 +367,7 @@ EXAMPLES
|
||||
and provide its App ID
|
||||
```
|
||||
|
||||
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/submit.ts)_
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -388,7 +386,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/webhook/create.ts)_
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -402,7 +400,7 @@ ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/webhook/delete.ts)_
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -416,7 +414,7 @@ OPTIONS
|
||||
--event=(BUILD) Event type that triggers the webhook
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/webhook/list.ts)_
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -436,7 +434,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/webhook/update.ts)_
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -450,5 +448,5 @@ ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.15.0/build/commands/webhook/view.ts)_
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
@@ -1696,6 +1696,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "subscription",
|
||||
"description": "Subscription info visible to members that have VIEWER role",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SubscriptionDetails",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "appleTeams",
|
||||
"description": "iOS credentials for account",
|
||||
@@ -1884,6 +1896,30 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "appleAppSpecificPasswords",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "AppleAppSpecificPassword",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "environmentSecrets",
|
||||
"description": "Environment secrets for an account",
|
||||
@@ -3839,6 +3875,12 @@
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "NEW",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "IN_QUEUE",
|
||||
"description": "",
|
||||
@@ -4100,6 +4142,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "appBuildVersion",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "sdkVersion",
|
||||
"description": "",
|
||||
@@ -4124,6 +4178,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "channel",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "metrics",
|
||||
"description": "",
|
||||
@@ -6528,6 +6594,18 @@
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "appleAppSpecificPasswordId",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
@@ -6703,6 +6781,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "appSpecificPassword",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "AppleAppSpecificPassword",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "iosAppBuildCredentialsArray",
|
||||
"description": "",
|
||||
@@ -7850,6 +7940,30 @@
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "iosAppCredentialsList",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "IosAppCredentials",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
@@ -7878,6 +7992,109 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AppleAppSpecificPassword",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Account",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "appleIdUsername",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "passwordLabel",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"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": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "AndroidAppCredentialsFilter",
|
||||
@@ -11711,6 +11928,22 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "appleAppSpecificPassword",
|
||||
"description": "Mutations that modify an App Specific Password for an Apple User Account",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "AppleAppSpecificPasswordMutation",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "appleDevice",
|
||||
"description": "Mutations that modify an Apple Device",
|
||||
@@ -13742,6 +13975,111 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AppleAppSpecificPasswordMutation",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "createAppleAppSpecificPassword",
|
||||
"description": "Create an App Specific Password for an Apple User Account",
|
||||
"args": [
|
||||
{
|
||||
"name": "appleAppSpecificPasswordInput",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "AppleAppSpecificPasswordInput",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "accountId",
|
||||
"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": "AppleAppSpecificPassword",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "AppleAppSpecificPasswordInput",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "appleIdUsername",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "passwordLabel",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "appSpecificPassword",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AppleDeviceMutation",
|
||||
@@ -14678,13 +15016,9 @@
|
||||
"name": "appleTeamName",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
@@ -15022,6 +15356,37 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "deleteBuild",
|
||||
"description": "Delete an EAS Build build",
|
||||
"args": [
|
||||
{
|
||||
"name": "buildId",
|
||||
"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": "Build",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "cancel",
|
||||
"description": "Cancel an EAS Build build",
|
||||
@@ -15308,6 +15673,16 @@
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "updates",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "BuildUpdatesInput",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "secrets",
|
||||
"description": "",
|
||||
@@ -15431,6 +15806,27 @@
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "BuildUpdatesInput",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "channel",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "AndroidJobSecretsInput",
|
||||
@@ -15712,6 +16108,16 @@
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "appBuildVersion",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "cliVersion",
|
||||
"description": "",
|
||||
@@ -15762,6 +16168,16 @@
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "channel",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "distribution",
|
||||
"description": "",
|
||||
@@ -16022,6 +16438,16 @@
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "updates",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "BuildUpdatesInput",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "secrets",
|
||||
"description": "",
|
||||
@@ -16150,6 +16576,16 @@
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "updates",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "BuildUpdatesInput",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "distribution",
|
||||
"description": "",
|
||||
@@ -16524,6 +16960,16 @@
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "updates",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "BuildUpdatesInput",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "distribution",
|
||||
"description": "",
|
||||
@@ -16651,9 +17097,13 @@
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "IosAppBuildCredentials",
|
||||
"ofType": null
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "IosAppBuildCredentials",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
@@ -16692,9 +17142,13 @@
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "IosAppBuildCredentials",
|
||||
"ofType": null
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "IosAppBuildCredentials",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
@@ -16733,9 +17187,13 @@
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "IosAppBuildCredentials",
|
||||
"ofType": null
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "IosAppBuildCredentials",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
@@ -16852,9 +17310,13 @@
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "IosAppCredentials",
|
||||
"ofType": null
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "IosAppCredentials",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
@@ -16893,9 +17355,58 @@
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "IosAppCredentials",
|
||||
"ofType": null
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "IosAppCredentials",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "setAppSpecificPassword",
|
||||
"description": "Set the app-specific password to be used for an iOS app",
|
||||
"args": [
|
||||
{
|
||||
"name": "id",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "appSpecificPasswordId",
|
||||
"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": "IosAppCredentials",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
@@ -16916,13 +17427,9 @@
|
||||
"name": "appleTeamId",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
@@ -16935,6 +17442,16 @@
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "appSpecificPasswordId",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
@@ -17330,6 +17847,37 @@
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "createIosSubmission",
|
||||
"description": "Create an iOS EAS Submit submission",
|
||||
"args": [
|
||||
{
|
||||
"name": "input",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "CreateIosSubmissionInput",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "CreateSubmissionResult",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
@@ -17417,6 +17965,118 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "CreateIosSubmissionInput",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "appId",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "config",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "NewIosSubmissionConfig",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "NewIosSubmissionConfig",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "appleAppSpecificPasswordId",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "appleAppSpecificPassword",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "archiveUrl",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "appleIdUsername",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "ascAppIdentifier",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "UpdateChannelMutation",
|
||||
@@ -19615,6 +20275,12 @@
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "DEV_CLIENT_USERS",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.15.0",
|
||||
"version": "0.18.2",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -11,10 +11,10 @@
|
||||
"@amplitude/identify": "1.5.0",
|
||||
"@amplitude/node": "1.5.0",
|
||||
"@expo/apple-utils": "0.0.0-alpha.20",
|
||||
"@expo/config": "3.3.19",
|
||||
"@expo/config-plugins": "1.0.31",
|
||||
"@expo/eas-build-job": "0.2.35",
|
||||
"@expo/eas-json": "^0.15.0",
|
||||
"@expo/config": "3.3.42",
|
||||
"@expo/config-plugins": "2.0.2",
|
||||
"@expo/eas-build-job": "0.2.37",
|
||||
"@expo/eas-json": "^0.18.2",
|
||||
"@expo/json-file": "8.2.25",
|
||||
"@expo/pkcs12": "0.0.4",
|
||||
"@expo/plist": "0.0.11",
|
||||
@@ -41,6 +41,7 @@
|
||||
"got": "11.5.2",
|
||||
"graphql": "15.3.0",
|
||||
"graphql-tag": "2.11.0",
|
||||
"ignore": "5.1.8",
|
||||
"indent-string": "4.0.0",
|
||||
"jks-js": "1.0.0",
|
||||
"keychain": "1.3.0",
|
||||
@@ -56,6 +57,7 @@
|
||||
"progress": "2.0.3",
|
||||
"prompts": "2.3.2",
|
||||
"qrcode-terminal": "0.12.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.3.4",
|
||||
"strip-ansi": "6.0.0",
|
||||
"tar": "6.0.5",
|
||||
@@ -166,13 +168,14 @@
|
||||
"repository": "expo/eas-cli",
|
||||
"scripts": {
|
||||
"postpack": "rm -f oclif.manifest.json",
|
||||
"prepack": "rm -rf build && yarn build && yarn oclif-dev manifest && yarn oclif-dev readme",
|
||||
"prepack": "rm -rf build && yarn build && yarn oclif-dev manifest && yarn oclif-dev readme && node patch-readme",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"watch": "yarn build --watch --preserveWatchOutput",
|
||||
"typecheck": "tsc",
|
||||
"test": "jest",
|
||||
"version": "yarn oclif-dev readme && git add README.md",
|
||||
"version": "yarn oclif-dev readme && node patch-readme && git add README.md",
|
||||
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml"
|
||||
},
|
||||
"types": "build/index.d.ts"
|
||||
"types": "build/index.d.ts",
|
||||
"gitHead": "161808bc37c255fdea6801d69d23dbc725c056c3"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
const fs = require('fs');
|
||||
|
||||
// Patch `oclif-dev readme` path and link generation
|
||||
let readmeContent = fs.readFileSync('README.md', 'utf8');
|
||||
readmeContent = readmeContent.replace(/\[build\//g, '[src/');
|
||||
readmeContent = readmeContent.replace(/build\/commands/g, 'packages/eas-cli/src/commands');
|
||||
fs.writeFileSync('README.md', readmeContent);
|
||||
|
||||
console.log('Patched README path generation');
|
||||
@@ -96,6 +96,11 @@ function getContext() {
|
||||
os_version: os.release(),
|
||||
device_brand: platform,
|
||||
device_model: platform,
|
||||
app_version: packageJSON?.version,
|
||||
source_version: packageJSON?.version,
|
||||
source: 'eas cli',
|
||||
};
|
||||
}
|
||||
|
||||
export enum AnalyticsEvent {
|
||||
ACTION = 'action', // generic event type which is used to determine the 'daily active user' stat, include an `action: eas ${subcommand}` property inside of the event properties object
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { AndroidConfig, AndroidManifest } from '@expo/config-plugins';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import Log from '../../log';
|
||||
@@ -10,7 +10,7 @@ import { ensureValidVersions } from '../utils/updates';
|
||||
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
|
||||
|
||||
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
|
||||
@@ -22,12 +22,7 @@ export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig)
|
||||
}
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
|
||||
androidManifestPath
|
||||
);
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
|
||||
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, accountName)) {
|
||||
const result = AndroidConfig.Updates.setUpdatesConfig(exp, androidManifest, accountName);
|
||||
@@ -52,10 +47,7 @@ export async function syncUpdatesConfigurationAsync(
|
||||
}
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
let androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(androidManifestPath);
|
||||
let androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
|
||||
if (!AndroidConfig.Updates.areVersionsSynced(exp, androidManifest)) {
|
||||
androidManifest = AndroidConfig.Updates.setVersionsConfig(exp, androidManifest);
|
||||
@@ -70,7 +62,7 @@ export async function syncUpdatesConfigurationAsync(
|
||||
}
|
||||
|
||||
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
|
||||
|
||||
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
|
||||
@@ -78,13 +70,7 @@ async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig)
|
||||
throw new Error(`Missing ${gradleScriptApply} in ${buildGradlePath}`);
|
||||
}
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
|
||||
androidManifestPath
|
||||
);
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
|
||||
if (!AndroidConfig.Updates.isMainApplicationMetaDataSet(androidManifest)) {
|
||||
throw new Error('Missing values in AndroidManifest.xml');
|
||||
@@ -93,13 +79,7 @@ async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig)
|
||||
|
||||
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
|
||||
androidManifestPath
|
||||
);
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
return AndroidConfig.Manifest.getMainApplicationMetaDataValue(
|
||||
androidManifest,
|
||||
AndroidConfig.Updates.Config.RELEASE_CHANNEL
|
||||
@@ -108,3 +88,28 @@ export async function readReleaseChannelSafelyAsync(projectDir: string): Promise
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
const stringifiedRequestHeaders = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
|
||||
androidManifest,
|
||||
'expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY' //TODO-JJ AndroidConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY once https://github.com/expo/expo-cli/pull/3571 is published
|
||||
);
|
||||
if (!stringifiedRequestHeaders) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(stringifiedRequestHeaders)['expo-channel-name'] ?? null;
|
||||
} catch (err) {
|
||||
console.log({ err });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getAndroidManifestAsync(projectDir: string): Promise<AndroidManifest> {
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
return AndroidConfig.Manifest.readAndroidManifestAsync(androidManifestPath);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { readChannelSafelyAsync } from '../UpdatesModule';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
afterAll(() => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.removeSync(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.mkdirpSync(os.tmpdir());
|
||||
});
|
||||
|
||||
const testChannel = 'test-channel';
|
||||
describe(readChannelSafelyAsync, () => {
|
||||
it('retrieves the channel when it is defined natively', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./android/app/src/main/AndroidManifest.xml': AndroidManifestWithChannel,
|
||||
},
|
||||
'/expo-project'
|
||||
);
|
||||
const channel = await readChannelSafelyAsync('/expo-project');
|
||||
expect(channel).toBe(testChannel);
|
||||
});
|
||||
it('returns null if the channel is not defined natively', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./android/app/src/main/AndroidManifest.xml': AndroidManifestNoChannel,
|
||||
},
|
||||
'/expo-project'
|
||||
);
|
||||
const channel = await readChannelSafelyAsync('/expo-project');
|
||||
expect(channel).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const AndroidManifestWithChannel = `
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.expo.mycoolapp">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<meta-data android:name="expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY" android:value='{"expo-channel-name":"${testChannel}"}'/>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
</application>
|
||||
|
||||
|
||||
</manifest>
|
||||
`;
|
||||
const AndroidManifestNoChannel = `
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.expo.mycoolapp">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
</application>
|
||||
|
||||
|
||||
</manifest>
|
||||
`;
|
||||
@@ -2,6 +2,7 @@ import { Android, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
import { EasConfig } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
|
||||
import AndroidCredentialsProvider, {
|
||||
@@ -10,8 +11,12 @@ import AndroidCredentialsProvider, {
|
||||
import { createCredentialsContextAsync } from '../../credentials/context';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import Log from '../../log';
|
||||
import { getOrConfigureApplicationIdAsync } from '../../project/android/applicationId';
|
||||
import {
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
getApplicationId,
|
||||
} from '../../project/android/applicationId';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { CredentialsResult, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommandContext, createBuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
@@ -63,12 +68,12 @@ This means that it will most likely produce an AAB and you will not be able to i
|
||||
}
|
||||
}
|
||||
|
||||
// this function throws if application id is invalid
|
||||
await getOrConfigureApplicationIdAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
if (buildCtx.buildProfile.workflow === Workflow.MANAGED) {
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
}
|
||||
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx: buildCtx,
|
||||
projectConfiguration: {},
|
||||
ensureCredentialsAsync: ensureAndroidCredentialsAsync,
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
|
||||
@@ -114,16 +119,23 @@ async function ensureAndroidCredentialsAsync(
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
return;
|
||||
}
|
||||
const androidApplicationIdentifier = getApplicationId(
|
||||
ctx.commandCtx.projectDir,
|
||||
ctx.commandCtx.exp
|
||||
);
|
||||
const provider = new AndroidCredentialsProvider(
|
||||
await createCredentialsContextAsync(ctx.commandCtx.projectDir, {
|
||||
nonInteractive: ctx.commandCtx.nonInteractive,
|
||||
}),
|
||||
{
|
||||
app: {
|
||||
account: nullthrows(
|
||||
findAccountByName(ctx.commandCtx.user.accounts, ctx.commandCtx.accountName),
|
||||
`You do not have access to account: ${ctx.commandCtx.accountName}`
|
||||
),
|
||||
projectName: ctx.commandCtx.projectName,
|
||||
accountName: ctx.commandCtx.accountName,
|
||||
androidApplicationIdentifier,
|
||||
},
|
||||
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
|
||||
}
|
||||
);
|
||||
const { credentialsSource } = ctx.buildProfile;
|
||||
|
||||
@@ -3,17 +3,17 @@ import { AndroidConfig } from '@expo/config-plugins';
|
||||
|
||||
import Log from '../../log';
|
||||
import {
|
||||
getOrConfigureApplicationIdAsync,
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
warnIfAndroidPackageDefinedInAppConfigForGenericProject,
|
||||
} from '../../project/android/applicationId';
|
||||
import { gitAddAsync } from '../../utils/git';
|
||||
import vcs from '../../vcs';
|
||||
import { ConfigureContext } from '../context';
|
||||
import { isExpoUpdatesInstalled } from '../utils/updates';
|
||||
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
|
||||
|
||||
export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void> {
|
||||
if (!ctx.hasAndroidNativeProject) {
|
||||
await getOrConfigureApplicationIdAsync(ctx.projectDir, ctx.exp);
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void
|
||||
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
|
||||
|
||||
const easGradlePath = AndroidConfig.EasBuild.getEasBuildGradlePath(ctx.projectDir);
|
||||
await gitAddAsync(easGradlePath, { intentToAdd: true });
|
||||
await vcs.trackFileAsync(easGradlePath);
|
||||
|
||||
if (isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
|
||||
|
||||
@@ -12,6 +12,7 @@ export function transformGenericJob(job: Android.GenericJob): AndroidGenericJobI
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
updates: job.updates,
|
||||
secrets: job.secrets,
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
@@ -25,6 +26,7 @@ export function transformManagedJob(job: Android.ManagedJob): AndroidManagedJobI
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
updates: job.updates,
|
||||
secrets: job.secrets,
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
|
||||
@@ -3,10 +3,9 @@ import { AndroidGenericBuildProfile, AndroidManagedBuildProfile } from '@expo/ea
|
||||
import path from 'path';
|
||||
|
||||
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
|
||||
import { readEnvironmentSecretsAsync } from '../../credentials/credentialsJson/read';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { gitRootDirectoryAsync } from '../../utils/git';
|
||||
import vcs from '../../vcs';
|
||||
import { BuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
|
||||
@@ -39,7 +38,6 @@ interface CommonJobProperties {
|
||||
buildCredentials?: {
|
||||
keystore: Android.Keystore;
|
||||
};
|
||||
environmentSecrets?: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,8 +45,7 @@ async function prepareJobCommonAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData
|
||||
): Promise<Partial<CommonJobProperties>> {
|
||||
const environmentSecrets = await readEnvironmentSecretsAsync(ctx.commandCtx.projectDir);
|
||||
const credentials = jobData.credentials;
|
||||
const { credentials } = jobData;
|
||||
const buildCredentials = credentials
|
||||
? {
|
||||
buildCredentials: {
|
||||
@@ -78,7 +75,6 @@ async function prepareJobCommonAsync(
|
||||
clear: ctx.commandCtx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
...(environmentSecrets ? { environmentSecrets } : {}),
|
||||
...buildCredentials,
|
||||
},
|
||||
};
|
||||
@@ -90,13 +86,14 @@ async function prepareGenericJobAsync(
|
||||
buildProfile: AndroidGenericBuildProfile
|
||||
): Promise<Partial<Android.GenericJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
type: Workflow.GENERIC,
|
||||
gradleCommand: buildProfile.gradleCommand,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
updates: { channel: buildProfile.channel },
|
||||
projectRootDirectory,
|
||||
};
|
||||
}
|
||||
@@ -107,7 +104,7 @@ async function prepareManagedJobAsync(
|
||||
buildProfile: AndroidManagedBuildProfile
|
||||
): Promise<Partial<Android.ManagedJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
@@ -115,6 +112,7 @@ async function prepareManagedJobAsync(
|
||||
username,
|
||||
buildType: buildProfile.buildType,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
updates: { channel: buildProfile.channel },
|
||||
projectRootDirectory,
|
||||
};
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export function readVersionName(projectDir: string, exp: ExpoConfig): string | u
|
||||
}
|
||||
|
||||
function readBuildGradle(projectDir: string): string | undefined {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
if (!fs.pathExistsSync(buildGradlePath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { promptAsync } from '../prompts';
|
||||
import { uploadAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
import vcs from '../vcs';
|
||||
import { requestedPlatformDisplayNames } from './constants';
|
||||
import { BuildContext } from './context';
|
||||
import { runLocalBuildAsync } from './local';
|
||||
@@ -16,46 +17,34 @@ import { collectMetadata } from './metadata';
|
||||
import { Platform, TrackingContext } from './types';
|
||||
import Analytics, { Event } from './utils/analytics';
|
||||
import { printDeprecationWarnings } from './utils/printBuildInfo';
|
||||
import {
|
||||
commitChangedFilesAsync,
|
||||
commitPromptAsync,
|
||||
isGitStatusCleanAsync,
|
||||
makeProjectTarballAsync,
|
||||
showDiffAsync,
|
||||
} from './utils/repository';
|
||||
import { commitPromptAsync, makeProjectTarballAsync } from './utils/repository';
|
||||
|
||||
export interface CredentialsResult<Credentials> {
|
||||
source: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
|
||||
credentials: Credentials;
|
||||
}
|
||||
|
||||
interface Builder<TPlatform extends Platform, Credentials, ProjectConfiguration, TJob extends Job> {
|
||||
export interface JobData<Credentials> {
|
||||
credentials?: Credentials;
|
||||
projectArchive: ArchiveSource;
|
||||
}
|
||||
|
||||
interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
|
||||
ctx: BuildContext<TPlatform>;
|
||||
projectConfiguration: ProjectConfiguration;
|
||||
|
||||
ensureCredentialsAsync(
|
||||
ctx: BuildContext<TPlatform>
|
||||
): Promise<CredentialsResult<Credentials> | undefined>;
|
||||
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
|
||||
prepareJobAsync(
|
||||
ctx: BuildContext<TPlatform>,
|
||||
jobData: {
|
||||
credentials?: Credentials;
|
||||
projectConfiguration?: ProjectConfiguration;
|
||||
projectArchive: ArchiveSource;
|
||||
}
|
||||
): Promise<Job>;
|
||||
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
|
||||
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
|
||||
}
|
||||
|
||||
export async function prepareBuildRequestForPlatformAsync<
|
||||
TPlatform extends Platform,
|
||||
Credentials,
|
||||
ProjectConfiguration,
|
||||
TJob extends Job
|
||||
>(
|
||||
builder: Builder<TPlatform, Credentials, ProjectConfiguration, TJob>
|
||||
): Promise<() => Promise<string | undefined>> {
|
||||
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<() => Promise<string | undefined>> {
|
||||
const credentialsResult = await withAnalyticsAsync(
|
||||
async () => await builder.ensureCredentialsAsync(builder.ctx),
|
||||
{
|
||||
@@ -72,7 +61,7 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await isGitStatusCleanAsync())) {
|
||||
if (await vcs.hasUncommittedChangesAsync()) {
|
||||
Log.addNewLineIfNone();
|
||||
await reviewAndCommitChangesAsync(
|
||||
`[EAS Build] Run EAS Build for ${
|
||||
@@ -100,7 +89,6 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
const job = await builder.prepareJobAsync(builder.ctx, {
|
||||
projectArchive,
|
||||
credentials: credentialsResult?.credentials,
|
||||
projectConfiguration: builder.projectConfiguration,
|
||||
});
|
||||
|
||||
return async () => {
|
||||
@@ -171,13 +159,8 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
}
|
||||
}
|
||||
|
||||
async function sendBuildRequestAsync<
|
||||
TPlatform extends Platform,
|
||||
Credentials,
|
||||
ProjectConfiguration,
|
||||
TJob extends Job
|
||||
>(
|
||||
builder: Builder<TPlatform, Credentials, ProjectConfiguration, TJob>,
|
||||
async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJob extends Job>(
|
||||
builder: Builder<TPlatform, Credentials, TJob>,
|
||||
job: TJob,
|
||||
metadata: Metadata
|
||||
): Promise<string> {
|
||||
@@ -237,7 +220,7 @@ async function reviewAndCommitChangesAsync(
|
||||
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
|
||||
): Promise<void> {
|
||||
if (process.env.EAS_BUILD_AUTOCOMMIT) {
|
||||
await commitChangedFilesAsync(initialCommitMessage);
|
||||
await vcs.commitAsync({ commitMessage: initialCommitMessage, commitAllFiles: false });
|
||||
Log.withTick('Committed changes.');
|
||||
return;
|
||||
}
|
||||
@@ -270,7 +253,7 @@ async function reviewAndCommitChangesAsync(
|
||||
await commitPromptAsync({ initialCommitMessage });
|
||||
Log.withTick('Committed changes.');
|
||||
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
|
||||
await showDiffAsync();
|
||||
await vcs.showDiffAsync();
|
||||
await reviewAndCommitChangesAsync(initialCommitMessage, {
|
||||
nonInteractive,
|
||||
askedFirstTime: false,
|
||||
|
||||
@@ -6,18 +6,12 @@ import path from 'path';
|
||||
import Log from '../log';
|
||||
import { promptAsync } from '../prompts';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { gitAddAsync } from '../utils/git';
|
||||
import vcs from '../vcs';
|
||||
import { configureAndroidAsync } from './android/configure';
|
||||
import { ConfigureContext } from './context';
|
||||
import { configureIosAsync } from './ios/configure';
|
||||
import { RequestedPlatform } from './types';
|
||||
import {
|
||||
commitPromptAsync,
|
||||
ensureGitRepoExistsAsync,
|
||||
isGitStatusCleanAsync,
|
||||
maybeBailOnGitStatusAsync,
|
||||
showDiffAsync,
|
||||
} from './utils/repository';
|
||||
import { commitPromptAsync, maybeBailOnRepoStatusAsync } from './utils/repository';
|
||||
|
||||
const configureCommitMessage = {
|
||||
[RequestedPlatform.Android]: 'Configure EAS Build for Android',
|
||||
@@ -28,10 +22,9 @@ const configureCommitMessage = {
|
||||
export async function configureAsync(options: {
|
||||
platform: RequestedPlatform;
|
||||
projectDir: string;
|
||||
allowExperimental: boolean;
|
||||
}): Promise<void> {
|
||||
await ensureGitRepoExistsAsync();
|
||||
await maybeBailOnGitStatusAsync();
|
||||
await vcs.ensureRepoExistsAsync();
|
||||
await maybeBailOnRepoStatusAsync();
|
||||
|
||||
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
@@ -39,7 +32,6 @@ export async function configureAsync(options: {
|
||||
user: await ensureLoggedInAsync(),
|
||||
projectDir: options.projectDir,
|
||||
exp,
|
||||
allowExperimental: options.allowExperimental,
|
||||
requestedPlatform: options.platform,
|
||||
shouldConfigureAndroid: [RequestedPlatform.All, RequestedPlatform.Android].includes(
|
||||
options.platform
|
||||
@@ -58,7 +50,7 @@ export async function configureAsync(options: {
|
||||
await configureIosAsync(ctx);
|
||||
}
|
||||
|
||||
if (!(await isGitStatusCleanAsync())) {
|
||||
if (await vcs.hasUncommittedChangesAsync()) {
|
||||
Log.newLine();
|
||||
await reviewAndCommitChangesAsync(configureCommitMessage[options.platform]);
|
||||
} else {
|
||||
@@ -67,6 +59,37 @@ export async function configureAsync(options: {
|
||||
}
|
||||
}
|
||||
|
||||
const MANAGED_DEFAULTS = {
|
||||
release: {
|
||||
workflow: 'managed',
|
||||
},
|
||||
development: {
|
||||
workflow: 'managed',
|
||||
buildType: 'development-client',
|
||||
distribution: 'internal',
|
||||
},
|
||||
};
|
||||
const ANDROID_GENERIC_DEFAULTS = {
|
||||
release: {
|
||||
workflow: 'generic',
|
||||
},
|
||||
development: {
|
||||
workflow: 'generic',
|
||||
gradleCommand: ':app:assembleDebug',
|
||||
distribution: 'internal',
|
||||
},
|
||||
};
|
||||
const IOS_GENERIC_DEFAULTS = {
|
||||
release: {
|
||||
workflow: 'generic',
|
||||
},
|
||||
development: {
|
||||
workflow: 'generic',
|
||||
schemeBuildConfiguration: 'Debug',
|
||||
distribution: 'internal',
|
||||
},
|
||||
};
|
||||
|
||||
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
|
||||
const easJsonPath = path.join(ctx.projectDir, 'eas.json');
|
||||
let existingEasJson;
|
||||
@@ -101,27 +124,19 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
|
||||
...existingEasJson,
|
||||
...(shouldInitAndroid
|
||||
? {
|
||||
android: {
|
||||
release: {
|
||||
workflow: ctx.hasAndroidNativeProject ? 'generic' : 'managed',
|
||||
},
|
||||
},
|
||||
android: ctx.hasAndroidNativeProject ? ANDROID_GENERIC_DEFAULTS : MANAGED_DEFAULTS,
|
||||
}
|
||||
: null),
|
||||
...(shouldInitIOS
|
||||
? {
|
||||
ios: {
|
||||
release: {
|
||||
workflow: ctx.hasIosNativeProject ? 'generic' : 'managed',
|
||||
},
|
||||
},
|
||||
ios: ctx.hasIosNativeProject ? IOS_GENERIC_DEFAULTS : MANAGED_DEFAULTS,
|
||||
}
|
||||
: null),
|
||||
},
|
||||
};
|
||||
|
||||
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
|
||||
await gitAddAsync(easJsonPath, { intentToAdd: true });
|
||||
await vcs.trackFileAsync(easJsonPath);
|
||||
Log.withTick(`${existingEasJson ? 'Updated' : 'Generated'} eas.json`);
|
||||
}
|
||||
|
||||
@@ -155,7 +170,7 @@ async function reviewAndCommitChangesAsync(
|
||||
await commitPromptAsync({ initialCommitMessage });
|
||||
Log.withTick('Committed changes');
|
||||
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
|
||||
await showDiffAsync();
|
||||
await vcs.showDiffAsync();
|
||||
await reviewAndCommitChangesAsync(initialCommitMessage, false);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile, EasConfig, IosBuildProfile } from '@expo/eas-json';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { getProjectAccountName } from '../project/projectUtils';
|
||||
@@ -21,7 +24,6 @@ export interface CommandContext {
|
||||
nonInteractive: boolean;
|
||||
local: boolean;
|
||||
clearCache: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
skipProjectConfiguration: boolean;
|
||||
waitForBuildEnd: boolean;
|
||||
}
|
||||
@@ -35,7 +37,6 @@ export async function createCommandContextAsync({
|
||||
nonInteractive = false,
|
||||
local,
|
||||
clearCache = false,
|
||||
skipCredentialsCheck = false,
|
||||
skipProjectConfiguration = false,
|
||||
waitForBuildEnd,
|
||||
}: {
|
||||
@@ -47,7 +48,6 @@ export async function createCommandContextAsync({
|
||||
nonInteractive: boolean;
|
||||
local: boolean;
|
||||
clearCache: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
skipProjectConfiguration: boolean;
|
||||
waitForBuildEnd: boolean;
|
||||
}): Promise<CommandContext> {
|
||||
@@ -67,7 +67,6 @@ export async function createCommandContextAsync({
|
||||
nonInteractive,
|
||||
local,
|
||||
clearCache,
|
||||
skipCredentialsCheck,
|
||||
skipProjectConfiguration,
|
||||
waitForBuildEnd,
|
||||
};
|
||||
@@ -77,7 +76,6 @@ export interface ConfigureContext {
|
||||
user: Actor;
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
allowExperimental: boolean;
|
||||
requestedPlatform: RequestedPlatform;
|
||||
shouldConfigureAndroid: boolean;
|
||||
shouldConfigureIos: boolean;
|
||||
@@ -111,6 +109,7 @@ export function createBuildContext<T extends Platform>({
|
||||
}
|
||||
|
||||
const accountId = findAccountByName(commandCtx.user.accounts, commandCtx.accountName)?.id;
|
||||
const devClientProperties = getDevClientEventProperties(platform, commandCtx, buildProfile);
|
||||
const trackingCtx = {
|
||||
tracking_id: uuidv4(),
|
||||
platform,
|
||||
@@ -118,6 +117,7 @@ export function createBuildContext<T extends Platform>({
|
||||
account_name: commandCtx.accountName,
|
||||
project_id: commandCtx.projectId,
|
||||
project_type: buildProfile.workflow,
|
||||
...devClientProperties,
|
||||
};
|
||||
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
|
||||
return {
|
||||
@@ -127,3 +127,36 @@ export function createBuildContext<T extends Platform>({
|
||||
buildProfile,
|
||||
};
|
||||
}
|
||||
|
||||
function getDevClientEventProperties(
|
||||
platform: Platform,
|
||||
commandCtx: CommandContext,
|
||||
buildProfile: AndroidBuildProfile | IosBuildProfile
|
||||
): Partial<TrackingContext> {
|
||||
let includesDevClient;
|
||||
const version = tryGetDevClientVersion(commandCtx.projectDir);
|
||||
if (buildProfile.workflow === Workflow.MANAGED) {
|
||||
includesDevClient = buildProfile.buildType === 'development-client';
|
||||
} else if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
|
||||
} else if (platform === Platform.IOS && 'schemeBuildConfiguration' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.schemeBuildConfiguration === 'Debug');
|
||||
} else {
|
||||
includesDevClient = false;
|
||||
}
|
||||
|
||||
if (version) {
|
||||
return { dev_client: includesDevClient, dev_client_version: version };
|
||||
} else {
|
||||
return { dev_client: includesDevClient };
|
||||
}
|
||||
}
|
||||
|
||||
function tryGetDevClientVersion(projectDir: string): string | null {
|
||||
try {
|
||||
const pkg = JsonFile.read(resolveFrom(projectDir, 'expo-dev-client/package.json'));
|
||||
return pkg.version?.toString() ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,17 @@ import { BuildFragment, BuildStatus } from '../graphql/generated';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import Log from '../log';
|
||||
import { sleep } from '../utils/promise';
|
||||
import vcs from '../vcs';
|
||||
import { prepareAndroidBuildAsync } from './android/build';
|
||||
import { CommandContext } from './context';
|
||||
import { prepareIosBuildAsync } from './ios/build';
|
||||
import { Platform, RequestedPlatform } from './types';
|
||||
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
|
||||
import { ensureGitRepoExistsAsync, ensureGitStatusIsCleanAsync } from './utils/repository';
|
||||
import { ensureRepoIsCleanAsync } from './utils/repository';
|
||||
|
||||
export async function buildAsync(commandCtx: CommandContext): Promise<void> {
|
||||
await ensureGitRepoExistsAsync();
|
||||
await ensureGitStatusIsCleanAsync(commandCtx.nonInteractive);
|
||||
await vcs.ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(commandCtx.nonInteractive);
|
||||
|
||||
const scheduledBuilds = await startBuildsAsync(commandCtx);
|
||||
if (commandCtx.local) {
|
||||
@@ -93,6 +94,9 @@ async function waitForBuildEndAsync(
|
||||
case BuildStatus.Finished:
|
||||
spinner.succeed('Build finished');
|
||||
return builds;
|
||||
case BuildStatus.New:
|
||||
spinner.text = 'Build created';
|
||||
break;
|
||||
case BuildStatus.InQueue:
|
||||
spinner.text = 'Build queued...';
|
||||
break;
|
||||
@@ -130,6 +134,7 @@ async function waitForBuildEndAsync(
|
||||
spinner.fail('Some of the builds were canceled or failed.');
|
||||
return builds;
|
||||
} else {
|
||||
const newBuilds = builds.filter(build => build?.status === BuildStatus.New).length;
|
||||
const inQueue = builds.filter(build => build?.status === BuildStatus.InQueue).length;
|
||||
const inProgress = builds.filter(build => build?.status === BuildStatus.InProgress).length;
|
||||
const errored = builds.filter(build => build?.status === BuildStatus.Errored).length;
|
||||
@@ -137,6 +142,7 @@ async function waitForBuildEndAsync(
|
||||
const canceled = builds.filter(build => build?.status === BuildStatus.Canceled).length;
|
||||
const unknownState = builds.length - inQueue - inProgress - errored - finished;
|
||||
spinner.text = [
|
||||
newBuilds && `Builds created: ${newBuilds}`,
|
||||
inQueue && `Builds in queue: ${inQueue}`,
|
||||
inProgress && `Builds in progress: ${inProgress}`,
|
||||
canceled && `Builds canceled: ${canceled}`,
|
||||
|
||||
@@ -27,18 +27,10 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
|
||||
|
||||
export function transformMetadata(metadata: Metadata): BuildMetadataInput {
|
||||
return {
|
||||
appIdentifier: metadata.appIdentifier,
|
||||
appName: metadata.appName,
|
||||
appVersion: metadata.appVersion,
|
||||
buildProfile: metadata.buildProfile,
|
||||
cliVersion: metadata.cliVersion,
|
||||
...metadata,
|
||||
credentialsSource:
|
||||
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
|
||||
distribution: metadata.distribution && transformDistribution(metadata.distribution),
|
||||
gitCommitHash: metadata.gitCommitHash,
|
||||
releaseChannel: metadata.releaseChannel,
|
||||
sdkVersion: metadata.sdkVersion,
|
||||
trackingContext: metadata.trackingContext,
|
||||
workflow: metadata.workflow && transformWorkflow(metadata.workflow),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import fs from 'fs-extra';
|
||||
import Log from '../../log';
|
||||
import { getProjectAccountName } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { gitAddAsync } from '../../utils/git';
|
||||
import vcs from '../../vcs';
|
||||
import { ensureValidVersions } from '../utils/updates';
|
||||
import { readPlistAsync, writePlistAsync } from './plist';
|
||||
|
||||
@@ -84,7 +84,7 @@ async function writeExpoPlistAsync(
|
||||
): Promise<void> {
|
||||
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
|
||||
await writePlistAsync(expoPlistPath, expoPlist);
|
||||
await gitAddAsync(expoPlistPath, { intentToAdd: true });
|
||||
await vcs.trackFileAsync(expoPlistPath);
|
||||
}
|
||||
|
||||
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
@@ -95,3 +95,13 @@ export async function readReleaseChannelSafelyAsync(projectDir: string): Promise
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
const expoPlist: any = await readExpoPlistAsync(projectDir); // TODO-JJ remove any once IOSConfig.ExpoPlist is updated to include `EXUpdatesRequestHeaders : Record<string,string>`
|
||||
const updatesRequestHeaders = expoPlist['EXUpdatesRequestHeaders'] ?? {}; //TODO-JJ 'EXUpdatesRequestHeaders' IosConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY once https://github.com/expo/expo-cli/pull/3571 is published
|
||||
return updatesRequestHeaders['expo-channel-name'] ?? null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export function getBundleIdentifier(): string {
|
||||
return 'com.bundle.id';
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { readChannelSafelyAsync } from '../UpdatesModule';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
afterAll(() => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.removeSync(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.mkdirpSync(os.tmpdir());
|
||||
});
|
||||
|
||||
const testChannel = 'test-channel';
|
||||
describe(readChannelSafelyAsync, () => {
|
||||
it('retrieves the channel when it is defined natively', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/expo-project/Supporting/Expo.plist': ExpoPlistWithChannel,
|
||||
'ios/expo-project/AppDelegate.m': '',
|
||||
},
|
||||
'/expo-project'
|
||||
);
|
||||
const channel = await readChannelSafelyAsync('/expo-project');
|
||||
expect(channel).toBe(testChannel);
|
||||
});
|
||||
it('returns null if the channel is not defined natively', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/expo-project/Supporting/Expo.plist': ExpoPlistWithOutChannel,
|
||||
'ios/expo-project/AppDelegate.m': '',
|
||||
},
|
||||
'/expo-project'
|
||||
);
|
||||
const channel = await readChannelSafelyAsync('/expo-project');
|
||||
expect(channel).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const ExpoPlistWithChannel = `
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>EXUpdatesCheckOnLaunch</key>
|
||||
<string>ALWAYS</string>
|
||||
<key>EXUpdatesEnabled</key>
|
||||
<true/>
|
||||
<key>EXUpdatesLaunchWaitMs</key>
|
||||
<integer>0</integer>
|
||||
<key>EXUpdatesReleaseChannel</key>
|
||||
<string>default</string>
|
||||
<key>EXUpdatesSDKVersion</key>
|
||||
<string>40.0.0</string>
|
||||
<key>EXUpdatesURL</key>
|
||||
<string>https://exp.host/@user/expo-project</string>
|
||||
<key>EXUpdatesRequestHeaders</key>
|
||||
<dict>
|
||||
<key>expo-channel-name</key>
|
||||
<string>${testChannel}</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
`;
|
||||
const ExpoPlistWithOutChannel = `
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>EXUpdatesCheckOnLaunch</key>
|
||||
<string>ALWAYS</string>
|
||||
<key>EXUpdatesEnabled</key>
|
||||
<true/>
|
||||
<key>EXUpdatesLaunchWaitMs</key>
|
||||
<integer>0</integer>
|
||||
<key>EXUpdatesReleaseChannel</key>
|
||||
<string>default</string>
|
||||
<key>EXUpdatesSDKVersion</key>
|
||||
<string>40.0.0</string>
|
||||
<key>EXUpdatesURL</key>
|
||||
<string>https://exp.host/@user/expo-project</string>
|
||||
</dict>
|
||||
</plist>
|
||||
`;
|
||||
@@ -1,23 +1,22 @@
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Ios, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
import { Ios, Job, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
import { EasConfig } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import path from 'path';
|
||||
|
||||
import { IosCredentials } from '../../credentials/ios/types';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import Log from '../../log';
|
||||
import { getOrConfigureBundleIdentifierAsync } from '../../project/ios/bundleIdentifier';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
|
||||
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
|
||||
import { resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommandContext, createBuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { Platform } from '../types';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { ensureIosCredentialsAsync } from './credentials';
|
||||
import { transformGenericJob, transformManagedJob } from './graphql';
|
||||
import { prepareJobAsync, sanitizedTargetName } from './prepareJob';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
|
||||
export async function prepareIosBuildAsync(
|
||||
commandCtx: CommandContext,
|
||||
@@ -28,9 +27,10 @@ export async function prepareIosBuildAsync(
|
||||
platform: Platform.IOS,
|
||||
easConfig,
|
||||
});
|
||||
const { buildProfile } = buildCtx;
|
||||
|
||||
if (
|
||||
buildCtx.buildProfile.workflow === Workflow.GENERIC &&
|
||||
buildProfile.workflow === Workflow.GENERIC &&
|
||||
!(await fs.pathExists(path.join(commandCtx.projectDir, 'ios')))
|
||||
) {
|
||||
throw new Error(
|
||||
@@ -40,42 +40,47 @@ export async function prepareIosBuildAsync(
|
||||
);
|
||||
}
|
||||
|
||||
let iosBuildScheme: string | undefined;
|
||||
let iosApplicationTarget: string | undefined;
|
||||
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
|
||||
iosBuildScheme = buildCtx.buildProfile.scheme ?? (await resolveSchemeAsync(buildCtx));
|
||||
iosApplicationTarget = await IOSConfig.BuildScheme.getApplicationTargetForSchemeAsync(
|
||||
buildCtx.commandCtx.projectDir,
|
||||
iosBuildScheme
|
||||
if (buildProfile.workflow === Workflow.MANAGED) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(
|
||||
commandCtx.projectDir,
|
||||
commandCtx.exp
|
||||
);
|
||||
} else {
|
||||
if (!commandCtx.exp.name) {
|
||||
throw new Error('"expo.name" is required in your app.json');
|
||||
}
|
||||
iosApplicationTarget = sanitizedTargetName(commandCtx.exp.name);
|
||||
if (!iosApplicationTarget) {
|
||||
throw new Error('"expo.name" needs to contain some alphanumeric characters');
|
||||
}
|
||||
}
|
||||
|
||||
// this function throws if bundle id is invalid
|
||||
await getOrConfigureBundleIdentifierAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||
{
|
||||
projectDir: commandCtx.projectDir,
|
||||
nonInteractive: commandCtx.nonInteractive,
|
||||
exp: commandCtx.exp,
|
||||
},
|
||||
buildProfile
|
||||
);
|
||||
const targets = await resolveTargetsAsync(
|
||||
{
|
||||
projectDir: commandCtx.projectDir,
|
||||
exp: commandCtx.exp,
|
||||
},
|
||||
xcodeBuildContext
|
||||
);
|
||||
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx: buildCtx,
|
||||
projectConfiguration: {
|
||||
iosBuildScheme,
|
||||
iosApplicationTarget,
|
||||
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
|
||||
return ensureIosCredentialsAsync(ctx, targets);
|
||||
},
|
||||
ensureCredentialsAsync: ensureIosCredentialsAsync,
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
await validateAndSyncProjectConfigurationAsync({
|
||||
projectDir: commandCtx.projectDir,
|
||||
exp: commandCtx.exp,
|
||||
buildProfile: buildCtx.buildProfile,
|
||||
buildProfile,
|
||||
});
|
||||
},
|
||||
prepareJobAsync,
|
||||
prepareJobAsync: async (
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData<IosCredentials>
|
||||
): Promise<Job> => {
|
||||
return await prepareJobAsync(ctx, { ...jobData, buildScheme: xcodeBuildContext.buildScheme });
|
||||
},
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
job: Ios.Job,
|
||||
@@ -100,39 +105,3 @@ export async function prepareIosBuildAsync(
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveSchemeAsync(ctx: BuildContext<Platform.IOS>): Promise<string> {
|
||||
const schemes = IOSConfig.BuildScheme.getSchemesFromXcodeproj(ctx.commandCtx.projectDir);
|
||||
if (schemes.length === 1) {
|
||||
return schemes[0];
|
||||
}
|
||||
|
||||
const sortedSchemes = sortBy(schemes);
|
||||
Log.newLine();
|
||||
Log.log(
|
||||
`We've found multiple schemes in your Xcode project: ${chalk.bold(sortedSchemes.join(', '))}`
|
||||
);
|
||||
Log.log(
|
||||
`You can specify the scheme you want to build at ${chalk.bold(
|
||||
`builds.ios.${ctx.commandCtx.profile}.scheme`
|
||||
)} in eas.json.`
|
||||
);
|
||||
if (ctx.commandCtx.nonInteractive) {
|
||||
const withoutTvOS = sortedSchemes.filter(i => !i.includes('tvOS'));
|
||||
const scheme = withoutTvOS.length > 0 ? withoutTvOS[0] : sortedSchemes[0];
|
||||
Log.log(
|
||||
`You've run Expo CLI in non-interactive mode, choosing the ${chalk.bold(scheme)} scheme.`
|
||||
);
|
||||
Log.newLine();
|
||||
return scheme;
|
||||
} else {
|
||||
const { selectedScheme } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'selectedScheme',
|
||||
message: 'Which scheme would you like to build now?',
|
||||
choices: sortedSchemes.map(scheme => ({ title: scheme, value: scheme })),
|
||||
});
|
||||
Log.newLine();
|
||||
return selectedScheme as string;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { IosBuildProfile, VersionAutoIncrement } from '@expo/eas-json';
|
||||
|
||||
import Log from '../../log';
|
||||
import {
|
||||
getOrConfigureBundleIdentifierAsync,
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
|
||||
warnIfBundleIdentifierDefinedInAppConfigForGenericProject,
|
||||
} from '../../project/ios/bundleIdentifier';
|
||||
import { ConfigureContext } from '../context';
|
||||
@@ -14,7 +14,7 @@ import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './ver
|
||||
|
||||
export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
|
||||
if (!ctx.hasIosNativeProject) {
|
||||
await getOrConfigureBundleIdentifierAsync(ctx.projectDir, ctx.exp);
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,75 +1,42 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { CredentialsSource, IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
|
||||
import { createCredentialsContextAsync } from '../../credentials/context';
|
||||
import IosCredentialsProvider, {
|
||||
IosCredentials,
|
||||
} from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { getAppLookupParamsFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
|
||||
import { IosCapabilitiesOptions } from '../../credentials/ios/appstore/ensureAppExists';
|
||||
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
|
||||
import { resolveEntitlementsJsonAsync } from '../../credentials/ios/appstore/entitlements';
|
||||
import { AppLookupParams } from '../../credentials/ios/credentials';
|
||||
import { getBundleIdentifier } from '../../project/ios/bundleIdentifier';
|
||||
import { IosCredentials, Target } from '../../credentials/ios/types';
|
||||
import { CredentialsResult } from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
|
||||
export async function ensureIosCredentialsAsync(
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
buildCtx: BuildContext<Platform.IOS>,
|
||||
targets: Target[]
|
||||
): Promise<CredentialsResult<IosCredentials> | undefined> {
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
if (!shouldProvideCredentials(buildCtx)) {
|
||||
return;
|
||||
}
|
||||
const bundleIdentifier = getBundleIdentifier(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
return await resolveIosCredentialsAsync(ctx.commandCtx.projectDir, {
|
||||
app: {
|
||||
accountName: ctx.commandCtx.accountName,
|
||||
projectName: ctx.commandCtx.projectName,
|
||||
bundleIdentifier,
|
||||
},
|
||||
|
||||
const { projectDir } = buildCtx.commandCtx;
|
||||
|
||||
const ctx = await createCredentialsContextAsync(projectDir, {
|
||||
nonInteractive: buildCtx.commandCtx.nonInteractive,
|
||||
});
|
||||
|
||||
const provider = new IosCredentialsProvider(ctx, {
|
||||
app: getAppFromContext(ctx),
|
||||
targets,
|
||||
iosCapabilitiesOptions: {
|
||||
entitlements: await resolveEntitlementsJsonAsync(
|
||||
ctx.commandCtx.projectDir,
|
||||
ctx.buildProfile.workflow
|
||||
buildCtx.commandCtx.projectDir,
|
||||
buildCtx.buildProfile.workflow
|
||||
),
|
||||
},
|
||||
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,
|
||||
distribution: buildCtx.buildProfile.distribution ?? 'store',
|
||||
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
|
||||
});
|
||||
}
|
||||
|
||||
interface ResolveCredentialsParams {
|
||||
app: AppLookupParams;
|
||||
workflow: Workflow;
|
||||
credentialsSource: CredentialsSource;
|
||||
distribution: IosDistributionType;
|
||||
enterpriseProvisioning?: IosEnterpriseProvisioning;
|
||||
iosCapabilitiesOptions?: IosCapabilitiesOptions;
|
||||
nonInteractive: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
}
|
||||
const { credentialsSource } = buildCtx.buildProfile;
|
||||
|
||||
export async function resolveIosCredentialsAsync(
|
||||
projectDir: string,
|
||||
params: ResolveCredentialsParams
|
||||
): Promise<CredentialsResult<IosCredentials>> {
|
||||
const ctx = await createCredentialsContextAsync(projectDir, {
|
||||
nonInteractive: params.nonInteractive,
|
||||
});
|
||||
const app = getAppLookupParamsFromContext(ctx);
|
||||
const provider = new IosCredentialsProvider(ctx, {
|
||||
app,
|
||||
iosCapabilitiesOptions: params.iosCapabilitiesOptions,
|
||||
distribution: params.distribution,
|
||||
enterpriseProvisioning: params.enterpriseProvisioning,
|
||||
skipCredentialsCheck: params.skipCredentialsCheck,
|
||||
});
|
||||
const { credentialsSource } = params;
|
||||
logCredentialsSource(credentialsSource, Platform.IOS);
|
||||
return {
|
||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
||||
@@ -77,6 +44,6 @@ export async function resolveIosCredentialsAsync(
|
||||
};
|
||||
}
|
||||
|
||||
function shouldProvideCredentials(ctx: BuildContext<Platform.IOS>): boolean {
|
||||
return ctx.buildProfile.distribution !== 'simulator';
|
||||
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
|
||||
return buildCtx.buildProfile.distribution !== 'simulator';
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export function transformGenericJob(job: Ios.GenericJob): IosGenericJobInput {
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
updates: job.updates,
|
||||
distribution: job.distribution && transformDistributionType(job.distribution),
|
||||
secrets: transformIosSecrets(job.secrets),
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
@@ -30,6 +31,7 @@ export function transformManagedJob(job: Ios.ManagedJob): IosManagedJobInput {
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
updates: job.updates,
|
||||
distribution: job.distribution && transformDistributionType(job.distribution),
|
||||
secrets: transformIosSecrets(job.secrets),
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
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';
|
||||
import semver from 'semver';
|
||||
|
||||
import {
|
||||
IosCredentials,
|
||||
IosTargetCredentials,
|
||||
isCredentialsMap,
|
||||
readEnvironmentSecretsAsync,
|
||||
} from '../../credentials/credentialsJson/read';
|
||||
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { gitRootDirectoryAsync } from '../../utils/git';
|
||||
import vcs from '../../vcs';
|
||||
import { BuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
|
||||
interface JobData {
|
||||
projectArchive: ArchiveSource;
|
||||
credentials?: IosCredentials;
|
||||
projectConfiguration: {
|
||||
iosBuildScheme?: string;
|
||||
iosApplicationTarget?: string;
|
||||
};
|
||||
buildScheme: string;
|
||||
}
|
||||
|
||||
export async function prepareJobAsync(
|
||||
@@ -43,7 +35,6 @@ interface CommonJobProperties {
|
||||
platform: Platform.IOS;
|
||||
projectArchive: ArchiveSource;
|
||||
builderEnvironment: Ios.BuilderEnvironment;
|
||||
releaseChannel?: string;
|
||||
distribution?: Ios.DistributionType;
|
||||
cache: Cache;
|
||||
secrets: {
|
||||
@@ -54,28 +45,14 @@ interface CommonJobProperties {
|
||||
|
||||
async function prepareJobCommonAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
{
|
||||
credentials,
|
||||
targetName,
|
||||
projectArchive,
|
||||
}: { credentials?: IosCredentials; targetName?: string; projectArchive: ArchiveSource }
|
||||
{ credentials, projectArchive }: { credentials?: IosCredentials; projectArchive: ArchiveSource }
|
||||
): Promise<Partial<CommonJobProperties>> {
|
||||
const environmentSecrets = await readEnvironmentSecretsAsync(ctx.commandCtx.projectDir);
|
||||
|
||||
let buildCredentials: CommonJobProperties['secrets']['buildCredentials'] = {};
|
||||
if (credentials && isCredentialsMap(credentials)) {
|
||||
const targets = Object.keys(credentials);
|
||||
for (const target of targets) {
|
||||
buildCredentials[target] = prepareTargetCredentials(credentials[target]);
|
||||
const buildCredentials: CommonJobProperties['secrets']['buildCredentials'] = {};
|
||||
if (credentials) {
|
||||
const targetNames = Object.keys(credentials);
|
||||
for (const targetName of targetNames) {
|
||||
buildCredentials[targetName] = prepareTargetCredentials(credentials[targetName]);
|
||||
}
|
||||
} else if (credentials) {
|
||||
// targetName
|
||||
// - for managed projects: sanitized .name from the app config
|
||||
// - for generic projects: name of the application target
|
||||
assert(targetName, 'target name should be defined');
|
||||
buildCredentials = {
|
||||
[targetName]: prepareTargetCredentials(credentials),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -83,7 +60,7 @@ async function prepareJobCommonAsync(
|
||||
projectArchive,
|
||||
distribution: ctx.buildProfile.distribution,
|
||||
builderEnvironment: {
|
||||
image: ctx.buildProfile.image,
|
||||
image: resolveImage(ctx),
|
||||
node: ctx.buildProfile.node,
|
||||
yarn: ctx.buildProfile.yarn,
|
||||
bundler: ctx.buildProfile.bundler,
|
||||
@@ -97,18 +74,28 @@ async function prepareJobCommonAsync(
|
||||
clear: ctx.commandCtx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
...(environmentSecrets ? { environmentSecrets } : {}),
|
||||
buildCredentials,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function prepareTargetCredentials(targetCredentials: IosTargetCredentials): Ios.TargetCredentials {
|
||||
function resolveImage(ctx: BuildContext<Platform.IOS>): Ios.BuilderEnvironment['image'] {
|
||||
// see https://linear.app/expo/issue/ENG-1396/make-default-image-dependent-on-sdk-version
|
||||
if (!ctx.buildProfile.image && ctx.commandCtx.exp.sdkVersion) {
|
||||
const majorSdkVersion = semver.major(ctx.commandCtx.exp.sdkVersion);
|
||||
if (majorSdkVersion <= 41) {
|
||||
return 'macos-catalina-10.15-xcode-12.4';
|
||||
}
|
||||
}
|
||||
return ctx.buildProfile.image ?? 'default';
|
||||
}
|
||||
|
||||
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
|
||||
return {
|
||||
provisioningProfileBase64: targetCredentials.provisioningProfile,
|
||||
distributionCertificate: {
|
||||
dataBase64: targetCredentials.distributionCertificate.certP12,
|
||||
password: targetCredentials.distributionCertificate.certPassword,
|
||||
dataBase64: targetCredentials.distributionCertificate.certificateP12,
|
||||
password: targetCredentials.distributionCertificate.certificatePassword,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -119,18 +106,18 @@ async function prepareGenericJobAsync(
|
||||
buildProfile: IosGenericBuildProfile
|
||||
): Promise<Partial<Ios.GenericJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, {
|
||||
credentials: jobData.credentials,
|
||||
targetName: jobData.projectConfiguration.iosApplicationTarget,
|
||||
projectArchive: jobData.projectArchive,
|
||||
})),
|
||||
type: Workflow.GENERIC,
|
||||
scheme: jobData.projectConfiguration.iosBuildScheme,
|
||||
scheme: jobData.buildScheme,
|
||||
buildConfiguration: buildProfile.schemeBuildConfiguration,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
updates: { channel: buildProfile.channel },
|
||||
projectRootDirectory,
|
||||
};
|
||||
}
|
||||
@@ -141,27 +128,18 @@ async function prepareManagedJobAsync(
|
||||
buildProfile: IosManagedBuildProfile
|
||||
): Promise<Partial<Ios.ManagedJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, {
|
||||
credentials: jobData.credentials,
|
||||
targetName: jobData.projectConfiguration.iosApplicationTarget,
|
||||
projectArchive: jobData.projectArchive,
|
||||
})),
|
||||
type: Workflow.MANAGED,
|
||||
buildType: buildProfile.buildType,
|
||||
username,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
updates: { channel: buildProfile.channel },
|
||||
projectRootDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
// copy-pasted from expo-cli/packages/xdl/src/Exp.ts
|
||||
// it's used in eject
|
||||
export function sanitizedTargetName(name: string) {
|
||||
return name
|
||||
.replace(/[\W_]+/g, '')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '');
|
||||
}
|
||||
@@ -5,11 +5,17 @@ import { getApplicationId } from '../project/android/applicationId';
|
||||
import { getBundleIdentifier } from '../project/ios/bundleIdentifier';
|
||||
import { getUsername } from '../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { gitCommitHashAsync } from '../utils/git';
|
||||
import { readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync } from './android/UpdatesModule';
|
||||
import vcs from '../vcs';
|
||||
import {
|
||||
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
|
||||
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
|
||||
} from './android/UpdatesModule';
|
||||
import { readVersionCode, readVersionName } from './android/version';
|
||||
import { BuildContext } from './context';
|
||||
import { readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync } from './ios/UpdatesModule';
|
||||
import {
|
||||
readChannelSafelyAsync as readIosChannelSafelyAsync,
|
||||
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
|
||||
} from './ios/UpdatesModule';
|
||||
import { readBuildNumberAsync, readShortVersionAsync } from './ios/version';
|
||||
import { Platform } from './types';
|
||||
import { isExpoUpdatesInstalled } from './utils/updates';
|
||||
@@ -29,7 +35,11 @@ export async function collectMetadata<T extends Platform>(
|
||||
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
|
||||
}
|
||||
): Promise<Metadata> {
|
||||
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
|
||||
return {
|
||||
// TODO: type error fixed in https://github.com/expo/eas-build/pull/34
|
||||
// remove @ts-expect-error after upgrading @expo/eas-build-job
|
||||
// @ts-expect-error error TS2322: not assignable to type 'Record<string, string | number>'
|
||||
trackingContext: ctx.trackingCtx,
|
||||
appVersion: await resolveAppVersionAsync(ctx),
|
||||
appBuildVersion: await resolveAppBuildVersionAsync(ctx),
|
||||
@@ -37,12 +47,12 @@ export async function collectMetadata<T extends Platform>(
|
||||
workflow: ctx.buildProfile.workflow,
|
||||
credentialsSource,
|
||||
sdkVersion: ctx.commandCtx.exp.sdkVersion,
|
||||
releaseChannel: await resolveReleaseChannel(ctx),
|
||||
...channelOrReleaseChannel,
|
||||
distribution: ctx.buildProfile.distribution ?? 'store',
|
||||
appName: ctx.commandCtx.exp.name,
|
||||
appIdentifier: resolveAppIdentifier(ctx),
|
||||
buildProfile: ctx.commandCtx.profile,
|
||||
gitCommitHash: await gitCommitHashAsync(),
|
||||
gitCommitHash: await vcs.getCommitHashAsync(),
|
||||
username: getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync()),
|
||||
};
|
||||
}
|
||||
@@ -76,22 +86,52 @@ function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveReleaseChannel<T extends Platform>(
|
||||
async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<{ channel: string } | { releaseChannel: string } | null> {
|
||||
if (!isExpoUpdatesInstalled(ctx.commandCtx.projectDir)) {
|
||||
return null;
|
||||
}
|
||||
if (ctx.buildProfile.channel) {
|
||||
return { channel: ctx.buildProfile.channel };
|
||||
}
|
||||
if (ctx.buildProfile.releaseChannel) {
|
||||
return { releaseChannel: ctx.buildProfile.releaseChannel };
|
||||
}
|
||||
const channel = await getNativeChannelAsync(ctx);
|
||||
if (channel) {
|
||||
return { channel };
|
||||
}
|
||||
const releaseChannel = await getNativeReleaseChannelAsync(ctx);
|
||||
return { releaseChannel };
|
||||
}
|
||||
|
||||
async function getNativeReleaseChannelAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string> {
|
||||
switch (ctx.platform) {
|
||||
case Platform.ANDROID: {
|
||||
return (await readAndroidReleaseChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? 'default';
|
||||
}
|
||||
case Platform.IOS: {
|
||||
return (await readIosReleaseChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? 'default';
|
||||
}
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
|
||||
async function getNativeChannelAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string | undefined> {
|
||||
if (!isExpoUpdatesInstalled(ctx.commandCtx.projectDir)) {
|
||||
return undefined;
|
||||
switch (ctx.platform) {
|
||||
case Platform.ANDROID: {
|
||||
return (await readAndroidChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? undefined;
|
||||
}
|
||||
case Platform.IOS: {
|
||||
return (await readIosChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.buildProfile.releaseChannel) {
|
||||
return ctx.buildProfile.releaseChannel;
|
||||
}
|
||||
|
||||
let maybeReleaseChannel: string | null;
|
||||
if (ctx.platform === Platform.ANDROID) {
|
||||
maybeReleaseChannel = await readAndroidReleaseChannelSafelyAsync(ctx.commandCtx.projectDir);
|
||||
} else {
|
||||
maybeReleaseChannel = await readIosReleaseChannelSafelyAsync(ctx.commandCtx.projectDir);
|
||||
}
|
||||
return maybeReleaseChannel ?? 'default';
|
||||
return undefined;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export enum RequestedPlatform {
|
||||
}
|
||||
|
||||
export enum BuildStatus {
|
||||
NEW = 'new',
|
||||
IN_QUEUE = 'in-queue',
|
||||
IN_PROGRESS = 'in-progress',
|
||||
ERRORED = 'errored',
|
||||
@@ -14,4 +15,4 @@ export enum BuildStatus {
|
||||
CANCELED = 'canceled',
|
||||
}
|
||||
|
||||
export type TrackingContext = Record<string, string | number>;
|
||||
export type TrackingContext = Record<string, string | number | boolean>;
|
||||
@@ -18,6 +18,8 @@ export function formatGraphQLBuild(build: BuildFragment) {
|
||||
label: 'Status',
|
||||
get value() {
|
||||
switch (build.status) {
|
||||
case GraphQLBuildStatus.New:
|
||||
return chalk.blue('new');
|
||||
case GraphQLBuildStatus.InQueue:
|
||||
return chalk.blue('in queue');
|
||||
case GraphQLBuildStatus.InProgress:
|
||||
@@ -48,12 +50,8 @@ export function formatGraphQLBuild(build: BuildFragment) {
|
||||
{
|
||||
label: 'Artifact',
|
||||
get value() {
|
||||
if (
|
||||
build.status === GraphQLBuildStatus.InQueue ||
|
||||
build.status === GraphQLBuildStatus.InProgress
|
||||
) {
|
||||
}
|
||||
switch (build.status) {
|
||||
case GraphQLBuildStatus.New:
|
||||
case GraphQLBuildStatus.InQueue:
|
||||
case GraphQLBuildStatus.InProgress:
|
||||
return '<in progress>';
|
||||
@@ -72,11 +70,13 @@ export function formatGraphQLBuild(build: BuildFragment) {
|
||||
{ label: 'Started at', value: new Date(build.createdAt).toLocaleString() },
|
||||
{
|
||||
label: 'Finished at',
|
||||
value:
|
||||
build.status === GraphQLBuildStatus.InQueue ||
|
||||
build.status === GraphQLBuildStatus.InProgress
|
||||
? '<in progress>'
|
||||
: new Date(build.updatedAt).toLocaleString(),
|
||||
value: [
|
||||
GraphQLBuildStatus.New,
|
||||
GraphQLBuildStatus.InQueue,
|
||||
GraphQLBuildStatus.InProgress,
|
||||
].includes(build.status)
|
||||
? '<in progress>'
|
||||
: new Date(build.updatedAt).toLocaleString(),
|
||||
},
|
||||
{ label: 'Started by', value: actor ?? 'unknown' },
|
||||
];
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import ora from 'ora';
|
||||
@@ -9,55 +8,16 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import Log from '../../log';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { formatBytes } from '../../utils/files';
|
||||
import {
|
||||
doesGitRepoExistAsync,
|
||||
getGitDiffOutputAsync,
|
||||
gitDiffAsync,
|
||||
gitRootDirectoryAsync,
|
||||
gitStatusAsync,
|
||||
isGitInstalledAsync,
|
||||
} from '../../utils/git';
|
||||
import { getTmpDirectory } from '../../utils/paths';
|
||||
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
|
||||
import vcs from '../../vcs';
|
||||
|
||||
async function ensureGitRepoExistsAsync(): Promise<void> {
|
||||
if (!(await isGitInstalledAsync())) {
|
||||
throw new Error('git command not found, install it before proceeding');
|
||||
}
|
||||
|
||||
if (await doesGitRepoExistAsync()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.warn("It looks like you haven't initialized the git repository yet.");
|
||||
Log.warn('EAS Build requires you to use a git repository for your project.');
|
||||
|
||||
const confirmInit = await confirmAsync({
|
||||
message: `Would you like to run 'git init' in the current directory?`,
|
||||
});
|
||||
if (!confirmInit) {
|
||||
throw new Error(
|
||||
'A git repository is required for building your project. Initialize it and run this command again.'
|
||||
);
|
||||
}
|
||||
await spawnAsync('git', ['init']);
|
||||
|
||||
Log.log("We're going to make an initial commit for you repository.");
|
||||
|
||||
await commitPromptAsync({ initialCommitMessage: 'Initial commit', commitAllFiles: true });
|
||||
}
|
||||
|
||||
async function isGitStatusCleanAsync(): Promise<boolean> {
|
||||
const changes = await gitStatusAsync({ showUntracked: true });
|
||||
return changes.length === 0;
|
||||
}
|
||||
|
||||
async function maybeBailOnGitStatusAsync(): Promise<void> {
|
||||
if (await isGitStatusCleanAsync()) {
|
||||
export async function maybeBailOnRepoStatusAsync(): Promise<void> {
|
||||
if (!(await vcs.hasUncommittedChangesAsync())) {
|
||||
return;
|
||||
}
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(`${chalk.bold('Warning!')} Your git working tree is dirty.`);
|
||||
Log.warn(`${chalk.bold('Warning!')} Your repository working tree is dirty.`);
|
||||
Log.log(
|
||||
`It's recommended to ${chalk.bold(
|
||||
'commit all your changes before proceeding'
|
||||
@@ -72,12 +32,12 @@ async function maybeBailOnGitStatusAsync(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureGitStatusIsCleanAsync(nonInteractive = false): Promise<void> {
|
||||
if (await isGitStatusCleanAsync()) {
|
||||
export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<void> {
|
||||
if (!(await vcs.hasUncommittedChangesAsync())) {
|
||||
return;
|
||||
}
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(`${chalk.bold('Warning!')} Your git working tree is dirty.`);
|
||||
Log.warn(`${chalk.bold('Warning!')} Your repository working tree is dirty.`);
|
||||
Log.log(
|
||||
`This operation needs to be run on a clean working tree, please ${chalk.bold(
|
||||
'commit all your changes before proceeding'
|
||||
@@ -96,7 +56,24 @@ async function ensureGitStatusIsCleanAsync(nonInteractive = false): Promise<void
|
||||
}
|
||||
}
|
||||
|
||||
async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
|
||||
export async function commitPromptAsync({
|
||||
initialCommitMessage,
|
||||
commitAllFiles,
|
||||
}: {
|
||||
initialCommitMessage?: string;
|
||||
commitAllFiles?: boolean;
|
||||
} = {}): Promise<void> {
|
||||
const { message } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'message',
|
||||
message: 'Commit message:',
|
||||
initial: initialCommitMessage,
|
||||
validate: (input: string) => input !== '',
|
||||
});
|
||||
await vcs.commitAsync({ commitAllFiles, commitMessage: message });
|
||||
}
|
||||
|
||||
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
|
||||
const spinner = ora('Compressing project files');
|
||||
|
||||
await fs.mkdirp(getTmpDirectory());
|
||||
@@ -118,14 +95,7 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
|
||||
startTimer(compressTimerLabel);
|
||||
|
||||
try {
|
||||
await spawnAsync('git', [
|
||||
'clone',
|
||||
'--no-hardlinks',
|
||||
'--depth',
|
||||
'1',
|
||||
`file://${await gitRootDirectoryAsync()}`,
|
||||
shallowClonePath,
|
||||
]);
|
||||
await vcs.makeShallowCopyAsync(shallowClonePath);
|
||||
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
|
||||
'.',
|
||||
]);
|
||||
@@ -151,44 +121,3 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
|
||||
|
||||
return { size, path: tarPath };
|
||||
}
|
||||
|
||||
async function showDiffAsync() {
|
||||
const outputTooLarge = (await getGitDiffOutputAsync()).split(/\r\n|\r|\n/).length > 100;
|
||||
await gitDiffAsync({ withPager: outputTooLarge });
|
||||
}
|
||||
|
||||
async function commitPromptAsync({
|
||||
initialCommitMessage,
|
||||
commitAllFiles,
|
||||
}: {
|
||||
initialCommitMessage?: string;
|
||||
commitAllFiles?: boolean;
|
||||
} = {}): Promise<void> {
|
||||
const { message } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'message',
|
||||
message: 'Commit message:',
|
||||
initial: initialCommitMessage,
|
||||
validate: (input: string) => input !== '',
|
||||
});
|
||||
if (commitAllFiles) {
|
||||
await spawnAsync('git', ['add', '-A']);
|
||||
}
|
||||
await commitChangedFilesAsync(message);
|
||||
}
|
||||
|
||||
async function commitChangedFilesAsync(message: string): Promise<void> {
|
||||
await spawnAsync('git', ['add', '-u']);
|
||||
await spawnAsync('git', ['commit', '-m', message]);
|
||||
}
|
||||
|
||||
export {
|
||||
isGitStatusCleanAsync,
|
||||
showDiffAsync,
|
||||
commitPromptAsync,
|
||||
commitChangedFilesAsync,
|
||||
ensureGitRepoExistsAsync,
|
||||
ensureGitStatusIsCleanAsync,
|
||||
maybeBailOnGitStatusAsync,
|
||||
makeProjectTarballAsync,
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Command } from '@oclif/command';
|
||||
|
||||
import {
|
||||
AnalyticsEvent,
|
||||
flushAsync as flushAnalyticsAsync,
|
||||
initAsync as initAnalyticsAsync,
|
||||
logEvent,
|
||||
} from '../analytics';
|
||||
import { getUserAsync } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
|
||||
export default abstract class EasCommand extends Command {
|
||||
/**
|
||||
* When user data is unavailable locally, determines if the command will
|
||||
* force the user to log in
|
||||
*/
|
||||
protected requiresAuthentication = true;
|
||||
|
||||
async init() {
|
||||
await initAnalyticsAsync();
|
||||
|
||||
if (this.requiresAuthentication) {
|
||||
await ensureLoggedInAsync();
|
||||
} else {
|
||||
await getUserAsync();
|
||||
}
|
||||
logEvent(AnalyticsEvent.ACTION, {
|
||||
// id is assigned by oclif in constructor based on the filepath:
|
||||
// commands/submit === submit, commands/build/list === build:list
|
||||
action: `eas ${this.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
async finally(err: Error) {
|
||||
await flushAnalyticsAsync();
|
||||
return super.finally(err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { flushAsync, initAsync, logEvent } from '../../analytics';
|
||||
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
|
||||
import { getUserAsync } from '../../user/User';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import EasCommand from '../EasCommand';
|
||||
import TestEasCommand from './TestEasCommand';
|
||||
|
||||
describe(EasCommand.name, () => {
|
||||
beforeAll(() => {
|
||||
TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
|
||||
jest.mock('../../user/User', () => ({ getUserAsync: jest.fn(() => mockJester) }));
|
||||
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
|
||||
jest.mock('../../analytics', () => {
|
||||
const { AnalyticsEvent } = jest.requireActual('../../analytics');
|
||||
return {
|
||||
AnalyticsEvent,
|
||||
logEvent: jest.fn(),
|
||||
initAsync: jest.fn(),
|
||||
flushAsync: jest.fn(),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(ensureLoggedInAsync as jest.Mock).mockClear();
|
||||
(initAsync as jest.Mock).mockClear();
|
||||
(flushAsync as jest.Mock).mockClear();
|
||||
(logEvent as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
describe('without exceptions', () => {
|
||||
it('ensures the user is logged in', async () => {
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(ensureLoggedInAsync).toHaveReturnedWith(mockJester);
|
||||
});
|
||||
|
||||
it('ensures the user data is read from cache', async () => {
|
||||
(TestEasCommand.prototype.authValue as jest.Mock).mockImplementationOnce(() => false);
|
||||
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(getUserAsync).toHaveReturnedWith(mockJester);
|
||||
});
|
||||
|
||||
it('initializes analytics', async () => {
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(initAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('flushes analytics', async () => {
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(flushAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs events', async () => {
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(logEvent).toHaveBeenCalledWith('action', {
|
||||
action: `eas ${TestEasCommand.id}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('after exceptions', () => {
|
||||
it('flushes analytics', async () => {
|
||||
try {
|
||||
await TestEasCommand.run().then(() => {
|
||||
throw new Error('foo');
|
||||
});
|
||||
} catch (error) {}
|
||||
|
||||
expect(flushAsync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import EasCommand from '../EasCommand';
|
||||
|
||||
export default class TestEasCommand extends EasCommand {
|
||||
requiresAuthentication = this.authValue();
|
||||
|
||||
authValue() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async run() {}
|
||||
}
|
||||
|
||||
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { getBranchNameAsync } from '../../utils/git';
|
||||
import vcs from '../../vcs';
|
||||
|
||||
export async function createUpdateBranchOnAppAsync({
|
||||
appId,
|
||||
@@ -93,7 +93,7 @@ export default class BranchCreate extends Command {
|
||||
name: 'name',
|
||||
message: 'Please name the branch:',
|
||||
initial:
|
||||
(await getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`,
|
||||
(await vcs.getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`,
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
} from '../../project/publish';
|
||||
import { promptAsync, selectAsync } from '../../prompts';
|
||||
import { formatUpdate } from '../../update/utils';
|
||||
import { getLastCommitMessageAsync } from '../../utils/git';
|
||||
import vcs from '../../vcs';
|
||||
import { listBranchesAsync } from './list';
|
||||
import { viewUpdateBranchAsync } from './view';
|
||||
|
||||
@@ -192,43 +192,68 @@ export default class BranchPublish extends Command {
|
||||
if (group) {
|
||||
updatesToRepublish = await getUpdateGroupAsync({ group });
|
||||
} else {
|
||||
const updateGroups = uniqBy(updates, u => u.group).map(update => ({
|
||||
title: formatUpdateTitle(update),
|
||||
value: update.group,
|
||||
}));
|
||||
// Drop into interactive mode if the user has not specified an update group to republish.
|
||||
if (jsonFlag) {
|
||||
throw new Error('You must specify the update group to republish.');
|
||||
}
|
||||
|
||||
const updateGroups = uniqBy(updates, u => u.group)
|
||||
.filter(update => {
|
||||
// Only show groups that have updates on the specified platform(s).
|
||||
return platformFlag === 'all' || update.platform === platformFlag;
|
||||
})
|
||||
.map(update => ({
|
||||
title: formatUpdateTitle(update),
|
||||
value: update.group,
|
||||
}));
|
||||
if (updateGroups.length === 0) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${name}". Did you mean to do a regular publish?`
|
||||
`There are no updates on branch "${name}" published on the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
|
||||
);
|
||||
}
|
||||
const updateGroup = await selectAsync<string>(
|
||||
|
||||
const selectedUpdateGroup = await selectAsync<string>(
|
||||
'which update would you like to republish?',
|
||||
updateGroups
|
||||
);
|
||||
updatesToRepublish = updates.filter(update => update.group === updateGroup);
|
||||
updatesToRepublish = updates.filter(update => update.group === selectedUpdateGroup);
|
||||
}
|
||||
const updatesToRepublishFilteredByPlatform = updatesToRepublish.filter(
|
||||
// Only republish to the specified platforms
|
||||
update => platformFlag === 'all' || update.platform === platformFlag
|
||||
);
|
||||
if (updatesToRepublishFilteredByPlatform.length === 0) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${name}" published on the platform(s) "${platformFlag}" with group ID "${
|
||||
group ? group : updatesToRepublish[0].group
|
||||
}". Did you mean to publish a new update instead?`
|
||||
);
|
||||
}
|
||||
|
||||
if (updatesToRepublish.length === 0) {
|
||||
throw new Error(`There are no updates in this group`);
|
||||
let publicationPlatformMessage: string;
|
||||
if (platformFlag === 'all') {
|
||||
if (updatesToRepublishFilteredByPlatform.length !== defaultPublishPlatforms.length) {
|
||||
Log.warn(`You are republishing an update that wasn't published for all platforms.`);
|
||||
}
|
||||
publicationPlatformMessage = `The republished update will appear on the same plaforms it was originally published on: ${updatesToRepublishFilteredByPlatform
|
||||
.map(update => update.platform)
|
||||
.join(',')}`;
|
||||
} else {
|
||||
publicationPlatformMessage = `The republished update will appear only on: ${platformFlag}`;
|
||||
}
|
||||
Log.withTick(publicationPlatformMessage);
|
||||
|
||||
for (const update of updatesToRepublish) {
|
||||
for (const update of updatesToRepublishFilteredByPlatform) {
|
||||
const { manifestFragment } = update;
|
||||
const platform = update.platform as PublishPlatform;
|
||||
|
||||
if (platformFlag === 'all' || platformFlag === platform) {
|
||||
updateInfoGroup[platform] = JSON.parse(manifestFragment);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updateInfoGroup).length === 0) {
|
||||
throw new Error(`There are no updates for platform ${platformFlag} in this group`);
|
||||
updateInfoGroup[platform] = JSON.parse(manifestFragment);
|
||||
}
|
||||
|
||||
// These are the same for each member of an update group
|
||||
group = updatesToRepublish[0].group;
|
||||
oldMessage = updatesToRepublish[0].message ?? '';
|
||||
oldRuntimeVersion = updatesToRepublish[0].runtimeVersion;
|
||||
group = updatesToRepublishFilteredByPlatform[0].group;
|
||||
oldMessage = updatesToRepublishFilteredByPlatform[0].message ?? '';
|
||||
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
|
||||
} else {
|
||||
// build bundle and upload assets for a new publish
|
||||
if (!skipBundler) {
|
||||
@@ -259,7 +284,7 @@ export default class BranchPublish extends Command {
|
||||
message: `Please enter a publication message.`,
|
||||
initial: republish
|
||||
? `Republish "${oldMessage!}" - group: ${group}`
|
||||
: (await getLastCommitMessageAsync())?.trim(),
|
||||
: (await vcs.getLastCommitMessageAsync())?.trim(),
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -47,7 +47,15 @@ function formatUnfinishedBuild(
|
||||
): string {
|
||||
const platform = appPlatformEmojis[build.platform];
|
||||
const startTime = new Date(build.createdAt).toLocaleString();
|
||||
const status = chalk.blue(build.status === BuildStatus.InQueue ? 'in-queue' : 'in-progress');
|
||||
let statusText: string;
|
||||
if (build.status === BuildStatus.New) {
|
||||
statusText = 'new';
|
||||
} else if (build.status === BuildStatus.InQueue) {
|
||||
statusText = 'in queue';
|
||||
} else {
|
||||
statusText = 'in progress';
|
||||
}
|
||||
const status = chalk.blue(statusText);
|
||||
return `${platform} Started at: ${startTime}, Status: ${status}, Id: ${build.id}`;
|
||||
}
|
||||
|
||||
@@ -58,12 +66,13 @@ async function selectBuildToCancelAsync(
|
||||
const spinner = ora().start('Fetching the uncompleted builds…');
|
||||
let builds;
|
||||
try {
|
||||
const [inQueueBuilds, inProgressBuilds] = await Promise.all([
|
||||
const [newBuilds, inQueueBuilds, inProgressBuilds] = await Promise.all([
|
||||
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.New }),
|
||||
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.InQueue }),
|
||||
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.InProgress }),
|
||||
]);
|
||||
spinner.stop();
|
||||
builds = [...inQueueBuilds, ...inProgressBuilds];
|
||||
builds = [...newBuilds, ...inQueueBuilds, ...inProgressBuilds];
|
||||
} catch (error) {
|
||||
spinner.fail(
|
||||
`Something went wrong and we couldn't fetch the builds for the project ${projectFullName}.`
|
||||
|
||||
@@ -17,10 +17,6 @@ export default class BuildConfigure extends Command {
|
||||
char: 'p',
|
||||
options: ['android', 'ios', 'all'],
|
||||
}),
|
||||
'allow-experimental': flags.boolean({
|
||||
description: 'Enable experimental configuration steps.',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
@@ -32,20 +28,10 @@ export default class BuildConfigure extends Command {
|
||||
|
||||
const platform =
|
||||
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
|
||||
const allowExperimental = flags['allow-experimental'];
|
||||
|
||||
if (allowExperimental) {
|
||||
Log.warn(
|
||||
`Project configuration will execute some additional steps that might fail if structure of your native project is significantly different from ${chalk.bold(
|
||||
'expo eject'
|
||||
)} or ${chalk.bold('expo init')}`
|
||||
);
|
||||
}
|
||||
|
||||
await ensureLoggedInAsync();
|
||||
await configureAsync({
|
||||
platform,
|
||||
allowExperimental,
|
||||
projectDir: (await findProjectRootAsync()) ?? process.cwd(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import { ExpoConfig, getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import {
|
||||
flushAsync as flushAnalyticsAsync,
|
||||
initAsync as initAnalyticsAsync,
|
||||
} from '../../analytics';
|
||||
import { configureAsync } from '../../build/configure';
|
||||
import { createCommandContextAsync } from '../../build/context';
|
||||
import { buildAsync } from '../../build/create';
|
||||
import { RequestedPlatform } from '../../build/types';
|
||||
import { isGitStatusCleanAsync } from '../../build/utils/repository';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
@@ -20,16 +16,16 @@ import {
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import vcs from '../../vcs';
|
||||
|
||||
export default class Build extends Command {
|
||||
export default class Build extends EasCommand {
|
||||
static description = 'Start a build';
|
||||
|
||||
static flags = {
|
||||
platform: flags.enum({ char: 'p', options: ['android', 'ios', 'all'] }),
|
||||
'skip-credentials-check': flags.boolean({
|
||||
default: false,
|
||||
description: 'Skip validation of build credentials',
|
||||
hidden: true,
|
||||
}),
|
||||
'skip-project-configuration': flags.boolean({
|
||||
default: false,
|
||||
@@ -60,51 +56,52 @@ export default class Build extends Command {
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { flags } = this.parse(Build);
|
||||
await initAnalyticsAsync(); // TODO: run in oclif hook for every command
|
||||
try {
|
||||
await ensureLoggedInAsync();
|
||||
|
||||
const nonInteractive = flags['non-interactive'];
|
||||
if (!flags.platform && nonInteractive) {
|
||||
throw new Error('--platform is required when building in non-interactive mode');
|
||||
}
|
||||
const platform =
|
||||
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
let { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!flags.local && !(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags.local && !verifyOptionsForLocalBuilds(platform)) {
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
exp = (await ensureProjectConfiguredAsync(projectDir)) ?? exp;
|
||||
|
||||
const commandCtx = await createCommandContextAsync({
|
||||
requestedPlatform: platform,
|
||||
profile: flags.profile,
|
||||
exp,
|
||||
projectDir,
|
||||
projectId,
|
||||
nonInteractive,
|
||||
clearCache: flags['clear-cache'],
|
||||
skipCredentialsCheck: flags['skip-credentials-check'],
|
||||
local: flags.local,
|
||||
skipProjectConfiguration: flags['skip-project-configuration'],
|
||||
waitForBuildEnd: flags.wait,
|
||||
});
|
||||
await buildAsync(commandCtx);
|
||||
} finally {
|
||||
await flushAnalyticsAsync(); // TODO: run in oclif hook for every command
|
||||
const nonInteractive = flags['non-interactive'];
|
||||
if (!flags.platform && nonInteractive) {
|
||||
throw new Error('--platform is required when building in non-interactive mode');
|
||||
}
|
||||
const platform =
|
||||
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
|
||||
|
||||
if (flags['skip-credentials-check']) {
|
||||
Log.warnDeprecatedFlag(
|
||||
'skip-credentials-check',
|
||||
'Build credential validation is always skipped with the --non-interactive flag. You can also skip interactively.'
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
let { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!flags.local && !(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags.local && !verifyOptionsForLocalBuilds(platform)) {
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
exp = (await ensureProjectConfiguredAsync(projectDir)) ?? exp;
|
||||
|
||||
const commandCtx = await createCommandContextAsync({
|
||||
requestedPlatform: platform,
|
||||
profile: flags.profile,
|
||||
exp,
|
||||
projectDir,
|
||||
projectId,
|
||||
nonInteractive,
|
||||
clearCache: flags['clear-cache'],
|
||||
local: flags.local,
|
||||
skipProjectConfiguration: flags['skip-project-configuration'],
|
||||
waitForBuildEnd: flags.wait,
|
||||
});
|
||||
await buildAsync(commandCtx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,11 +151,10 @@ async function ensureProjectConfiguredAsync(projectDir: string): Promise<ExpoCon
|
||||
await configureAsync({
|
||||
projectDir,
|
||||
platform: RequestedPlatform.All,
|
||||
allowExperimental: false,
|
||||
});
|
||||
if (!(await isGitStatusCleanAsync())) {
|
||||
if (await vcs.hasUncommittedChangesAsync()) {
|
||||
throw new Error(
|
||||
'Build process requires clean git working tree, please commit all your changes and run `eas build` again'
|
||||
'Build process requires clean working tree, please commit all your changes and run `eas build` again'
|
||||
);
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
@@ -23,6 +23,7 @@ export default class BuildList extends Command {
|
||||
}),
|
||||
status: flags.enum({
|
||||
options: [
|
||||
BuildStatus.NEW,
|
||||
BuildStatus.IN_QUEUE,
|
||||
BuildStatus.IN_PROGRESS,
|
||||
BuildStatus.ERRORED,
|
||||
@@ -92,6 +93,8 @@ const toAppPlatform = (requestedPlatform?: RequestedPlatform): AppPlatform | und
|
||||
const toGraphQLBuildStatus = (buildStatus: BuildStatus): GraphQLBuildStatus | undefined => {
|
||||
if (!buildStatus) {
|
||||
return undefined;
|
||||
} else if (buildStatus === BuildStatus.NEW) {
|
||||
return GraphQLBuildStatus.New;
|
||||
} else if (buildStatus === BuildStatus.IN_QUEUE) {
|
||||
return GraphQLBuildStatus.InQueue;
|
||||
} else if (buildStatus === BuildStatus.IN_PROGRESS) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import { assert } from 'console';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Command } from '@oclif/command';
|
||||
|
||||
import { CredentialsManager } from '../credentials/CredentialsManager';
|
||||
import { createCredentialsContextAsync } from '../credentials/context';
|
||||
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
|
||||
|
||||
@@ -9,6 +8,6 @@ export default class Credentials extends Command {
|
||||
|
||||
async run() {
|
||||
const ctx = await createCredentialsContextAsync(process.cwd(), {});
|
||||
new CredentialsManager(ctx).runActionAsync(new SelectPlatform());
|
||||
await new SelectPlatform().runAsync(ctx);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { flags } from '@oclif/command';
|
||||
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { AppPlatform } from '../graphql/generated';
|
||||
import { learnMore } from '../log';
|
||||
import { isEasEnabledForProjectAsync, warnEasUnavailable } from '../project/isEasEnabledForProject';
|
||||
@@ -9,13 +10,12 @@ import { promptAsync } from '../prompts';
|
||||
import AndroidSubmitCommand from '../submissions/android/AndroidSubmitCommand';
|
||||
import IosSubmitCommand from '../submissions/ios/IosSubmitCommand';
|
||||
import { AndroidSubmitCommandFlags, IosSubmitCommandFlags } from '../submissions/types';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
|
||||
const COMMON_FLAGS = '';
|
||||
const ANDROID_FLAGS = 'Android specific options';
|
||||
const IOS_FLAGS = 'iOS specific options';
|
||||
|
||||
export default class BuildSubmit extends Command {
|
||||
export default class BuildSubmit extends EasCommand {
|
||||
static description = 'Submits build artifact to app store';
|
||||
static usage = 'submit --platform=(android|ios)';
|
||||
static aliases = ['build:submit'];
|
||||
@@ -160,8 +160,6 @@ export default class BuildSubmit extends Command {
|
||||
},
|
||||
} = this.parse(BuildSubmit);
|
||||
|
||||
await ensureLoggedInAsync();
|
||||
|
||||
const platform =
|
||||
(flags.platform?.toUpperCase() as AppPlatform | undefined) ??
|
||||
(await promptForPlatformAsync());
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Context } from './context';
|
||||
|
||||
export interface Action<T = void> {
|
||||
runAsync(manager: CredentialsManager, ctx: Context): Promise<T>;
|
||||
}
|
||||
|
||||
export class CredentialsManager {
|
||||
constructor(private ctx: Context) {}
|
||||
|
||||
public async runActionAsync<T>(action: Action<T>): Promise<T> {
|
||||
return await action.runAsync(this, this.ctx);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { getAndroidApiMock } from '../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../__tests__/fixtures-context';
|
||||
|
||||
export const createCredentialsContextAsync = () =>
|
||||
createCtxMock({
|
||||
android: getAndroidApiMock(),
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
import {
|
||||
AndroidAppBuildCredentialsFragment,
|
||||
AndroidKeystoreFragment,
|
||||
AndroidKeystoreType,
|
||||
AppFragment,
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
} from '../../graphql/generated';
|
||||
import { testKeystore } from './fixtures-android';
|
||||
import { testKeystoreBase64 } from './fixtures-base64-data';
|
||||
|
||||
export const testAppFragment: AppFragment = {
|
||||
id: 'test-app-id',
|
||||
fullName: '@testuser/testapp',
|
||||
slug: 'testapp',
|
||||
};
|
||||
|
||||
export const testJksAndroidKeystoreFragment: AndroidKeystoreFragment = {
|
||||
id: 'test-id',
|
||||
type: AndroidKeystoreType.Jks,
|
||||
keystore: testKeystoreBase64,
|
||||
keystorePassword: testKeystore.keystorePassword,
|
||||
keyAlias: testKeystore.keyAlias,
|
||||
keyPassword: testKeystore.keyPassword,
|
||||
md5CertificateFingerprint: 'test-md5',
|
||||
sha1CertificateFingerprint: 'test-sha1',
|
||||
sha256CertificateFingerprint: 'test-sha256',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
export const testLegacyAndroidBuildCredentialsFragment: AndroidAppBuildCredentialsFragment = {
|
||||
id: 'test-id',
|
||||
isDefault: true,
|
||||
isLegacy: true,
|
||||
name: 'legacy',
|
||||
androidKeystore: testJksAndroidKeystoreFragment,
|
||||
};
|
||||
|
||||
export const testLegacyAndroidAppCredentialsFragment: CommonAndroidAppCredentialsFragment = {
|
||||
id: 'test-id',
|
||||
applicationIdentifier: null,
|
||||
isLegacy: true,
|
||||
app: testAppFragment,
|
||||
androidAppBuildCredentialsList: [testLegacyAndroidBuildCredentialsFragment],
|
||||
};
|
||||
|
||||
export const testAndroidBuildCredentialsFragment: AndroidAppBuildCredentialsFragment = {
|
||||
id: 'test-id',
|
||||
isDefault: true,
|
||||
isLegacy: false,
|
||||
name: 'Google App Store Build Credentials',
|
||||
androidKeystore: testJksAndroidKeystoreFragment,
|
||||
};
|
||||
|
||||
export const testAndroidAppCredentialsFragment: CommonAndroidAppCredentialsFragment = {
|
||||
id: 'test-id',
|
||||
applicationIdentifier: null,
|
||||
isLegacy: false,
|
||||
app: testAppFragment,
|
||||
androidAppBuildCredentialsList: [testLegacyAndroidBuildCredentialsFragment],
|
||||
};
|
||||
|
||||
export function getNewAndroidApiMockWithoutCredentials() {
|
||||
return {
|
||||
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(),
|
||||
getAndroidAppBuildCredentialsListAsync: jest.fn(() => []),
|
||||
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(),
|
||||
getLegacyAndroidAppBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
updateAndroidAppBuildCredentialsAsync: jest.fn(),
|
||||
createAndroidAppBuildCredentialsAsync: jest.fn(),
|
||||
getDefaultAndroidAppBuildCredentialsAsync: jest.fn(),
|
||||
getAndroidAppBuildCredentialsByNameAsync: jest.fn(),
|
||||
createOrUpdateAndroidAppBuildCredentialsByNameAsync: jest.fn(),
|
||||
createKeystoreAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,19 @@
|
||||
import { AndroidCredentials, Keystore } from '../android/credentials';
|
||||
import {
|
||||
AndroidAppBuildCredentialsFragment,
|
||||
AndroidFcmFragment,
|
||||
AndroidFcmVersion,
|
||||
AndroidKeystoreFragment,
|
||||
AndroidKeystoreType,
|
||||
AppFragment,
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
} from '../../graphql/generated';
|
||||
import { Keystore } from '../android/credentials';
|
||||
import {
|
||||
testKeystore2Base64,
|
||||
testKeystoreBase64,
|
||||
testPKCS12KeystoreBase64,
|
||||
testPKCS12KeystoreEmptyPasswordBase64,
|
||||
} from './fixtures-base64-data';
|
||||
import { testExperienceName, testJester2ExperienceName } from './fixtures-constants';
|
||||
|
||||
export const testKeystore: Keystore = {
|
||||
keystore: testKeystoreBase64,
|
||||
@@ -28,56 +37,98 @@ export const testPKCS12Keystore: Keystore = {
|
||||
keyAlias: 'test-alias',
|
||||
};
|
||||
|
||||
export const testPushCredentials = {
|
||||
fcmApiKey: 'examplefcmapikey',
|
||||
// openssl req -new -newkey rsa:4096 -nodes -keyout test.key -out test.csr
|
||||
// openssl x509 -req -sha256 -days 365 -in test.csr -signkey test.key -out test.pem
|
||||
// openssl pkcs12 -export -in test.pem -inkey test.key -name "test-alias" -passout pass: -out emptyPassword.p12
|
||||
// cat emptyPassword.p12 | base64
|
||||
export const testPKCS12EmptyPasswordKeystore: Keystore = {
|
||||
keystore: testPKCS12KeystoreEmptyPasswordBase64,
|
||||
keystorePassword: '',
|
||||
keyAlias: 'test-alias',
|
||||
};
|
||||
|
||||
export const testAppCredentials = {
|
||||
experienceName: testExperienceName,
|
||||
keystore: testKeystore,
|
||||
pushCredentials: testPushCredentials,
|
||||
export const testAppFragment: AppFragment = {
|
||||
id: 'test-app-id',
|
||||
fullName: '@testuser/testapp',
|
||||
slug: 'testapp',
|
||||
};
|
||||
|
||||
export const testJester2AppCredentials = {
|
||||
experienceName: testJester2ExperienceName,
|
||||
keystore: testKeystore2,
|
||||
pushCredentials: testPushCredentials,
|
||||
export const testLegacyAndroidFcmFragment: AndroidFcmFragment = {
|
||||
id: 'test-id',
|
||||
snippet: {
|
||||
firstFourCharacters: 'abcd',
|
||||
lastFourCharacters: 'efgh',
|
||||
},
|
||||
credential: 'abcdxxxxxxefgh',
|
||||
version: AndroidFcmVersion.Legacy,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
export const testAllCredentials: { [key: string]: AndroidCredentials } = {
|
||||
[testExperienceName]: testAppCredentials,
|
||||
export const testJksAndroidKeystoreFragment: AndroidKeystoreFragment = {
|
||||
id: 'test-id',
|
||||
type: AndroidKeystoreType.Jks,
|
||||
keystore: testKeystoreBase64,
|
||||
keystorePassword: testKeystore.keystorePassword,
|
||||
keyAlias: testKeystore.keyAlias,
|
||||
keyPassword: testKeystore.keyPassword,
|
||||
md5CertificateFingerprint: 'test-md5',
|
||||
sha1CertificateFingerprint: 'test-sha1',
|
||||
sha256CertificateFingerprint: 'test-sha256',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
export function getApiClientWrapperMock() {
|
||||
// by default all method throw exceptions to make sure that we only call what is expected
|
||||
const getUnexpectedCallMock = () =>
|
||||
jest.fn(() => {
|
||||
throw new Error('unexpected call');
|
||||
});
|
||||
export const testLegacyAndroidBuildCredentialsFragment: AndroidAppBuildCredentialsFragment = {
|
||||
id: 'test-id',
|
||||
isDefault: true,
|
||||
isLegacy: true,
|
||||
name: 'legacy',
|
||||
androidKeystore: testJksAndroidKeystoreFragment,
|
||||
};
|
||||
|
||||
export const testLegacyAndroidAppCredentialsFragment: CommonAndroidAppCredentialsFragment = {
|
||||
id: 'test-id',
|
||||
applicationIdentifier: null,
|
||||
isLegacy: true,
|
||||
app: testAppFragment,
|
||||
androidFcm: testLegacyAndroidFcmFragment,
|
||||
androidAppBuildCredentialsList: [testLegacyAndroidBuildCredentialsFragment],
|
||||
};
|
||||
|
||||
export const testAndroidBuildCredentialsFragment: AndroidAppBuildCredentialsFragment = {
|
||||
id: 'test-id',
|
||||
isDefault: true,
|
||||
isLegacy: false,
|
||||
name: 'Google App Store Build Credentials',
|
||||
androidKeystore: testJksAndroidKeystoreFragment,
|
||||
};
|
||||
|
||||
export const testAndroidAppCredentialsFragment: CommonAndroidAppCredentialsFragment = {
|
||||
id: 'test-id',
|
||||
applicationIdentifier: null,
|
||||
isLegacy: false,
|
||||
app: testAppFragment,
|
||||
androidFcm: testLegacyAndroidFcmFragment,
|
||||
androidAppBuildCredentialsList: [testLegacyAndroidBuildCredentialsFragment],
|
||||
};
|
||||
|
||||
export function getNewAndroidApiMock() {
|
||||
return {
|
||||
getAllCredentialsApi: getUnexpectedCallMock(),
|
||||
getAllCredentialsForAppApi: getUnexpectedCallMock(),
|
||||
updateKeystoreApi: getUnexpectedCallMock(),
|
||||
updateFcmKeyApi: getUnexpectedCallMock(),
|
||||
removeKeystoreApi: getUnexpectedCallMock(),
|
||||
removeFcmKeyApi: getUnexpectedCallMock(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getAndroidApiMock() {
|
||||
return {
|
||||
fetchAllAsync: jest.fn(() => testAllCredentials),
|
||||
fetchKeystoreAsync: jest.fn(() => testKeystore),
|
||||
updateKeystoreAsync: jest.fn(),
|
||||
removeKeystoreAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getAndroidApiMockWithoutCredentials() {
|
||||
return {
|
||||
fetchAllAsync: jest.fn(() => []),
|
||||
fetchKeystoreAsync: jest.fn(() => null),
|
||||
updateKeystoreAsync: jest.fn(),
|
||||
removeKeystoreAsync: jest.fn(),
|
||||
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(),
|
||||
getAndroidAppBuildCredentialsListAsync: jest.fn(() => []),
|
||||
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(),
|
||||
getLegacyAndroidAppBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
updateAndroidAppCredentialsAsync: jest.fn(),
|
||||
updateAndroidAppBuildCredentialsAsync: jest.fn(),
|
||||
createAndroidAppBuildCredentialsAsync: jest.fn(),
|
||||
getDefaultAndroidAppBuildCredentialsAsync: jest.fn(),
|
||||
getAndroidAppBuildCredentialsByNameAsync: jest.fn(),
|
||||
createOrUpdateAndroidAppBuildCredentialsByNameAsync: jest.fn(),
|
||||
createKeystoreAsync: jest.fn(),
|
||||
createFcmAsync: jest.fn(),
|
||||
deleteKeystoreAsync: jest.fn(),
|
||||
deleteFcmAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
@@ -1,18 +1,15 @@
|
||||
import merge from 'lodash/merge';
|
||||
|
||||
import { CredentialsManager } from '../CredentialsManager';
|
||||
import { Context } from '../context';
|
||||
import { getAndroidApiMockWithoutCredentials } from './fixtures-android';
|
||||
import { getNewAndroidApiMockWithoutCredentials } from './fixtures-android-new';
|
||||
import { getNewAndroidApiMock } from './fixtures-android';
|
||||
import { getAppstoreMock } from './fixtures-appstore';
|
||||
import { testAppJson, testUsername } from './fixtures-constants';
|
||||
import { getNewIosApiMockWithoutCredentials } from './fixtures-ios';
|
||||
import { getNewIosApiMock } from './fixtures-ios';
|
||||
|
||||
export function createCtxMock(mockOverride: Record<string, any> = {}): Context {
|
||||
const defaultMock = {
|
||||
ios: getNewIosApiMockWithoutCredentials(),
|
||||
android: getAndroidApiMockWithoutCredentials(),
|
||||
newAndroid: getNewAndroidApiMockWithoutCredentials(),
|
||||
ios: getNewIosApiMock(),
|
||||
android: getNewAndroidApiMock(),
|
||||
appStore: getAppstoreMock(),
|
||||
bestEffortAppStoreAuthenticateAsync: jest.fn(),
|
||||
ensureAppleCtx: jest.fn(),
|
||||
@@ -29,14 +26,3 @@ export function createCtxMock(mockOverride: Record<string, any> = {}): Context {
|
||||
};
|
||||
return merge(defaultMock, mockOverride) as any;
|
||||
}
|
||||
|
||||
export function createManagerMock(mockOverride: Record<string, any> = {}): CredentialsManager {
|
||||
return merge(
|
||||
{
|
||||
runActionAsync: jest.fn(() => {
|
||||
throw new Error('unexpected call'); // should be implemented in test directly
|
||||
}),
|
||||
},
|
||||
mockOverride
|
||||
) as any;
|
||||
}
|
||||
@@ -3,20 +3,15 @@ import {
|
||||
AppleAppIdentifierFragment,
|
||||
AppleDistributionCertificateFragment,
|
||||
AppleProvisioningProfileFragment,
|
||||
ApplePushKeyFragment,
|
||||
AppleTeamFragment,
|
||||
CommonIosAppCredentialsFragment,
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
} from '../../graphql/generated';
|
||||
import { IosAppCredentialsWithBuildCredentialsQueryResult } from '../ios/api/graphql/queries/IosAppCredentialsQuery';
|
||||
import {
|
||||
DistributionCertificate,
|
||||
ProvisioningProfile,
|
||||
PushKey,
|
||||
} from '../ios/appstore/Credentials.types';
|
||||
import { IosDistCredentials, IosPushCredentials } from '../ios/credentials';
|
||||
import { DistributionCertificate, ProvisioningProfile } from '../ios/appstore/Credentials.types';
|
||||
import { Target } from '../ios/types';
|
||||
import { testProvisioningProfileBase64 } from './fixtures-base64-data';
|
||||
import { testBundleIdentifier, testExperienceName } from './fixtures-constants';
|
||||
|
||||
export const testProvisioningProfile: ProvisioningProfile = {
|
||||
provisioningProfileId: 'test-id',
|
||||
@@ -40,6 +35,16 @@ export const testAppFragment: AppFragment = {
|
||||
slug: 'testapp',
|
||||
};
|
||||
|
||||
export const testPushKey: ApplePushKeyFragment = {
|
||||
id: 'test-push-key-id',
|
||||
keyIdentifier: 'test-key-identifier',
|
||||
appleTeam: { ...testAppleTeamFragment },
|
||||
updatedAt: new Date(),
|
||||
iosAppCredentialsList: [],
|
||||
};
|
||||
|
||||
export const testTargets: Target[] = [{ targetName: 'testapp', bundleIdentifier: 'foo.bar.com' }];
|
||||
|
||||
export const testProvisioningProfileFragment: AppleProvisioningProfileFragment = {
|
||||
id: 'test-prov-prof-id-1',
|
||||
expiration: new Date(),
|
||||
@@ -102,11 +107,7 @@ export const testCommonIosAppCredentialsFragment: CommonIosAppCredentialsFragmen
|
||||
app: testAppFragment,
|
||||
appleTeam: testAppleTeamFragment,
|
||||
appleAppIdentifier: testAppleAppIdentifierFragment,
|
||||
iosAppBuildCredentialsList: [testIosAppBuildCredentialsFragment],
|
||||
};
|
||||
|
||||
export const testIosAppCredentialsWithBuildCredentialsQueryResult: IosAppCredentialsWithBuildCredentialsQueryResult = {
|
||||
id: 'test-app-credential-id',
|
||||
pushKey: testPushKey,
|
||||
iosAppBuildCredentialsList: [testIosAppBuildCredentialsFragment],
|
||||
};
|
||||
|
||||
@@ -114,11 +115,13 @@ export const testAppleTeam = {
|
||||
id: 'test-team-id',
|
||||
};
|
||||
|
||||
export function getNewIosApiMockWithoutCredentials() {
|
||||
export function getNewIosApiMock() {
|
||||
return {
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(),
|
||||
createOrGetIosAppCredentialsWithCommonFieldsAsync: jest.fn(),
|
||||
updateIosAppCredentialsAsync: jest.fn(),
|
||||
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(),
|
||||
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingAppleTeamAsync: () => testAppleTeam,
|
||||
createOrGetExistingAppleAppIdentifierAsync: jest.fn(),
|
||||
getDevicesForAppleTeamAsync: jest.fn(),
|
||||
@@ -130,25 +133,9 @@ export function getNewIosApiMockWithoutCredentials() {
|
||||
getDistributionCertificatesForAccountAsync: jest.fn(),
|
||||
createDistributionCertificateAsync: jest.fn(),
|
||||
deleteDistributionCertificateAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getNewIosApiMockWithCredentials() {
|
||||
return {
|
||||
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(),
|
||||
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingAppleTeamAsync: jest.fn(),
|
||||
createOrGetExistingAppleAppIdentifierAsync: jest.fn(),
|
||||
getDevicesForAppleTeamAsync: jest.fn(),
|
||||
createProvisioningProfileAsync: jest.fn(),
|
||||
getProvisioningProfileAsync: jest.fn(),
|
||||
updateProvisioningProfileAsync: jest.fn(),
|
||||
deleteProvisioningProfilesAsync: jest.fn(),
|
||||
getDistributionCertificateForAppAsync: jest.fn(),
|
||||
getDistributionCertificatesForAccountAsync: jest.fn(),
|
||||
createDistributionCertificateAsync: jest.fn(),
|
||||
deleteDistributionCertificateAsync: jest.fn(),
|
||||
createPushKeyAsync: jest.fn(),
|
||||
getPushKeyForAppAsync: jest.fn(),
|
||||
deletePushKeyAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -158,39 +145,3 @@ export const testDistCert: DistributionCertificate = {
|
||||
distCertSerialNumber: 'test-serial',
|
||||
teamId: 'test-team-id',
|
||||
};
|
||||
export const testIosDistCredential: IosDistCredentials = {
|
||||
id: 1,
|
||||
type: 'dist-cert',
|
||||
...testDistCert,
|
||||
};
|
||||
|
||||
export const testPushKey: PushKey = {
|
||||
apnsKeyP8: 'test-p8',
|
||||
apnsKeyId: 'test-key-id',
|
||||
teamId: 'test-team-id',
|
||||
};
|
||||
|
||||
/**
|
||||
* Legacy format fixtures
|
||||
*/
|
||||
|
||||
export const testIosPushCredential: IosPushCredentials = {
|
||||
id: 2,
|
||||
type: 'push-key',
|
||||
...testPushKey,
|
||||
};
|
||||
|
||||
export const testAppCredential = {
|
||||
experienceName: testExperienceName,
|
||||
bundleIdentifier: testBundleIdentifier,
|
||||
distCredentialsId: testIosDistCredential.id,
|
||||
pushCredentialsId: testIosPushCredential.id,
|
||||
credentials: {
|
||||
...testProvisioningProfile,
|
||||
},
|
||||
};
|
||||
export const testAllCredentialsForApp = {
|
||||
...testAppCredential,
|
||||
pushCredentials: testPushKey,
|
||||
distCredentials: testDistCert,
|
||||
};
|
||||
@@ -1,24 +1,20 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { CredentialsManager } from '../CredentialsManager';
|
||||
import { AndroidAppBuildCredentialsFragment } from '../../graphql/generated';
|
||||
import { Context } from '../context';
|
||||
import * as credentialsJsonReader from '../credentialsJson/read';
|
||||
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
|
||||
import { AppLookupParams } from './api/GraphqlClient';
|
||||
import { Keystore } from './credentials';
|
||||
|
||||
export interface AndroidCredentials {
|
||||
keystore: Keystore;
|
||||
}
|
||||
|
||||
interface AppLookupParams {
|
||||
projectName: string;
|
||||
accountName: string;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
app: AppLookupParams;
|
||||
skipCredentialsCheck?: boolean;
|
||||
}
|
||||
|
||||
export default class AndroidCredentialsProvider {
|
||||
@@ -26,11 +22,6 @@ export default class AndroidCredentialsProvider {
|
||||
|
||||
constructor(private ctx: Context, private options: Options) {}
|
||||
|
||||
private get projectFullName(): string {
|
||||
const { projectName, accountName } = this.options.app;
|
||||
return `@${accountName}/${projectName}`;
|
||||
}
|
||||
|
||||
public async getCredentialsAsync(
|
||||
src: CredentialsSource.LOCAL | CredentialsSource.REMOTE
|
||||
): Promise<AndroidCredentials> {
|
||||
@@ -43,14 +34,22 @@ export default class AndroidCredentialsProvider {
|
||||
}
|
||||
|
||||
private async getRemoteAsync(): Promise<AndroidCredentials> {
|
||||
await new CredentialsManager(this.ctx).runActionAsync(
|
||||
new SetupBuildCredentials(this.projectFullName)
|
||||
);
|
||||
const keystore = await this.ctx.android.fetchKeystoreAsync(this.projectFullName);
|
||||
if (!keystore) {
|
||||
throw new Error('Unable to set up credentials, failed to fetch keystore from EAS servers');
|
||||
}
|
||||
return { keystore };
|
||||
const setupBuildCredentialsAction = await new SetupBuildCredentials({ app: this.options.app });
|
||||
const buildCredentials = await setupBuildCredentialsAction.runAsync(this.ctx);
|
||||
return this.toAndroidCredentials(buildCredentials);
|
||||
}
|
||||
|
||||
private toAndroidCredentials(
|
||||
androidBuildCredentials: AndroidAppBuildCredentialsFragment
|
||||
): AndroidCredentials {
|
||||
return {
|
||||
keystore: {
|
||||
keystore: nullthrows(androidBuildCredentials.androidKeystore?.keystore),
|
||||
keystorePassword: nullthrows(androidBuildCredentials.androidKeystore?.keystorePassword),
|
||||
keyAlias: nullthrows(androidBuildCredentials.androidKeystore?.keyAlias),
|
||||
keyPassword: nullthrows(androidBuildCredentials.androidKeystore?.keyPassword),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async getLocalAsync(): Promise<AndroidCredentials> {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
AndroidFcmFragment,
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
} from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Context } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
|
||||
export class AssignFcm {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
public async runAsync(
|
||||
ctx: Context,
|
||||
fcm: AndroidFcmFragment
|
||||
): Promise<CommonAndroidAppCredentialsFragment> {
|
||||
const appCredentials = await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
|
||||
this.app
|
||||
);
|
||||
const updatedAppCredentials = await ctx.android.updateAndroidAppCredentialsAsync(
|
||||
appCredentials,
|
||||
{
|
||||
androidFcmId: fcm.id,
|
||||
}
|
||||
);
|
||||
Log.succeed(`FCM API Key assigned to ${this.app.androidApplicationIdentifier}`);
|
||||
return updatedAppCredentials;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,91 @@
|
||||
import assert from 'assert';
|
||||
import { nanoid } from 'nanoid';
|
||||
import ora from 'ora';
|
||||
|
||||
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
|
||||
import { getApplicationId } from '../../../project/android/applicationId';
|
||||
import { getProjectAccountName, getProjectConfigDescription } from '../../../project/projectUtils';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { findAccountByName } from '../../../user/Account';
|
||||
import { Context } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
|
||||
/**
|
||||
* Legacy credentials can be copied over to EAS if the user does not have
|
||||
* EAS credentials set up yet
|
||||
*/
|
||||
export async function canCopyLegacyCredentialsAsync(
|
||||
ctx: Context,
|
||||
app: AppLookupParams
|
||||
): Promise<boolean> {
|
||||
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(app);
|
||||
if (appCredentials) {
|
||||
return false; // modern credentials already exist
|
||||
}
|
||||
|
||||
const legacyAppCredentials = await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(
|
||||
app
|
||||
);
|
||||
return !!legacyAppCredentials; // user has some legacy credentials
|
||||
}
|
||||
|
||||
export async function promptUserAndCopyLegacyCredentialsAsync(
|
||||
ctx: Context,
|
||||
app: AppLookupParams
|
||||
): Promise<void> {
|
||||
assert(
|
||||
await canCopyLegacyCredentialsAsync(ctx, app),
|
||||
'User not eligible to copy classic build credentials to EAS'
|
||||
);
|
||||
|
||||
const spinner = ora('Classic credentials detected, copying to EAS...').start();
|
||||
|
||||
try {
|
||||
const legacyAppCredentials = await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(
|
||||
app
|
||||
);
|
||||
if (!legacyAppCredentials) {
|
||||
return;
|
||||
}
|
||||
|
||||
const appCredentials = await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
|
||||
app
|
||||
);
|
||||
const legacyFcm = legacyAppCredentials.androidFcm;
|
||||
if (legacyFcm) {
|
||||
const clonedFcm = await ctx.android.createFcmAsync(
|
||||
app.account,
|
||||
legacyFcm.credential,
|
||||
legacyFcm.version
|
||||
);
|
||||
await ctx.android.updateAndroidAppCredentialsAsync(appCredentials, {
|
||||
androidFcmId: clonedFcm.id,
|
||||
});
|
||||
}
|
||||
|
||||
const legacyBuildCredentials = await ctx.android.getLegacyAndroidAppBuildCredentialsAsync(app);
|
||||
const legacyKeystore = legacyBuildCredentials?.androidKeystore ?? null;
|
||||
|
||||
if (legacyKeystore) {
|
||||
const clonedKeystore = await ctx.android.createKeystoreAsync(app.account, {
|
||||
keystore: legacyKeystore.keystore,
|
||||
keystorePassword: legacyKeystore.keystorePassword,
|
||||
keyAlias: legacyKeystore.keyAlias,
|
||||
keyPassword: legacyKeystore.keyPassword ?? undefined,
|
||||
type: legacyKeystore.type,
|
||||
});
|
||||
await createOrUpdateDefaultAndroidAppBuildCredentialsAsync(ctx, app, {
|
||||
androidKeystoreId: clonedKeystore.id,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
spinner.fail(`Unable to migrate credentials to EAS.`);
|
||||
throw e;
|
||||
}
|
||||
|
||||
spinner.succeed('Credentials copied to EAS.');
|
||||
}
|
||||
|
||||
export function getAppLookupParamsFromContext(ctx: Context): AppLookupParams {
|
||||
ctx.ensureProjectContext();
|
||||
const projectName = ctx.exp.slug;
|
||||
@@ -17,7 +95,7 @@ export function getAppLookupParamsFromContext(ctx: Context): AppLookupParams {
|
||||
throw new Error(`You do not have access to account: ${accountName}`);
|
||||
}
|
||||
|
||||
const androidApplicationIdentifier = ctx.exp.android?.package;
|
||||
const androidApplicationIdentifier = getApplicationId(ctx.projectDir, ctx.exp);
|
||||
if (!androidApplicationIdentifier) {
|
||||
throw new Error(
|
||||
`android.package needs to be defined in your ${getProjectConfigDescription(
|
||||
@@ -42,15 +120,23 @@ export async function createOrUpdateDefaultAndroidAppBuildCredentialsAsync(
|
||||
!ctx.nonInteractive,
|
||||
'createOrUpdateDefaultAndroidAppBuildCredentialsAsync must be run in interactive mode'
|
||||
);
|
||||
const existingDefaultBuildCredentials = await ctx.newAndroid.getDefaultAndroidAppBuildCredentialsAsync(
|
||||
const existingDefaultBuildCredentials = await ctx.android.getDefaultAndroidAppBuildCredentialsAsync(
|
||||
appLookupParams
|
||||
);
|
||||
if (existingDefaultBuildCredentials) {
|
||||
return await ctx.newAndroid.updateAndroidAppBuildCredentialsAsync(
|
||||
return await ctx.android.updateAndroidAppBuildCredentialsAsync(
|
||||
existingDefaultBuildCredentials,
|
||||
{ androidKeystoreId }
|
||||
);
|
||||
}
|
||||
return await ctx.android.createAndroidAppBuildCredentialsAsync(appLookupParams, {
|
||||
name: generateRandomName(),
|
||||
isDefault: true,
|
||||
androidKeystoreId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function promptForNameAsync(): Promise<string> {
|
||||
const { providedName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'providedName',
|
||||
@@ -58,10 +144,23 @@ export async function createOrUpdateDefaultAndroidAppBuildCredentialsAsync(
|
||||
initial: generateRandomName(),
|
||||
validate: (input: string) => input !== '',
|
||||
});
|
||||
return await ctx.newAndroid.createAndroidAppBuildCredentialsAsync(appLookupParams, {
|
||||
name: providedName,
|
||||
isDefault: true,
|
||||
androidKeystoreId,
|
||||
return providedName;
|
||||
}
|
||||
|
||||
/**
|
||||
* sort a build credentials array in descending order of preference
|
||||
* prefer default credentials, then prefer names that come first lexicographically
|
||||
*/
|
||||
export function sortBuildCredentials(
|
||||
androidAppBuildCredentialsList: AndroidAppBuildCredentialsFragment[]
|
||||
): AndroidAppBuildCredentialsFragment[] {
|
||||
return androidAppBuildCredentialsList.sort((buildCredentialsA, buildCredentialsB) => {
|
||||
if (buildCredentialsA.isDefault) {
|
||||
return -1;
|
||||
} else if (buildCredentialsB.isDefault) {
|
||||
return 1;
|
||||
}
|
||||
return buildCredentialsA.name.localeCompare(buildCredentialsB.name);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { AndroidFcmFragment, AndroidFcmVersion } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { Context } from '../../context';
|
||||
|
||||
export class CreateFcm {
|
||||
constructor(private account: Account) {}
|
||||
|
||||
public async runAsync(ctx: Context): Promise<AndroidFcmFragment> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(`FCM API Key cannot be uploaded in non-interactive mode.`);
|
||||
}
|
||||
const { fcmApiKey } = await promptAsync([
|
||||
{
|
||||
type: 'text',
|
||||
name: 'fcmApiKey',
|
||||
message: 'FCM API Key',
|
||||
validate: (value: string) => value.length > 0 || "FCM API Key can't be empty",
|
||||
},
|
||||
]);
|
||||
const fcmFragment = await ctx.android.createFcmAsync(
|
||||
this.account,
|
||||
fcmApiKey,
|
||||
AndroidFcmVersion.Legacy
|
||||
);
|
||||
Log.succeed('Uploaded FCM API Key');
|
||||
return fcmFragment;
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -1,11 +1,11 @@
|
||||
import { AndroidKeystoreFragment } from '../../../../graphql/generated';
|
||||
import Log from '../../../../log';
|
||||
import { Account } from '../../../../user/Account';
|
||||
import { Context } from '../../../context';
|
||||
import { askForUserProvidedAsync } from '../../../utils/promptForCredentials';
|
||||
import { KeystoreWithType, keystoreSchema } from '../../credentials';
|
||||
import { generateRandomKeystoreAsync } from '../../utils/keystore';
|
||||
import { getKeystoreWithType, validateKeystore } from '../../utils/keystoreNew';
|
||||
import { AndroidKeystoreFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { Context } from '../../context';
|
||||
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
|
||||
import { KeystoreWithType, keystoreSchema } from '../credentials';
|
||||
import { generateRandomKeystoreAsync } from '../utils/keystore';
|
||||
import { getKeystoreWithType, validateKeystore } from '../utils/keystoreNew';
|
||||
|
||||
export class CreateKeystore {
|
||||
constructor(private account: Account) {}
|
||||
@@ -15,7 +15,7 @@ export class CreateKeystore {
|
||||
throw new Error(`New keystore cannot be created in non-interactive mode.`);
|
||||
}
|
||||
const keystore = await this.provideOrGenerateAsync();
|
||||
const keystoreFragment = await ctx.newAndroid.createKeystoreAsync(this.account, keystore);
|
||||
const keystoreFragment = await ctx.android.createKeystoreAsync(this.account, keystore);
|
||||
Log.succeed('Created keystore');
|
||||
return keystoreFragment;
|
||||
}
|
||||
@@ -1,66 +1,82 @@
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import { maybeRenameExistingFileAsync } from '../../../utils/files';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
import { AppLookupParams, formatProjectFullName } from '../api/GraphqlClient';
|
||||
import { displayAndroidKeystore } from '../utils/printCredentials';
|
||||
|
||||
interface DownloadKeystoreOptions {
|
||||
displayCredentials?: boolean;
|
||||
app: AppLookupParams;
|
||||
displaySensitiveInformation?: boolean;
|
||||
outputPath?: string;
|
||||
}
|
||||
|
||||
export class DownloadKeystore implements Action {
|
||||
constructor(private projectFullName: string, private options?: DownloadKeystoreOptions) {}
|
||||
export class DownloadKeystore {
|
||||
constructor(private options: DownloadKeystoreOptions) {}
|
||||
|
||||
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
const keystore = await ctx.android.fetchKeystoreAsync(this.projectFullName);
|
||||
public async runAsync(
|
||||
ctx: Context,
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment
|
||||
): Promise<void> {
|
||||
const keystore = buildCredentials.androidKeystore;
|
||||
if (!keystore) {
|
||||
Log.warn('There is no valid Keystore defined for this app');
|
||||
Log.warn(
|
||||
`There is no valid Keystore defined for build credentials: ${buildCredentials.name}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let displayCredentials = this.options?.displayCredentials;
|
||||
if (displayCredentials === undefined && !ctx.nonInteractive) {
|
||||
displayCredentials = await confirmAsync({
|
||||
message: 'Do you want to display the Android Keystore credentials?',
|
||||
let displaySensitiveInformation = this.options?.displaySensitiveInformation;
|
||||
if (displaySensitiveInformation === undefined && !ctx.nonInteractive) {
|
||||
displaySensitiveInformation = await confirmAsync({
|
||||
message: 'Do you want to display the sensitive information of the Android Keystore?',
|
||||
});
|
||||
}
|
||||
|
||||
const keystorePath =
|
||||
this.options?.outputPath ?? `${this.projectFullName.replace('/', '__')}.jks`;
|
||||
const projectFullName = formatProjectFullName(this.options.app);
|
||||
const keystorePath = this.options?.outputPath ?? `${projectFullName.replace('/', '__')}.jks`;
|
||||
|
||||
await maybeRenameExistingFileAsync(ctx.projectDir, keystorePath);
|
||||
|
||||
Log.log(`Saving Keystore to ${keystorePath}`);
|
||||
await fs.writeFile(keystorePath, keystore.keystore, 'base64');
|
||||
|
||||
if (displayCredentials) {
|
||||
Log.log(`Keystore credentials
|
||||
Keystore password: ${chalk.bold(keystore.keystorePassword)}
|
||||
Key alias: ${chalk.bold(keystore.keyAlias)}
|
||||
Key password: ${chalk.bold(keystore.keyPassword)}
|
||||
// non-sensitive information
|
||||
displayAndroidKeystore(keystore);
|
||||
|
||||
Path to Keystore: ${keystorePath}
|
||||
if (displaySensitiveInformation) {
|
||||
Log.newLine();
|
||||
Log.log(`Sensitive Keystore information:
|
||||
Keystore password: ${chalk.bold(keystore.keystorePassword)}
|
||||
Key alias: ${chalk.bold(keystore.keyAlias)}
|
||||
Key password: ${chalk.bold(keystore.keyPassword)}
|
||||
|
||||
Path to Keystore: ${keystorePath}
|
||||
`);
|
||||
}
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
export class BackupKeystore implements Action {
|
||||
constructor(private projectFullName: string) {}
|
||||
export class BackupKeystore {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
if (await ctx.android.fetchKeystoreAsync(this.projectFullName)) {
|
||||
Log.warn('Backing up your old Android Keystore now...');
|
||||
await manager.runActionAsync(
|
||||
new DownloadKeystore(this.projectFullName, {
|
||||
displayCredentials: true,
|
||||
outputPath: `${this.projectFullName}.bak.jks`.replace('/', '__'),
|
||||
})
|
||||
);
|
||||
public async runAsync(
|
||||
ctx: Context,
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment
|
||||
): Promise<void> {
|
||||
if (!buildCredentials.androidKeystore) {
|
||||
return;
|
||||
}
|
||||
const projectFullName = formatProjectFullName(this.app);
|
||||
Log.warn('Backing up your old Android Keystore now...');
|
||||
await new DownloadKeystore({
|
||||
app: this.app,
|
||||
outputPath: `${projectFullName}.bak.jks`.replace('/', '__'),
|
||||
displaySensitiveInformation: true,
|
||||
}).runAsync(ctx, buildCredentials);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import Log from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
|
||||
export class UpdateFcmKey implements Action {
|
||||
constructor(private experienceName: string) {}
|
||||
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
const { fcmApiKey } = await promptAsync([
|
||||
{
|
||||
type: 'text',
|
||||
name: 'fcmApiKey',
|
||||
message: 'FCM API Key',
|
||||
validate: (value: string) => value.length > 0 || "FCM API Key can't be empty",
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.android.updateFcmKeyAsync(this.experienceName, fcmApiKey);
|
||||
Log.succeed('Updated successfully');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import { Context } from '../../context';
|
||||
import { AppLookupParams, formatProjectFullName } from '../api/GraphqlClient';
|
||||
|
||||
export class RemoveFcm {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
async runAsync(ctx: Context): Promise<void> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(
|
||||
"Deleting an FCM Api Key is a destructive operation. Start the CLI without the '--non-interactive' flag to delete the credentials."
|
||||
);
|
||||
}
|
||||
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(
|
||||
this.app
|
||||
);
|
||||
const fcm = appCredentials?.androidFcm;
|
||||
if (!fcm) {
|
||||
Log.warn(
|
||||
`There is no valid FCM Api Key defined for ${formatProjectFullName(this.app)}, ${
|
||||
this.app.androidApplicationIdentifier
|
||||
}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: 'Permanently delete the FCM Api Key from Expo servers?',
|
||||
initial: false,
|
||||
});
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.android.deleteFcmAsync(fcm);
|
||||
Log.succeed('FCM Api Key removed');
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,44 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { BackupKeystore } from './DownloadKeystore';
|
||||
|
||||
export class RemoveKeystore implements Action {
|
||||
constructor(private projectFullName: string) {}
|
||||
export class RemoveKeystore {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
if (!(await ctx.android.fetchKeystoreAsync(this.projectFullName))) {
|
||||
Log.warn('There is no Keystore defined for this app.');
|
||||
async runAsync(
|
||||
ctx: Context,
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment
|
||||
): Promise<void> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(
|
||||
"Deleting a keystore is a destructive operation. Start the CLI without the '--non-interactive' flag to delete the credentials."
|
||||
);
|
||||
}
|
||||
const keystore = buildCredentials.androidKeystore;
|
||||
if (!keystore) {
|
||||
Log.warn(
|
||||
`There is no valid Keystore defined for build credentials: ${buildCredentials.name}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.displayWarning();
|
||||
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(
|
||||
"Deleting build credentials is a destructive operation. Start the CLI without the '--non-interactive' flag to delete the credentials."
|
||||
);
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: 'Permanently delete the Android build credentials from our servers?',
|
||||
message: 'Permanently delete the Android Keystore?',
|
||||
initial: false,
|
||||
});
|
||||
if (confirm) {
|
||||
await manager.runActionAsync(new BackupKeystore(this.projectFullName));
|
||||
|
||||
await ctx.android.removeKeystoreAsync(this.projectFullName);
|
||||
Log.succeed('Keystore removed');
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
await new BackupKeystore(this.app).runAsync(ctx, buildCredentials);
|
||||
|
||||
await ctx.android.deleteKeystoreAsync(keystore);
|
||||
Log.succeed('Keystore removed');
|
||||
}
|
||||
|
||||
displayWarning() {
|
||||
|
||||
@@ -1,45 +1,136 @@
|
||||
import nullthrows from 'nullthrows';
|
||||
import ora from 'ora';
|
||||
|
||||
import {
|
||||
AndroidAppBuildCredentialsFragment,
|
||||
AndroidKeystoreFragment,
|
||||
} from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
import { readAndroidCredentialsAsync } from '../../credentialsJson/read';
|
||||
import { validateKeystoreAsync } from '../utils/keystore';
|
||||
import { UpdateKeystore } from './UpdateKeystore';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../errors';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import {
|
||||
canCopyLegacyCredentialsAsync,
|
||||
createOrUpdateDefaultAndroidAppBuildCredentialsAsync,
|
||||
promptUserAndCopyLegacyCredentialsAsync,
|
||||
} from './BuildCredentialsUtils';
|
||||
import { CreateKeystore } from './CreateKeystore';
|
||||
|
||||
export class SetupBuildCredentials implements Action {
|
||||
constructor(private projectFullName: string) {}
|
||||
interface Options {
|
||||
app: AppLookupParams;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
if (await ctx.android.fetchKeystoreAsync(this.projectFullName)) {
|
||||
return;
|
||||
/**
|
||||
* Sets up Build Credentials for Android
|
||||
* @name: sets up build credentials for the specified configuration. If no name is specified, the default configuration is setup
|
||||
*/
|
||||
export class SetupBuildCredentials {
|
||||
constructor(private options: Options) {}
|
||||
|
||||
async runAsync(ctx: Context): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
const { app, name: maybeName } = this.options;
|
||||
|
||||
if (!ctx.nonInteractive) {
|
||||
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, app);
|
||||
if (canCopyLegacyCredentials) {
|
||||
await promptUserAndCopyLegacyCredentialsAsync(ctx, app);
|
||||
}
|
||||
}
|
||||
|
||||
const alreadySetupBuildCredentials = await this.getFullySetupBuildCredentialsAsync({
|
||||
ctx,
|
||||
app,
|
||||
name: maybeName,
|
||||
});
|
||||
if (alreadySetupBuildCredentials) {
|
||||
return alreadySetupBuildCredentials;
|
||||
}
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error('Generating a new Keystore is not supported in --non-interactive mode');
|
||||
}
|
||||
|
||||
await manager.runActionAsync(new UpdateKeystore(this.projectFullName));
|
||||
}
|
||||
}
|
||||
|
||||
export class SetupBuildCredentialsFromCredentialsJson implements Action {
|
||||
constructor(
|
||||
private projectFullName: string,
|
||||
private options: { skipKeystoreValidation: boolean }
|
||||
) {}
|
||||
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
let localCredentials;
|
||||
try {
|
||||
localCredentials = await readAndroidCredentialsAsync(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 new MissingCredentialsNonInteractiveError(
|
||||
'Generating a new Keystore is not supported in --non-interactive mode'
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
if (!this.options.skipKeystoreValidation) {
|
||||
await validateKeystoreAsync(localCredentials.keystore);
|
||||
|
||||
const keystore = await new CreateKeystore(app.account).runAsync(ctx);
|
||||
return await this.assignBuildCredentialsAsync({ ctx, app, name: maybeName, keystore });
|
||||
}
|
||||
|
||||
async assignBuildCredentialsAsync({
|
||||
ctx,
|
||||
app,
|
||||
name,
|
||||
keystore,
|
||||
}: {
|
||||
ctx: Context;
|
||||
app: AppLookupParams;
|
||||
name?: string;
|
||||
keystore: AndroidKeystoreFragment;
|
||||
}): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
if (name) {
|
||||
return await ctx.android.createOrUpdateAndroidAppBuildCredentialsByNameAsync(app, name, {
|
||||
androidKeystoreId: keystore.id,
|
||||
});
|
||||
}
|
||||
await ctx.android.updateKeystoreAsync(this.projectFullName, localCredentials.keystore);
|
||||
Log.succeed('Keystore updated');
|
||||
return await createOrUpdateDefaultAndroidAppBuildCredentialsAsync(ctx, app, {
|
||||
androidKeystoreId: keystore.id,
|
||||
});
|
||||
}
|
||||
|
||||
async getFullySetupBuildCredentialsAsync({
|
||||
ctx,
|
||||
app,
|
||||
name,
|
||||
}: {
|
||||
ctx: Context;
|
||||
app: AppLookupParams;
|
||||
name?: string;
|
||||
}): Promise<AndroidAppBuildCredentialsFragment | null> {
|
||||
if (name) {
|
||||
return await this.getFullySetupBuildCredentialsByNameAsync({ ctx, app, name });
|
||||
}
|
||||
|
||||
const defaultBuildCredentials = await ctx.android.getDefaultAndroidAppBuildCredentialsAsync(
|
||||
app
|
||||
);
|
||||
const defaultKeystore = defaultBuildCredentials?.androidKeystore ?? null;
|
||||
if (defaultKeystore) {
|
||||
ora(
|
||||
`Using Keystore from configuration: ${nullthrows(defaultBuildCredentials).name} (default)`
|
||||
).succeed();
|
||||
return defaultBuildCredentials;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getFullySetupBuildCredentialsByNameAsync({
|
||||
ctx,
|
||||
app,
|
||||
name,
|
||||
}: {
|
||||
ctx: Context;
|
||||
app: AppLookupParams;
|
||||
name: string;
|
||||
}): Promise<AndroidAppBuildCredentialsFragment | null> {
|
||||
const maybeBuildCredentials = await ctx.android.getAndroidAppBuildCredentialsByNameAsync(
|
||||
app,
|
||||
name
|
||||
);
|
||||
const keystore = maybeBuildCredentials?.androidKeystore ?? null;
|
||||
if (keystore) {
|
||||
const buildCredentials = nullthrows(maybeBuildCredentials);
|
||||
ora(
|
||||
`Using Keystore from configuration: ${buildCredentials.name}${
|
||||
buildCredentials.isDefault ? ' (default)' : ''
|
||||
}`
|
||||
).succeed();
|
||||
return buildCredentials;
|
||||
}
|
||||
Log.log(
|
||||
`No Keystore found for configuration: ${name}${
|
||||
maybeBuildCredentials?.isDefault ? ' (default)' : ''
|
||||
}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
AndroidAppBuildCredentialsFragment,
|
||||
AndroidKeystoreType,
|
||||
} from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import { Context } from '../../context';
|
||||
import { readAndroidCredentialsAsync } from '../../credentialsJson/read';
|
||||
import {
|
||||
SelectAndroidBuildCredentials,
|
||||
SelectAndroidBuildCredentialsResultType,
|
||||
} from '../../manager/SelectAndroidBuildCredentials';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { getKeystoreWithType } from '../utils/keystoreNew';
|
||||
import { BackupKeystore } from './DownloadKeystore';
|
||||
|
||||
export class SetupBuildCredentialsFromCredentialsJson {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
async runAsync(ctx: Context): Promise<AndroidAppBuildCredentialsFragment | null> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(
|
||||
'Setting up build credentials from credentials.json is only available in interactive mode'
|
||||
);
|
||||
}
|
||||
|
||||
let localCredentials;
|
||||
try {
|
||||
localCredentials = await readAndroidCredentialsAsync(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;
|
||||
}
|
||||
|
||||
const selectBuildCredentialsResult = await new SelectAndroidBuildCredentials(this.app).runAsync(
|
||||
ctx
|
||||
);
|
||||
if (
|
||||
selectBuildCredentialsResult.resultType ===
|
||||
SelectAndroidBuildCredentialsResultType.EXISTING_CREDENTIALS &&
|
||||
selectBuildCredentialsResult.result.androidKeystore
|
||||
) {
|
||||
const buildCredentials = selectBuildCredentialsResult.result;
|
||||
const confirmOverwrite = await confirmAsync({
|
||||
message: `The build configuration ${buildCredentials.name} already has a keystore configured. Overwrite?`,
|
||||
});
|
||||
if (!confirmOverwrite) {
|
||||
return null;
|
||||
}
|
||||
await new BackupKeystore(this.app).runAsync(ctx, buildCredentials);
|
||||
}
|
||||
|
||||
const providedKeystoreWithType = getKeystoreWithType(localCredentials.keystore);
|
||||
if (providedKeystoreWithType.type === AndroidKeystoreType.Unknown) {
|
||||
const confirmKeystoreIsSketchy = await confirmAsync({
|
||||
message: `The keystore you provided could not be parsed and may be corrupt. Proceed anyway?`,
|
||||
});
|
||||
if (!confirmKeystoreIsSketchy) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const keystoreFragment = await ctx.android.createKeystoreAsync(
|
||||
this.app.account,
|
||||
providedKeystoreWithType
|
||||
);
|
||||
let buildCredentials: AndroidAppBuildCredentialsFragment;
|
||||
if (
|
||||
selectBuildCredentialsResult.resultType ===
|
||||
SelectAndroidBuildCredentialsResultType.CREATE_REQUEST
|
||||
) {
|
||||
buildCredentials = await ctx.android.createAndroidAppBuildCredentialsAsync(this.app, {
|
||||
...selectBuildCredentialsResult.result,
|
||||
androidKeystoreId: keystoreFragment.id,
|
||||
});
|
||||
} else {
|
||||
buildCredentials = await ctx.android.updateAndroidAppBuildCredentialsAsync(
|
||||
selectBuildCredentialsResult.result,
|
||||
{
|
||||
androidKeystoreId: keystoreFragment.id,
|
||||
}
|
||||
);
|
||||
}
|
||||
Log.succeed('Keystore updated');
|
||||
return buildCredentials;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
import { updateAndroidCredentialsAsync } from '../../credentialsJson/update';
|
||||
|
||||
export class UpdateCredentialsJson implements Action {
|
||||
constructor(private projectFullName: string) {}
|
||||
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
export class UpdateCredentialsJson {
|
||||
async runAsync(
|
||||
ctx: Context,
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment
|
||||
): Promise<void> {
|
||||
Log.log('Updating Android credentials in credentials.json');
|
||||
await updateAndroidCredentialsAsync(ctx);
|
||||
await updateAndroidCredentialsAsync(ctx, buildCredentials);
|
||||
Log.succeed(
|
||||
'Android part of your local credentials.json is synced with values stored on EAS servers.'
|
||||
);
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../../log';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
|
||||
import { Keystore, keystoreSchema } from '../credentials';
|
||||
import { generateRandomKeystoreAsync } from '../utils/keystore';
|
||||
|
||||
export class UpdateKeystore implements Action {
|
||||
constructor(private projectFullName: string) {}
|
||||
|
||||
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
if (await ctx.android.fetchKeystoreAsync(this.projectFullName)) {
|
||||
this.displayWarning();
|
||||
}
|
||||
const keystore = await this.provideOrGenerateAsync();
|
||||
|
||||
await ctx.android.updateKeystoreAsync(this.projectFullName, keystore);
|
||||
Log.succeed('Keystore updated');
|
||||
}
|
||||
|
||||
private async provideOrGenerateAsync(): Promise<Keystore> {
|
||||
const providedKeystore = await askForUserProvidedAsync(keystoreSchema);
|
||||
if (providedKeystore) {
|
||||
return providedKeystore;
|
||||
}
|
||||
|
||||
return await generateRandomKeystoreAsync();
|
||||
}
|
||||
|
||||
private displayWarning() {
|
||||
Log.newLine();
|
||||
Log.warn(
|
||||
`Updating your Android build credentials will remove previous version from our servers, this is a ${chalk.bold(
|
||||
'PERMANENT and IRREVERSIBLE action.'
|
||||
)}`
|
||||
);
|
||||
Log.warn(
|
||||
chalk.bold(
|
||||
'Android Keystore must be identical to the one previously used to submit your app to the Google Play Store.'
|
||||
)
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { testLegacyAndroidFcmFragment } from '../../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { AssignFcm } from '../AssignFcm';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
|
||||
describe(AssignFcm, () => {
|
||||
it('assigns an fcm api key in Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const assignFcmAction = new AssignFcm(appLookupParams);
|
||||
await assignFcmAction.runAsync(ctx, testLegacyAndroidFcmFragment);
|
||||
|
||||
// expect app credentials to be fetched/created, then updated
|
||||
expect(
|
||||
ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync as any
|
||||
).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateAndroidAppCredentialsAsync as any).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('works in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const assignFcmAction = new AssignFcm(appLookupParams);
|
||||
|
||||
// dont fail if users are running in non-interactive mode
|
||||
await expect(
|
||||
assignFcmAction.runAsync(ctx, testLegacyAndroidFcmFragment)
|
||||
).resolves.not.toThrowError();
|
||||
});
|
||||
});
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import { confirmAsync, promptAsync } from '../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
testAndroidAppCredentialsFragment,
|
||||
testJksAndroidKeystoreFragment,
|
||||
testLegacyAndroidAppCredentialsFragment,
|
||||
testLegacyAndroidBuildCredentialsFragment,
|
||||
testLegacyAndroidFcmFragment,
|
||||
} from '../../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import {
|
||||
canCopyLegacyCredentialsAsync,
|
||||
getAppLookupParamsFromContext,
|
||||
promptUserAndCopyLegacyCredentialsAsync,
|
||||
} from '../BuildCredentialsUtils';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
|
||||
describe('BuildCredentialsUtils', () => {
|
||||
describe(canCopyLegacyCredentialsAsync, () => {
|
||||
it('returns true if the user has legacy credentials and no modern ones', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
android: {
|
||||
...getNewAndroidApiMock(),
|
||||
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(() => null),
|
||||
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testLegacyAndroidAppCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
|
||||
expect(canCopyLegacyCredentials).toBe(true);
|
||||
});
|
||||
it('returns false if the user has modern credentials', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
android: {
|
||||
...getNewAndroidApiMock(),
|
||||
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testAndroidAppCredentialsFragment
|
||||
),
|
||||
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testLegacyAndroidAppCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
|
||||
expect(canCopyLegacyCredentials).toBe(false);
|
||||
});
|
||||
it('returns false if the user has no legacy credentials', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
android: {
|
||||
...getNewAndroidApiMock(),
|
||||
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(() => null),
|
||||
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(() => null),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
|
||||
expect(canCopyLegacyCredentials).toBe(false);
|
||||
});
|
||||
it('works in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
android: {
|
||||
...getNewAndroidApiMock(),
|
||||
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(() => null),
|
||||
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testLegacyAndroidAppCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
|
||||
expect(canCopyLegacyCredentials).toBe(true);
|
||||
});
|
||||
});
|
||||
describe(promptUserAndCopyLegacyCredentialsAsync, () => {
|
||||
it('copies all legacy credentials to EAS if the user is eligible', async () => {
|
||||
(promptAsync as jest.Mock).mockImplementation(() => ({ providedName: 'test-provided-name' }));
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
android: {
|
||||
...getNewAndroidApiMock(),
|
||||
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(() => null),
|
||||
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testLegacyAndroidAppCredentialsFragment
|
||||
),
|
||||
getLegacyAndroidAppBuildCredentialsAsync: jest.fn(
|
||||
() => testLegacyAndroidBuildCredentialsFragment
|
||||
),
|
||||
createKeystoreAsync: jest.fn(() => testJksAndroidKeystoreFragment),
|
||||
createFcmAsync: jest.fn(() => testLegacyAndroidFcmFragment),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
await promptUserAndCopyLegacyCredentialsAsync(ctx, appLookupParams);
|
||||
|
||||
expect(
|
||||
ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync as any
|
||||
).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createFcmAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateAndroidAppCredentialsAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createAndroidAppBuildCredentialsAsync as any).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
android: {
|
||||
...getNewAndroidApiMock(),
|
||||
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(() => null),
|
||||
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testLegacyAndroidAppCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
await expect(
|
||||
promptUserAndCopyLegacyCredentialsAsync(ctx, appLookupParams)
|
||||
).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { promptAsync } from '../../../../prompts';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { CreateFcm } from '../CreateFcm';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
(promptAsync as jest.Mock).mockImplementation(() => ({ fcmApiKey: 'blah' }));
|
||||
|
||||
describe(CreateFcm, () => {
|
||||
it('creates an fcm api key in Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: false });
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const createFcmAction = new CreateFcm(appLookupParams.account);
|
||||
await createFcmAction.runAsync(ctx);
|
||||
|
||||
// expect fcm api key to be created on expo servers
|
||||
expect(ctx.android.createFcmAsync as any).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: true });
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const createFcmAction = new CreateFcm(appLookupParams.account);
|
||||
|
||||
// fail if users are running in non-interactive mode
|
||||
await expect(createFcmAction.runAsync(ctx)).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
import { confirmAsync } from '../../../../../prompts';
|
||||
import { createCtxMock } from '../../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContext } from '../../BuildCredentialsUtils';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { CreateKeystore } from '../CreateKeystore';
|
||||
|
||||
jest.mock('../../../../../prompts');
|
||||
jest.mock('../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
|
||||
describe('CreateKeystore', () => {
|
||||
@@ -14,7 +14,7 @@ describe('CreateKeystore', () => {
|
||||
await createKeystoreAction.runAsync(ctx);
|
||||
|
||||
// expect keystore to be created on expo servers
|
||||
expect(ctx.newAndroid.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: true });
|
||||
@@ -0,0 +1,43 @@
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { DownloadKeystore } from '../DownloadKeystore';
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
describe(DownloadKeystore, () => {
|
||||
it('downloads a keystore', async () => {
|
||||
const fsWriteFileSpy = jest.spyOn(fs, 'writeFile');
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const downloadKeystoreAction = new DownloadKeystore({ app: appLookupParams });
|
||||
await downloadKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment);
|
||||
expect(fsWriteFileSpy as any).toHaveBeenCalledTimes(1);
|
||||
expect(fsWriteFileSpy as any).toHaveBeenCalledWith(
|
||||
`@${appLookupParams.account.name}__${appLookupParams.projectName}.jks`,
|
||||
testAndroidBuildCredentialsFragment.androidKeystore?.keystore,
|
||||
'base64'
|
||||
);
|
||||
fsWriteFileSpy.mockRestore();
|
||||
});
|
||||
it('works in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: true });
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const downloadKeystoreAction = new DownloadKeystore({ app: appLookupParams });
|
||||
await expect(
|
||||
downloadKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment)
|
||||
).resolves.not.toThrowError();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
testAndroidAppCredentialsFragment,
|
||||
} from '../../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { RemoveFcm } from '../RemoveFcm';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
describe(RemoveFcm, () => {
|
||||
it('removes an FCM Api Key', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
android: {
|
||||
...getNewAndroidApiMock(),
|
||||
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testAndroidAppCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const removeFcmApiKeyAction = new RemoveFcm(appLookupParams);
|
||||
await removeFcmApiKeyAction.runAsync(ctx);
|
||||
expect(ctx.android.deleteFcmAsync as any).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: true });
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const removeFcmApiKeyAction = new RemoveFcm(appLookupParams);
|
||||
await expect(removeFcmApiKeyAction.runAsync(ctx)).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
+25
-109
@@ -1,122 +1,38 @@
|
||||
import prompts from 'prompts';
|
||||
|
||||
import { Action } from '../../../CredentialsManager';
|
||||
import { testKeystore } from '../../../__tests__/fixtures-android';
|
||||
import { testExperienceName } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock, createManagerMock } from '../../../__tests__/fixtures-context';
|
||||
import { BackupKeystore } from '../DownloadKeystore';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { RemoveKeystore } from '../RemoveKeystore';
|
||||
|
||||
jest.mock('prompts');
|
||||
jest.mock('../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
|
||||
const originalWarn = console.warn;
|
||||
const originalLog = console.log;
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
beforeAll(() => {
|
||||
console.warn = jest.fn();
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.warn = originalWarn;
|
||||
console.log = originalLog;
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
beforeEach(() => {
|
||||
(prompts as any).mockReset();
|
||||
(prompts as any).mockImplementation(() => {
|
||||
throw new Error('Should not be called');
|
||||
});
|
||||
});
|
||||
|
||||
describe('run RemoveKeystore when keystore does exist', () => {
|
||||
it('should display warning prompt and abort', async () => {
|
||||
describe(RemoveKeystore, () => {
|
||||
it('removes a keystore', async () => {
|
||||
const ctx = createCtxMock({
|
||||
android: {
|
||||
fetchKeystoreAsync: jest.fn(() => testKeystore),
|
||||
},
|
||||
nonInteractive: false,
|
||||
});
|
||||
const manager = createManagerMock();
|
||||
(prompts as any).mockImplementationOnce(() => ({ value: false })); // prompt with warning message, false means abort
|
||||
|
||||
await new RemoveKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.removeKeystoreAsync).not.toHaveBeenCalled();
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const removeKeystoreAction = new RemoveKeystore(appLookupParams);
|
||||
await removeKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment);
|
||||
expect(ctx.android.deleteKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should display warning prompt and continue', async () => {
|
||||
const ctx = createCtxMock({
|
||||
android: {
|
||||
fetchKeystoreAsync: jest.fn(() => testKeystore),
|
||||
},
|
||||
});
|
||||
const manager = createManagerMock();
|
||||
|
||||
(manager.runActionAsync as any).mockImplementationOnce((action: Action) => {
|
||||
expect(action).toBeInstanceOf(BackupKeystore);
|
||||
});
|
||||
(prompts as any).mockImplementationOnce(() => ({ value: true })); // prompt with warning message, true means continue
|
||||
|
||||
await new RemoveKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.runActionAsync).toHaveBeenCalledTimes(1);
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.removeKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not display a prompt in non-interactive mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
android: {
|
||||
fetchKeystoreAsync: jest.fn(() => testKeystore),
|
||||
},
|
||||
});
|
||||
const manager = createManagerMock();
|
||||
|
||||
try {
|
||||
await new RemoveKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
throw new Error('make sure expect in catch is called');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch('Deleting build credentials is a destructive operation');
|
||||
}
|
||||
|
||||
expect(prompts).not.toHaveBeenCalled();
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.removeKeystoreAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('run RemoveKeystore when keystore does not exist', () => {
|
||||
it("shouldn't display warning prompt and finish", async () => {
|
||||
const ctx = createCtxMock({
|
||||
android: {
|
||||
fetchKeystoreAsync: jest.fn().mockImplementationOnce(() => null),
|
||||
},
|
||||
});
|
||||
const manager = createManagerMock();
|
||||
|
||||
await new RemoveKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(prompts).not.toHaveBeenCalled();
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.removeKeystoreAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
it("shouldn't display warning prompt and finish in non-interactive mode", async () => {
|
||||
const ctx = createCtxMock({
|
||||
android: {
|
||||
fetchKeystoreAsync: jest.fn().mockImplementationOnce(() => null),
|
||||
},
|
||||
});
|
||||
const manager = createManagerMock();
|
||||
|
||||
await new RemoveKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(prompts).not.toHaveBeenCalled();
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.removeKeystoreAsync).not.toHaveBeenCalled();
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: true });
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const removeKeystoreAction = new RemoveKeystore(appLookupParams);
|
||||
await expect(
|
||||
removeKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment)
|
||||
).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
+52
-78
@@ -1,100 +1,74 @@
|
||||
import prompts from 'prompts';
|
||||
|
||||
import { Action } from '../../../CredentialsManager';
|
||||
import { testKeystore } from '../../../__tests__/fixtures-android';
|
||||
import { testExperienceName } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock, createManagerMock } from '../../../__tests__/fixtures-context';
|
||||
import { confirmAsync, promptAsync } from '../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
testAndroidBuildCredentialsFragment,
|
||||
testJksAndroidKeystoreFragment,
|
||||
} from '../../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../../errors';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { SetupBuildCredentials } from '../SetupBuildCredentials';
|
||||
import { UpdateKeystore } from '../UpdateKeystore';
|
||||
|
||||
jest.mock('prompts');
|
||||
jest.mock('../../utils/keystore');
|
||||
jest.mock('../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
|
||||
const originalWarn = console.warn;
|
||||
const originalLog = console.log;
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
beforeAll(() => {
|
||||
// console.warn = jest.fn();
|
||||
// console.log = jest.fn();
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.warn = originalWarn;
|
||||
console.log = originalLog;
|
||||
});
|
||||
beforeEach(() => {
|
||||
(prompts as any).mockReset();
|
||||
(prompts as any).mockImplementation(() => {
|
||||
throw new Error('Should not be called');
|
||||
});
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
|
||||
describe('run SetupBuildCredentials when www has valid credentials', () => {
|
||||
it('should fetch credentials and exit', async () => {
|
||||
describe('SetupBuildCredentials', () => {
|
||||
it('skips setup when there are prior credentials', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
android: {
|
||||
fetchKeystoreAsync: jest.fn(() => testKeystore),
|
||||
...getNewAndroidApiMock(),
|
||||
getDefaultAndroidAppBuildCredentialsAsync: jest.fn(
|
||||
() => testAndroidBuildCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
const manager = createManagerMock();
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
|
||||
await new SetupBuildCredentials(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateKeystoreAsync).not.toHaveBeenCalled();
|
||||
// expect keystore not to be created
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it('sets up credentials when there are no prior credentials', async () => {
|
||||
(promptAsync as jest.Mock).mockImplementation(() => ({ providedName: 'test-provided-name' }));
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
android: {
|
||||
...getNewAndroidApiMock(),
|
||||
createKeystoreAsync: jest.fn(() => testJksAndroidKeystoreFragment),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
|
||||
it('should fetch credentials and exit in non-interactive mode', async () => {
|
||||
// expect keystore to be created
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
(promptAsync as jest.Mock).mockImplementation(() => ({ providedName: 'test-provided-name' }));
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
android: {
|
||||
fetchKeystoreAsync: jest.fn(() => testKeystore),
|
||||
...getNewAndroidApiMock(),
|
||||
},
|
||||
});
|
||||
const manager = createManagerMock();
|
||||
|
||||
await new SetupBuildCredentials(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateKeystoreAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('run SetupBuildCredentials when www has no credentials', () => {
|
||||
it('should try to fetch and launch UpdateKeystore', async () => {
|
||||
const ctx = createCtxMock({
|
||||
android: {
|
||||
fetchKeystoreAsync: jest.fn(() => null),
|
||||
},
|
||||
});
|
||||
const manager = createManagerMock();
|
||||
|
||||
(manager.runActionAsync as any).mockImplementationOnce((action: Action) => {
|
||||
expect(action).toBeInstanceOf(UpdateKeystore);
|
||||
});
|
||||
await new SetupBuildCredentials(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.runActionAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateKeystoreAsync).toHaveBeenCalledTimes(0); // will be called in UpdateKeystore
|
||||
});
|
||||
|
||||
it('should fail if credentials are missing in non-interactive mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
android: {
|
||||
fetchKeystore: jest.fn(() => null),
|
||||
},
|
||||
});
|
||||
const manager = createManagerMock();
|
||||
|
||||
try {
|
||||
await new SetupBuildCredentials(testExperienceName).runAsync(manager, ctx);
|
||||
throw new Error('SetupBuildCredentials.runAsync should throw an error');
|
||||
} catch (error) {
|
||||
expect(error.message).toMatch('Generating a new Keystore is not supported');
|
||||
}
|
||||
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
|
||||
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError(
|
||||
MissingCredentialsNonInteractiveError
|
||||
);
|
||||
});
|
||||
});
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { AndroidKeystoreType } from '../../../../graphql/generated';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
testAndroidBuildCredentialsFragment,
|
||||
testJksAndroidKeystoreFragment,
|
||||
} from '../../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import {
|
||||
SelectAndroidBuildCredentials,
|
||||
SelectAndroidBuildCredentialsResultType,
|
||||
} from '../../../manager/SelectAndroidBuildCredentials';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { SetupBuildCredentialsFromCredentialsJson } from '../SetupBuildCredentialsFromCredentialsJson';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
jest.mock('../../../manager/SelectAndroidBuildCredentials');
|
||||
|
||||
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(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
describe(SetupBuildCredentialsFromCredentialsJson, () => {
|
||||
it('sets up a new build configuration from credentials.json upon user request', async () => {
|
||||
(SelectAndroidBuildCredentials as any).mockImplementation(() => {
|
||||
return {
|
||||
runAsync: () => {
|
||||
return {
|
||||
resultType: SelectAndroidBuildCredentialsResultType.CREATE_REQUEST,
|
||||
result: { isDefault: true, name: 'test configuration' },
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
android: {
|
||||
...getNewAndroidApiMock(),
|
||||
createKeystoreAsync: jest.fn(() => testJksAndroidKeystoreFragment),
|
||||
},
|
||||
});
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
android: {
|
||||
keystore: {
|
||||
keystorePath: 'keystore.jks',
|
||||
keystorePassword: testJksAndroidKeystoreFragment.keystorePassword,
|
||||
keyAlias: testJksAndroidKeystoreFragment.keyAlias,
|
||||
keyPassword: testJksAndroidKeystoreFragment.keyPassword,
|
||||
},
|
||||
},
|
||||
}),
|
||||
'keystore.jks': 'some-binary-content',
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams
|
||||
);
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
|
||||
// expect keystore to be created with credentials.json content
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createKeystoreAsync as any).toBeCalledWith(appLookupParams.account, {
|
||||
keystorePassword: testJksAndroidKeystoreFragment.keystorePassword,
|
||||
keyAlias: testJksAndroidKeystoreFragment.keyAlias,
|
||||
keyPassword: testJksAndroidKeystoreFragment.keyPassword,
|
||||
keystore: Buffer.from('some-binary-content').toString('base64'),
|
||||
type: AndroidKeystoreType.Unknown,
|
||||
});
|
||||
|
||||
// expect new build credentials to be created
|
||||
expect(ctx.android.createAndroidAppBuildCredentialsAsync as any).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('uses an existing build configuration upon user request', async () => {
|
||||
(SelectAndroidBuildCredentials as any).mockImplementation(() => {
|
||||
return {
|
||||
runAsync: () => {
|
||||
return {
|
||||
resultType: SelectAndroidBuildCredentialsResultType.EXISTING_CREDENTIALS,
|
||||
result: testAndroidBuildCredentialsFragment,
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
android: {
|
||||
...getNewAndroidApiMock(),
|
||||
createKeystoreAsync: jest.fn(() => testJksAndroidKeystoreFragment),
|
||||
},
|
||||
});
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
android: {
|
||||
keystore: {
|
||||
keystorePath: 'keystore.jks',
|
||||
keystorePassword: testJksAndroidKeystoreFragment.keystorePassword,
|
||||
keyAlias: testJksAndroidKeystoreFragment.keyAlias,
|
||||
keyPassword: testJksAndroidKeystoreFragment.keyPassword,
|
||||
},
|
||||
},
|
||||
}),
|
||||
'keystore.jks': 'some-binary-content',
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams
|
||||
);
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
|
||||
// expect keystore to be created with credentials.json content
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createKeystoreAsync as any).toBeCalledWith(appLookupParams.account, {
|
||||
keystorePassword: testJksAndroidKeystoreFragment.keystorePassword,
|
||||
keyAlias: testJksAndroidKeystoreFragment.keyAlias,
|
||||
keyPassword: testJksAndroidKeystoreFragment.keyPassword,
|
||||
keystore: Buffer.from('some-binary-content').toString('base64'),
|
||||
type: AndroidKeystoreType.Unknown,
|
||||
});
|
||||
|
||||
// expect existing build credentials to be updated
|
||||
expect(ctx.android.updateAndroidAppBuildCredentialsAsync as any).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams
|
||||
);
|
||||
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
@@ -1,118 +0,0 @@
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import prompts from 'prompts';
|
||||
|
||||
import { testKeystore } from '../../../__tests__/fixtures-android';
|
||||
import { testExperienceName } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock, createManagerMock } from '../../../__tests__/fixtures-context';
|
||||
import { UpdateKeystore } from '../UpdateKeystore';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('prompts');
|
||||
jest.mock('../../utils/keystore');
|
||||
|
||||
const originalWarn = console.warn;
|
||||
const originalLog = console.log;
|
||||
beforeAll(() => {
|
||||
console.warn = jest.fn();
|
||||
console.log = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.warn = originalWarn;
|
||||
console.log = originalLog;
|
||||
});
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
(prompts as any).mockReset();
|
||||
(prompts as any).mockImplementation(() => {
|
||||
throw new Error('Should not be called');
|
||||
});
|
||||
});
|
||||
|
||||
describe('run UpdateKeystore when keystore exist on www', () => {
|
||||
it('should fetch old credentials and generate new ones', async () => {
|
||||
const ctx = createCtxMock({
|
||||
android: {
|
||||
fetchKeystoreAsync: jest.fn(() => testKeystore),
|
||||
},
|
||||
});
|
||||
const manager = createManagerMock();
|
||||
|
||||
(prompts as any).mockImplementationOnce(() => ({ value: true })); // promptForCredentials: let expo handle
|
||||
|
||||
await new UpdateKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should fetch old credentials and ask users to provide new ones', async () => {
|
||||
const ctx = createCtxMock({
|
||||
android: {
|
||||
fetchKeystoreAsync: jest.fn(() => testKeystore),
|
||||
},
|
||||
});
|
||||
const manager = createManagerMock();
|
||||
|
||||
await fs.writeFile('/keystore.jks', testKeystore.keystore, 'base64');
|
||||
(prompts as any)
|
||||
.mockImplementationOnce(() => ({ value: false })) // promptForCredentials: user specified
|
||||
.mockImplementationOnce(() => ({ input: '/keystore.jks' })) // keystore
|
||||
.mockImplementationOnce(() => ({ input: 'test' })) // keystore password
|
||||
.mockImplementationOnce(() => ({ input: 'test' })) // key alias
|
||||
.mockImplementationOnce(() => ({ input: 'test' })); // key password
|
||||
|
||||
await new UpdateKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(prompts).toHaveBeenCalledTimes(5);
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('run UpdateKeystore when keystore does not exist on www', () => {
|
||||
it('should fail to fetch credentials and generate new ones', async () => {
|
||||
const ctx = createCtxMock({
|
||||
android: {
|
||||
fetchKeystore: jest.fn(() => null),
|
||||
},
|
||||
});
|
||||
const manager = createManagerMock();
|
||||
|
||||
(prompts as any).mockImplementationOnce(() => ({ value: true })); // Let expo handle
|
||||
|
||||
await new UpdateKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should fail to fetch credentials and ask users to provide new ones', async () => {
|
||||
const ctx = createCtxMock({
|
||||
android: {
|
||||
fetchKeystore: jest.fn(() => null),
|
||||
},
|
||||
});
|
||||
const manager = createManagerMock();
|
||||
|
||||
await fs.writeFile('/keystore.jks', testKeystore.keystore, 'base64');
|
||||
(prompts as any)
|
||||
.mockImplementationOnce(() => ({ value: false })) // promptForCredentials: user specified
|
||||
.mockImplementationOnce(() => ({ input: '/keystore.jks' })) // keystore
|
||||
.mockImplementationOnce(() => ({ input: 'test' })) // keystore password
|
||||
.mockImplementationOnce(() => ({ input: 'test' })) // key alias
|
||||
.mockImplementationOnce(() => ({ input: 'test' })); // key password
|
||||
|
||||
await new UpdateKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(prompts).toHaveBeenCalledTimes(5);
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,133 +0,0 @@
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import {
|
||||
AndroidAppBuildCredentialsFragment,
|
||||
AndroidKeystoreFragment,
|
||||
} from '../../../../graphql/generated';
|
||||
import Log from '../../../../log';
|
||||
import { Context } from '../../../context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../../errors';
|
||||
import { AppLookupParams } from '../../api/GraphqlClient';
|
||||
import { createOrUpdateDefaultAndroidAppBuildCredentialsAsync } from '../BuildCredentialsUtils';
|
||||
import { CreateKeystore } from './CreateKeystore';
|
||||
|
||||
interface Options {
|
||||
app: AppLookupParams;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up Build Credentials for Android
|
||||
* @name: sets up build credentials for the specified configuration. If no name is specified, the default configuration is setup
|
||||
*/
|
||||
export class SetupBuildCredentials {
|
||||
constructor(private options: Options) {}
|
||||
|
||||
async runAsync(ctx: Context): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
const { app, name: maybeName } = this.options;
|
||||
const alreadySetupBuildCredentials = await this.getFullySetupBuildCredentialsAsync({
|
||||
ctx,
|
||||
app,
|
||||
name: maybeName,
|
||||
});
|
||||
if (alreadySetupBuildCredentials) {
|
||||
return alreadySetupBuildCredentials;
|
||||
}
|
||||
if (ctx.nonInteractive) {
|
||||
throw new MissingCredentialsNonInteractiveError(
|
||||
'Generating a new Keystore is not supported in --non-interactive mode'
|
||||
);
|
||||
}
|
||||
|
||||
const keystore = await new CreateKeystore(app.account).runAsync(ctx);
|
||||
return await this.assignBuildCredentialsAsync({ ctx, app, name: maybeName, keystore });
|
||||
}
|
||||
|
||||
async assignBuildCredentialsAsync({
|
||||
ctx,
|
||||
app,
|
||||
name,
|
||||
keystore,
|
||||
}: {
|
||||
ctx: Context;
|
||||
app: AppLookupParams;
|
||||
name?: string;
|
||||
keystore: AndroidKeystoreFragment;
|
||||
}): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
if (name) {
|
||||
return await ctx.newAndroid.createOrUpdateAndroidAppBuildCredentialsByNameAsync(app, name, {
|
||||
androidKeystoreId: keystore.id,
|
||||
});
|
||||
}
|
||||
return await createOrUpdateDefaultAndroidAppBuildCredentialsAsync(ctx, app, {
|
||||
androidKeystoreId: keystore.id,
|
||||
});
|
||||
}
|
||||
|
||||
async getFullySetupBuildCredentialsAsync({
|
||||
ctx,
|
||||
app,
|
||||
name,
|
||||
}: {
|
||||
ctx: Context;
|
||||
app: AppLookupParams;
|
||||
name?: string;
|
||||
}): Promise<AndroidAppBuildCredentialsFragment | null> {
|
||||
if (name) {
|
||||
return await this.getFullySetupBuildCredentialsByNameAsync({ ctx, app, name });
|
||||
}
|
||||
|
||||
const defaultBuildCredentials = await ctx.newAndroid.getDefaultAndroidAppBuildCredentialsAsync(
|
||||
app
|
||||
);
|
||||
const defaultKeystore = defaultBuildCredentials?.androidKeystore ?? null;
|
||||
if (defaultKeystore) {
|
||||
Log.log(
|
||||
`Using Keystore from configuration: ${nullthrows(defaultBuildCredentials).name} (default)`
|
||||
);
|
||||
return defaultBuildCredentials;
|
||||
}
|
||||
|
||||
// fall back to legacy credentials if we cant find a default keystore
|
||||
const legacyBuildCredentials = await ctx.newAndroid.getLegacyAndroidAppBuildCredentialsAsync(
|
||||
app
|
||||
);
|
||||
const legacyKeystore = legacyBuildCredentials?.androidKeystore ?? null;
|
||||
if (legacyKeystore) {
|
||||
Log.log('Using Keystore ported from Expo Classic (expo-cli)');
|
||||
return legacyBuildCredentials;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getFullySetupBuildCredentialsByNameAsync({
|
||||
ctx,
|
||||
app,
|
||||
name,
|
||||
}: {
|
||||
ctx: Context;
|
||||
app: AppLookupParams;
|
||||
name: string;
|
||||
}): Promise<AndroidAppBuildCredentialsFragment | null> {
|
||||
const maybeBuildCredentials = await ctx.newAndroid.getAndroidAppBuildCredentialsByNameAsync(
|
||||
app,
|
||||
name
|
||||
);
|
||||
const keystore = maybeBuildCredentials?.androidKeystore ?? null;
|
||||
if (keystore) {
|
||||
const buildCredentials = nullthrows(maybeBuildCredentials);
|
||||
Log.log(
|
||||
`Using Keystore from configuration: ${buildCredentials.name}${
|
||||
buildCredentials.isDefault ? ' (default)' : ''
|
||||
}`
|
||||
);
|
||||
return buildCredentials;
|
||||
}
|
||||
Log.log(
|
||||
`No Keystore found for configuration: ${name}${
|
||||
maybeBuildCredentials?.isDefault ? ' (default)' : ''
|
||||
}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
import { confirmAsync, promptAsync } from '../../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMockWithoutCredentials,
|
||||
testAndroidBuildCredentialsFragment,
|
||||
testJksAndroidKeystoreFragment,
|
||||
testLegacyAndroidBuildCredentialsFragment,
|
||||
} from '../../../../__tests__/fixtures-android-new';
|
||||
import { createCtxMock } from '../../../../__tests__/fixtures-context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../../../errors';
|
||||
import { getAppLookupParamsFromContext } from '../../BuildCredentialsUtils';
|
||||
import { SetupBuildCredentials } from '../SetupBuildCredentials';
|
||||
|
||||
jest.mock('../../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
|
||||
describe('SetupBuildCredentials', () => {
|
||||
it('skips setup when there are prior legacy credentials', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
newAndroid: {
|
||||
...getNewAndroidApiMockWithoutCredentials(),
|
||||
getLegacyAndroidAppBuildCredentialsAsync: jest.fn(
|
||||
() => testLegacyAndroidBuildCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
|
||||
// expect keystore not to be created
|
||||
expect(ctx.newAndroid.createKeystoreAsync as any).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it('skips setup when there are prior credentials', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
newAndroid: {
|
||||
...getNewAndroidApiMockWithoutCredentials(),
|
||||
getDefaultAndroidAppBuildCredentialsAsync: jest.fn(
|
||||
() => testAndroidBuildCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
|
||||
// expect keystore not to be created
|
||||
expect(ctx.newAndroid.createKeystoreAsync as any).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it('sets up credentials when there are no prior credentials', async () => {
|
||||
(promptAsync as jest.Mock).mockImplementation(() => ({ providedName: 'test-provided-name' }));
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
newAndroid: {
|
||||
...getNewAndroidApiMockWithoutCredentials(),
|
||||
createKeystoreAsync: jest.fn(() => testJksAndroidKeystoreFragment),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
|
||||
// expect keystore to be created
|
||||
expect(ctx.newAndroid.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
(promptAsync as jest.Mock).mockImplementation(() => ({ providedName: 'test-provided-name' }));
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
newAndroid: {
|
||||
...getNewAndroidApiMockWithoutCredentials(),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
|
||||
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError(
|
||||
MissingCredentialsNonInteractiveError
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import keyBy from 'lodash/keyBy';
|
||||
|
||||
import { AndroidCredentials, FcmCredentials, Keystore } from '../credentials';
|
||||
import ApiClient from './ClientWrapper';
|
||||
|
||||
export default class AndroidApi {
|
||||
private client = new ApiClient();
|
||||
private shouldRefetchAll: boolean = true;
|
||||
private credentials: { [key: string]: AndroidCredentials } = {};
|
||||
|
||||
public async fetchAllAsync(): Promise<{ [key: string]: AndroidCredentials }> {
|
||||
if (this.shouldRefetchAll) {
|
||||
this.credentials = keyBy(await this.client.getAllCredentialsApi(), 'experienceName');
|
||||
this.shouldRefetchAll = false;
|
||||
}
|
||||
return this.credentials;
|
||||
}
|
||||
|
||||
public async fetchKeystoreAsync(experienceName: string): Promise<Keystore | null> {
|
||||
await this.ensureCredentialsFetchedAsync(experienceName);
|
||||
return this.credentials[experienceName]?.keystore || null;
|
||||
}
|
||||
|
||||
public async fetchCredentialsAsync(experienceName: string): Promise<AndroidCredentials> {
|
||||
await this.ensureCredentialsFetchedAsync(experienceName);
|
||||
return this.credentials[experienceName];
|
||||
}
|
||||
|
||||
public async updateKeystoreAsync(experienceName: string, keystore: Keystore): Promise<void> {
|
||||
await this.ensureCredentialsFetchedAsync(experienceName);
|
||||
await this.client.updateKeystoreApi(experienceName, keystore);
|
||||
this.credentials[experienceName] = {
|
||||
experienceName,
|
||||
keystore,
|
||||
pushCredentials: this.credentials[experienceName]?.pushCredentials,
|
||||
};
|
||||
}
|
||||
|
||||
public async fetchFcmKeyAsync(experienceName: string): Promise<FcmCredentials | null> {
|
||||
await this.ensureCredentialsFetchedAsync(experienceName);
|
||||
return this.credentials?.[experienceName]?.pushCredentials;
|
||||
}
|
||||
|
||||
public async updateFcmKeyAsync(experienceName: string, fcmApiKey: string): Promise<void> {
|
||||
await this.ensureCredentialsFetchedAsync(experienceName);
|
||||
await this.client.updateFcmKeyApi(experienceName, fcmApiKey);
|
||||
this.credentials[experienceName] = {
|
||||
experienceName,
|
||||
keystore: this.credentials[experienceName]?.keystore,
|
||||
pushCredentials: { fcmApiKey },
|
||||
};
|
||||
}
|
||||
|
||||
public async removeFcmKeyAsync(experienceName: string): Promise<void> {
|
||||
await this.ensureCredentialsFetchedAsync(experienceName);
|
||||
await this.client.removeFcmKeyApi(experienceName);
|
||||
if (this.credentials[experienceName]) {
|
||||
this.credentials[experienceName].pushCredentials = null;
|
||||
}
|
||||
}
|
||||
|
||||
public async removeKeystoreAsync(experienceName: string): Promise<void> {
|
||||
await this.ensureCredentialsFetchedAsync(experienceName);
|
||||
await this.client.removeKeystoreApi(experienceName);
|
||||
if (this.credentials[experienceName]) {
|
||||
this.credentials[experienceName].keystore = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureCredentialsFetchedAsync(experienceName: string): Promise<void> {
|
||||
if (!this.credentials[experienceName]) {
|
||||
const response = await this.client.getAllCredentialsForAppApi(experienceName);
|
||||
this.credentials[experienceName] = {
|
||||
experienceName,
|
||||
keystore: response?.keystore,
|
||||
pushCredentials: response?.pushCredentials,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { apiClient } from '../../../api';
|
||||
import { AndroidCredentials, Keystore } from '../credentials';
|
||||
|
||||
type AllCredentialsApiResponse = AndroidCredentials[];
|
||||
|
||||
// This class should not be used directly, use only as part of cached api client from ./Client.ts
|
||||
// or mock it in tests (it's easier to mock this class than got directly)
|
||||
export default class ClientWrapper {
|
||||
public async getAllCredentialsApi(): Promise<AllCredentialsApiResponse> {
|
||||
return ((await apiClient.get('credentials/android').json()) as any).data?.credentials || [];
|
||||
}
|
||||
public async getAllCredentialsForAppApi(projectFullName: string): Promise<AndroidCredentials> {
|
||||
return ((await apiClient.get(`credentials/android/${projectFullName}`).json()) as any)?.data;
|
||||
}
|
||||
public async updateKeystoreApi(projectFullName: string, keystore: Keystore): Promise<void> {
|
||||
await apiClient.put(`credentials/android/keystore/${projectFullName}`, {
|
||||
json: {
|
||||
keystore,
|
||||
},
|
||||
});
|
||||
}
|
||||
public async updateFcmKeyApi(projectFullName: string, fcmApiKey: string): Promise<void> {
|
||||
await apiClient.put(`credentials/android/push/${projectFullName}`, { json: { fcmApiKey } });
|
||||
}
|
||||
public async removeKeystoreApi(projectFullName: string): Promise<void> {
|
||||
await apiClient.delete(`credentials/android/keystore/${projectFullName}`);
|
||||
}
|
||||
public async removeFcmKeyApi(projectFullName: string): Promise<void> {
|
||||
await apiClient.delete(`credentials/android/push/${projectFullName}`);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
AndroidAppBuildCredentialsFragment,
|
||||
AndroidFcmFragment,
|
||||
AndroidFcmVersion,
|
||||
AndroidKeystoreFragment,
|
||||
AppFragment,
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
@@ -9,6 +11,7 @@ import { AppQuery } from '../../ios/api/graphql/queries/AppQuery';
|
||||
import { KeystoreWithType } from '../credentials';
|
||||
import { AndroidAppBuildCredentialsMutation } from './graphql/mutations/AndroidAppBuildCredentialsMutation';
|
||||
import { AndroidAppCredentialsMutation } from './graphql/mutations/AndroidAppCredentialsMutation';
|
||||
import { AndroidFcmMutation } from './graphql/mutations/AndroidFcmMutation';
|
||||
import { AndroidKeystoreMutation } from './graphql/mutations/AndroidKeystoreMutation';
|
||||
import { AndroidAppCredentialsQuery } from './graphql/queries/AndroidAppCredentialsQuery';
|
||||
|
||||
@@ -73,15 +76,24 @@ export async function createOrGetExistingAndroidAppCredentialsWithBuildCredentia
|
||||
} else {
|
||||
const app = await getAppAsync(appLookupParams);
|
||||
return await AndroidAppCredentialsMutation.createAndroidAppCredentialsAsync(
|
||||
{
|
||||
/* fcmKey not supported yet */
|
||||
},
|
||||
{},
|
||||
app.id,
|
||||
appLookupParams.androidApplicationIdentifier
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateAndroidAppCredentialsAsync(
|
||||
appCredentials: CommonAndroidAppCredentialsFragment,
|
||||
{
|
||||
androidFcmId,
|
||||
}: {
|
||||
androidFcmId: string;
|
||||
}
|
||||
): Promise<CommonAndroidAppCredentialsFragment> {
|
||||
return await AndroidAppCredentialsMutation.setFcmKeyAsync(appCredentials.id, androidFcmId);
|
||||
}
|
||||
|
||||
export async function updateAndroidAppBuildCredentialsAsync(
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment,
|
||||
{
|
||||
@@ -193,10 +205,26 @@ export async function createKeystoreAsync(
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteKeystoreAsync(keystore: AndroidKeystoreFragment): Promise<void> {
|
||||
return await AndroidKeystoreMutation.deleteAndroidKeystore(keystore.id);
|
||||
}
|
||||
|
||||
export async function createFcmAsync(
|
||||
account: Account,
|
||||
fcmApiKey: string,
|
||||
version: AndroidFcmVersion
|
||||
): Promise<AndroidFcmFragment> {
|
||||
return await AndroidFcmMutation.createAndroidFcm({ credential: fcmApiKey, version }, account.id);
|
||||
}
|
||||
|
||||
export async function deleteFcmAsync(fcm: AndroidFcmFragment): Promise<void> {
|
||||
return await AndroidFcmMutation.deleteAndroidFcm(fcm.id);
|
||||
}
|
||||
|
||||
async function getAppAsync(appLookupParams: AppLookupParams): Promise<AppFragment> {
|
||||
const projectFullName = formatProjectFullName(appLookupParams);
|
||||
return await AppQuery.byFullNameAsync(projectFullName);
|
||||
}
|
||||
|
||||
const formatProjectFullName = ({ account, projectName }: AppLookupParams): string =>
|
||||
export const formatProjectFullName = ({ account, projectName }: AppLookupParams): string =>
|
||||
`@${account.name}/${projectName}`;
|
||||
+6
-5
@@ -5,18 +5,19 @@ import gql from 'graphql-tag';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import {
|
||||
AndroidAppBuildCredentialsFragment,
|
||||
AndroidAppBuildCredentialsInput,
|
||||
CreateAndroidAppBuildCredentialsMutation,
|
||||
SetKeystoreMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AndroidAppBuildCredentialsFragmentNode } from '../../../../../graphql/types/credentials/AndroidAppBuildCredentials';
|
||||
|
||||
export type AndroidAppBuildCredentialsMetadataInput = Omit<
|
||||
AndroidAppBuildCredentialsInput,
|
||||
'keystoreId'
|
||||
>;
|
||||
const AndroidAppBuildCredentialsMutation = {
|
||||
async createAndroidAppBuildCredentialsAsync(
|
||||
androidAppBuildCredentialsInput: {
|
||||
isDefault: boolean;
|
||||
name: string;
|
||||
keystoreId: string;
|
||||
},
|
||||
androidAppBuildCredentialsInput: AndroidAppBuildCredentialsInput,
|
||||
androidAppCredentialsId: string
|
||||
): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
|
||||
+29
@@ -6,6 +6,7 @@ import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/cl
|
||||
import {
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
CreateAndroidAppCredentialsMutation,
|
||||
SetFcmMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { CommonAndroidAppCredentialsFragmentNode } from '../../../../../graphql/types/credentials/AndroidAppCredentials';
|
||||
|
||||
@@ -53,6 +54,34 @@ const AndroidAppCredentialsMutation = {
|
||||
);
|
||||
return data.androidAppCredentials.createAndroidAppCredentials;
|
||||
},
|
||||
async setFcmKeyAsync(
|
||||
androidAppCredentialsId: string,
|
||||
fcmId: string
|
||||
): Promise<CommonAndroidAppCredentialsFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<SetFcmMutation>(
|
||||
gql`
|
||||
mutation SetFcmMutation($androidAppCredentialsId: ID!, $fcmId: ID!) {
|
||||
androidAppCredentials {
|
||||
setFcm(id: $androidAppCredentialsId, fcmId: $fcmId) {
|
||||
id
|
||||
...CommonAndroidAppCredentialsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(CommonAndroidAppCredentialsFragmentNode)}
|
||||
`,
|
||||
{
|
||||
androidAppCredentialsId,
|
||||
fcmId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(data.androidAppCredentials.setFcm, 'GraphQL: `setFcm` not defined in server response');
|
||||
return data.androidAppCredentials.setFcm;
|
||||
},
|
||||
};
|
||||
|
||||
export { AndroidAppCredentialsMutation };
|
||||
@@ -0,0 +1,71 @@
|
||||
import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import {
|
||||
AndroidFcmFragment,
|
||||
AndroidFcmInput,
|
||||
CreateAndroidFcmMutation,
|
||||
DeleteAndroidFcmMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AndroidFcmFragmentNode } from '../../../../../graphql/types/credentials/AndroidFcm';
|
||||
|
||||
const AndroidFcmMutation = {
|
||||
async createAndroidFcm(
|
||||
androidFcmInput: AndroidFcmInput,
|
||||
accountId: string
|
||||
): Promise<AndroidFcmFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateAndroidFcmMutation>(
|
||||
gql`
|
||||
mutation CreateAndroidFcmMutation($androidFcmInput: AndroidFcmInput!, $accountId: ID!) {
|
||||
androidFcm {
|
||||
createAndroidFcm(androidFcmInput: $androidFcmInput, accountId: $accountId) {
|
||||
id
|
||||
...AndroidFcmFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(AndroidFcmFragmentNode)}
|
||||
`,
|
||||
{
|
||||
androidFcmInput,
|
||||
accountId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(
|
||||
data.androidFcm.createAndroidFcm,
|
||||
'GraphQL: `createAndroidFcm` not defined in server response'
|
||||
);
|
||||
return data.androidFcm.createAndroidFcm;
|
||||
},
|
||||
async deleteAndroidFcm(androidFcmId: string): Promise<void> {
|
||||
await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<DeleteAndroidFcmMutation>(
|
||||
gql`
|
||||
mutation DeleteAndroidFcmMutation($androidFcmId: ID!) {
|
||||
androidFcm {
|
||||
deleteAndroidFcm(id: $androidFcmId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
androidFcmId,
|
||||
},
|
||||
{
|
||||
additionalTypenames: ['AndroidFcm', 'AndroidAppCredentials'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export { AndroidFcmMutation };
|
||||
+26
-8
@@ -5,20 +5,15 @@ import gql from 'graphql-tag';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import {
|
||||
AndroidKeystoreFragment,
|
||||
AndroidKeystoreType,
|
||||
AndroidKeystoreInput,
|
||||
CreateAndroidKeystoreMutation,
|
||||
DeleteAndroidKeystoreMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AndroidKeystoreFragmentNode } from '../../../../../graphql/types/credentials/AndroidKeystore';
|
||||
|
||||
const AndroidKeystoreMutation = {
|
||||
async createAndroidKeystore(
|
||||
androidKeystoreInput: {
|
||||
base64EncodedKeystore: string;
|
||||
keystorePassword: string;
|
||||
keyAlias: string;
|
||||
keyPassword?: string;
|
||||
type: AndroidKeystoreType;
|
||||
},
|
||||
androidKeystoreInput: AndroidKeystoreInput,
|
||||
accountId: string
|
||||
): Promise<AndroidKeystoreFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
@@ -54,6 +49,29 @@ const AndroidKeystoreMutation = {
|
||||
);
|
||||
return data.androidKeystore.createAndroidKeystore;
|
||||
},
|
||||
async deleteAndroidKeystore(androidKeystoreId: string): Promise<void> {
|
||||
await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<DeleteAndroidKeystoreMutation>(
|
||||
gql`
|
||||
mutation DeleteAndroidKeystoreMutation($androidKeystoreId: ID!) {
|
||||
androidKeystore {
|
||||
deleteAndroidKeystore(id: $androidKeystoreId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
androidKeystoreId,
|
||||
},
|
||||
{
|
||||
additionalTypenames: ['AndroidKeystore', 'AndroidAppBuildCredentials'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export { AndroidKeystoreMutation };
|
||||
+6
-1
@@ -52,7 +52,12 @@ const AndroidAppCredentialsQuery = {
|
||||
legacyOnly,
|
||||
},
|
||||
{
|
||||
additionalTypenames: ['AndroidAppCredentials', 'AndroidAppBuildCredentials', 'App'],
|
||||
additionalTypenames: [
|
||||
'AndroidAppCredentials',
|
||||
'AndroidAppBuildCredentials',
|
||||
'App',
|
||||
'AndroidFcm',
|
||||
],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`print credentials prints the AndroidAppCredentials fragment 1`] = `"Android Credentials Project: testApp Application Identifier: test.com.app Push Notifications (FCM): Key: abcd...efgh Updated 0 second ago Configuration: legacy (Default) Keystore: Type: JKS Key Alias: QHdrb3p5cmEvY3JlZGVudGlhbHMtdGVzdA== MD5 Fingerprint: TE:ST:-M:D5 SHA1 Fingerprint: TE:ST:-S:HA:1 SHA256 Fingerprint: TE:ST:-S:HA:25:6 Updated 0 second ago"`;
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`print credentials prints the AndroidAppCredentials fragment 1`] = `"Android Credentials Project: testApp Application Identifier: test.com.app EAS Credentials: These will take precedence over Expo Classic credentials Configuration: legacy (Default) Keystore: Type: JKS Key Alias: QHdrb3p5cmEvY3JlZGVudGlhbHMtdGVzdA== MD5 Fingerprint: TE:ST:-M:D5 SHA1 Fingerprint: TE:ST:-S:HA:1 SHA256 Fingerprint: TE:ST:-S:HA:25:6 Updated 0 second ago Credentials ported from Expo Classic (expo-cli): Keystore: Type: JKS Key Alias: QHdrb3p5cmEvY3JlZGVudGlhbHMtdGVzdA== MD5 Fingerprint: TE:ST:-M:D5 SHA1 Fingerprint: TE:ST:-S:HA:1 SHA256 Fingerprint: TE:ST:-S:HA:25:6 Updated 0 second ago"`;
|
||||
@@ -1,5 +1,9 @@
|
||||
import { AndroidKeystoreType } from '../../../../graphql/generated';
|
||||
import { testKeystore, testPKCS12Keystore } from '../../../__tests__/fixtures-android';
|
||||
import {
|
||||
testKeystore,
|
||||
testPKCS12EmptyPasswordKeystore,
|
||||
testPKCS12Keystore,
|
||||
} from '../../../__tests__/fixtures-android';
|
||||
import { getKeystoreType, getKeystoreWithType, validateKeystore } from '../keystoreNew';
|
||||
|
||||
describe('getKeystoreType', () => {
|
||||
@@ -34,6 +38,7 @@ describe('getKeystoreType', () => {
|
||||
describe('validateKeystore', () => {
|
||||
it('validates a correctly formatted jks keystore', async () => {
|
||||
const keystoreWithType = getKeystoreWithType(testKeystore);
|
||||
expect(keystoreWithType.type).toBe(AndroidKeystoreType.Jks);
|
||||
expect(() => validateKeystore(keystoreWithType)).not.toThrow();
|
||||
});
|
||||
it('doesnt validate a jks keystore with wrong alias', async () => {
|
||||
@@ -52,6 +57,7 @@ describe('validateKeystore', () => {
|
||||
});
|
||||
it('validates a correctly formatted pkcs 12 keystore', async () => {
|
||||
const keystoreWithType = getKeystoreWithType(testPKCS12Keystore);
|
||||
expect(keystoreWithType.type).toBe(AndroidKeystoreType.Pkcs12);
|
||||
expect(() => validateKeystore(keystoreWithType)).not.toThrow();
|
||||
});
|
||||
it('doesnt validate a PKCS 12 keystore with wrong alias', async () => {
|
||||
@@ -61,4 +67,9 @@ describe('validateKeystore', () => {
|
||||
});
|
||||
expect(() => validateKeystore(keystoreWithType)).toThrow();
|
||||
});
|
||||
it('validates an PKCS 12 Keystore with an empty password', async () => {
|
||||
const keystoreWithType = getKeystoreWithType(testPKCS12EmptyPasswordKeystore);
|
||||
expect(keystoreWithType.type).toBe(AndroidKeystoreType.Pkcs12);
|
||||
expect(() => validateKeystore(keystoreWithType)).not.toThrow();
|
||||
});
|
||||
});
|
||||
+2
-6
@@ -1,13 +1,10 @@
|
||||
import mockdate from 'mockdate';
|
||||
|
||||
import Log from '../../../../log';
|
||||
import {
|
||||
testAndroidAppCredentialsFragment,
|
||||
testLegacyAndroidAppCredentialsFragment,
|
||||
} from '../../../__tests__/fixtures-android-new';
|
||||
import { testAndroidAppCredentialsFragment } from '../../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContext } from '../../actions/BuildCredentialsUtils';
|
||||
import { displayAndroidAppCredentials } from '../printCredentialsBeta';
|
||||
import { displayAndroidAppCredentials } from '../printCredentials';
|
||||
|
||||
jest.mock('../../../../log');
|
||||
jest.mock('chalk', () => ({ bold: jest.fn(log => log) }));
|
||||
@@ -20,7 +17,6 @@ describe('print credentials', () => {
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
displayAndroidAppCredentials({
|
||||
appLookupParams,
|
||||
legacyAppCredentials: testLegacyAndroidAppCredentialsFragment,
|
||||
appCredentials: testAndroidAppCredentialsFragment,
|
||||
});
|
||||
const loggedSoFar = (Log.log as jest.Mock).mock.calls.reduce(
|
||||
@@ -1,34 +1,128 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import {
|
||||
AndroidAppBuildCredentialsFragment,
|
||||
AndroidFcmVersion,
|
||||
AndroidKeystoreFragment,
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
FcmSnippetLegacy,
|
||||
FcmSnippetV1,
|
||||
} from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { AndroidCredentials } from '../credentials';
|
||||
import { keytoolCommandExistsAsync, logKeystoreHashesAsync } from '../utils/keystore';
|
||||
import { fromNow } from '../../../utils/date';
|
||||
import { sortBuildCredentials } from '../actions/BuildCredentialsUtils';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
|
||||
export async function printAndroidCredentials(credentialsList: AndroidCredentials[]) {
|
||||
Log.log(chalk.bold('Available Android credentials'));
|
||||
export function displayEmptyAndroidCredentials(appLookupParams: AppLookupParams): void {
|
||||
const { projectName, androidApplicationIdentifier } = appLookupParams;
|
||||
Log.log(chalk.bold(`Android Credentials`));
|
||||
Log.log(` Project: ${projectName}`);
|
||||
Log.log(` Application Identifier: ${androidApplicationIdentifier}`);
|
||||
Log.log(` No credentials set up yet!`);
|
||||
}
|
||||
|
||||
function displayAndroidFcmCredentials(appCredentials: CommonAndroidAppCredentialsFragment): void {
|
||||
const maybeFcm = appCredentials.androidFcm;
|
||||
Log.log(` Push Notifications (FCM):`);
|
||||
if (!maybeFcm) {
|
||||
Log.log(` None assigned yet`);
|
||||
Log.newLine();
|
||||
return;
|
||||
}
|
||||
const { snippet, version, updatedAt } = maybeFcm;
|
||||
if (version === AndroidFcmVersion.Legacy) {
|
||||
const { firstFourCharacters, lastFourCharacters } = snippet as FcmSnippetLegacy;
|
||||
Log.log(` Key: ${firstFourCharacters}...${lastFourCharacters}`);
|
||||
} else if (version === AndroidFcmVersion.V1) {
|
||||
const { projectId, serviceAccountEmail, clientId, keyId } = snippet as FcmSnippetV1;
|
||||
Log.log(` Project Id: ${projectId}`);
|
||||
Log.log(` Service Account: ${serviceAccountEmail}`);
|
||||
Log.log(` Client Id: ${clientId}`);
|
||||
Log.log(` Key Id: ${keyId}`);
|
||||
}
|
||||
Log.log(` Updated ${fromNow(new Date(updatedAt))} ago`);
|
||||
Log.newLine();
|
||||
for (const credentials of credentialsList) {
|
||||
await printAndroidAppCredentials(credentials);
|
||||
}
|
||||
|
||||
function displayEASAndroidAppCredentials(
|
||||
appCredentials: CommonAndroidAppCredentialsFragment
|
||||
): void {
|
||||
displayAndroidFcmCredentials(appCredentials);
|
||||
const sortedBuildCredentialsList = sortBuildCredentials(
|
||||
appCredentials.androidAppBuildCredentialsList
|
||||
);
|
||||
for (const buildCredentials of sortedBuildCredentialsList) {
|
||||
displayAndroidBuildCredentials(buildCredentials);
|
||||
}
|
||||
}
|
||||
|
||||
export async function printAndroidAppCredentials(credentials: AndroidCredentials) {
|
||||
Log.log(chalk.green(credentials.experienceName));
|
||||
Log.log(chalk.bold(' Keystore hashes'));
|
||||
if (credentials.keystore?.keystore) {
|
||||
if (await keytoolCommandExistsAsync()) {
|
||||
try {
|
||||
await logKeystoreHashesAsync(credentials.keystore, ' ');
|
||||
} catch (error) {
|
||||
Log.error(' Failed to parse the Keystore', error);
|
||||
}
|
||||
function formatFingerprint(fingerprint: string | null): string {
|
||||
if (!fingerprint) {
|
||||
return 'unavailable';
|
||||
}
|
||||
const uppercaseFingerprint = fingerprint.toUpperCase();
|
||||
const bytes = [];
|
||||
for (let i = 0; i < uppercaseFingerprint.length; i++) {
|
||||
const halfByte = uppercaseFingerprint.charAt(i);
|
||||
if (i % 2 === 0) {
|
||||
bytes.push(halfByte); // first half of the byte
|
||||
} else {
|
||||
Log.warn(' keytool is required to calulate Keystore hashes');
|
||||
bytes[bytes.length - 1] += halfByte; // second half of the byte
|
||||
}
|
||||
}
|
||||
return bytes.join(':');
|
||||
}
|
||||
function displayAndroidBuildCredentials(
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment,
|
||||
skipConfigurationDisplay?: boolean
|
||||
): void {
|
||||
if (!skipConfigurationDisplay) {
|
||||
const { isDefault, name } = buildCredentials;
|
||||
Log.log(chalk.bold(` Configuration: ${name}${isDefault ? ' (Default)' : ''}`));
|
||||
}
|
||||
|
||||
const maybeKeystore = buildCredentials.androidKeystore;
|
||||
Log.log(` Keystore:`);
|
||||
if (maybeKeystore) {
|
||||
displayAndroidKeystore(maybeKeystore);
|
||||
} else {
|
||||
Log.log(' -----------------------');
|
||||
Log.log(` None assigned yet`);
|
||||
}
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
export function displayAndroidKeystore(keystore: AndroidKeystoreFragment) {
|
||||
const {
|
||||
keyAlias,
|
||||
type,
|
||||
md5CertificateFingerprint,
|
||||
sha1CertificateFingerprint,
|
||||
sha256CertificateFingerprint,
|
||||
updatedAt,
|
||||
} = keystore;
|
||||
Log.log(` Type: ${type}`);
|
||||
Log.log(` Key Alias: ${keyAlias}`);
|
||||
|
||||
Log.log(` MD5 Fingerprint: ${formatFingerprint(md5CertificateFingerprint ?? null)}`);
|
||||
Log.log(` SHA1 Fingerprint: ${formatFingerprint(sha1CertificateFingerprint ?? null)}`);
|
||||
Log.log(` SHA256 Fingerprint: ${formatFingerprint(sha256CertificateFingerprint ?? null)}`);
|
||||
Log.log(` Updated ${fromNow(new Date(updatedAt))} ago`);
|
||||
}
|
||||
|
||||
export function displayAndroidAppCredentials({
|
||||
appLookupParams,
|
||||
appCredentials,
|
||||
}: {
|
||||
appLookupParams: AppLookupParams;
|
||||
appCredentials: CommonAndroidAppCredentialsFragment | null;
|
||||
}): void {
|
||||
const { projectName, androidApplicationIdentifier } = appLookupParams;
|
||||
Log.log(chalk.bold(`Android Credentials`));
|
||||
Log.log(` Project: ${projectName}`);
|
||||
Log.log(` Application Identifier: ${androidApplicationIdentifier}`);
|
||||
Log.newLine();
|
||||
|
||||
if (appCredentials) {
|
||||
displayEASAndroidAppCredentials(appCredentials);
|
||||
}
|
||||
Log.log(chalk.bold(' Push Notifications credentials'));
|
||||
Log.log(' FCM API Key: ', credentials.pushCredentials?.fcmApiKey ?? '---------------------');
|
||||
Log.log('\n');
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import {
|
||||
AndroidAppBuildCredentialsFragment,
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
} from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { fromNow } from '../../../utils/date';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
|
||||
export function displayEmptyAndroidCredentials(appLookupParams: AppLookupParams): void {
|
||||
const { projectName, androidApplicationIdentifier } = appLookupParams;
|
||||
Log.log(chalk.bold(`Android Credentials`));
|
||||
Log.log(` Project: ${projectName}`);
|
||||
Log.log(` Application Identifier: ${androidApplicationIdentifier}`);
|
||||
Log.log(` No credentials set up yet!`);
|
||||
}
|
||||
|
||||
/**
|
||||
* sort a build credentials array in descending order of preference
|
||||
* prefer default credentials, then prefer names that come first lexicographically
|
||||
*/
|
||||
function sortBuildCredentials(
|
||||
androidAppBuildCredentialsList: AndroidAppBuildCredentialsFragment[]
|
||||
): AndroidAppBuildCredentialsFragment[] {
|
||||
return androidAppBuildCredentialsList.sort((buildCredentialsA, buildCredentialsB) => {
|
||||
if (buildCredentialsA.isDefault) {
|
||||
return -1;
|
||||
} else if (buildCredentialsB.isDefault) {
|
||||
return 1;
|
||||
}
|
||||
return buildCredentialsA.name.localeCompare(buildCredentialsB.name);
|
||||
});
|
||||
}
|
||||
|
||||
function displayLegacyAndroidAppCredentials(
|
||||
legacyAppCredentials: CommonAndroidAppCredentialsFragment,
|
||||
appCredentials: CommonAndroidAppCredentialsFragment | null
|
||||
): void {
|
||||
if (appCredentials) {
|
||||
// differentiate between old and new if there's both
|
||||
Log.log(chalk.bold(` Credentials ported from Expo Classic (expo-cli):`));
|
||||
}
|
||||
// There can only be one set of build credentials in legacy app credentials
|
||||
const [legacyBuildCredentials] = legacyAppCredentials.androidAppBuildCredentialsList;
|
||||
if (legacyBuildCredentials) {
|
||||
displayAndroidBuildCredentials(
|
||||
legacyBuildCredentials,
|
||||
/* only one set to show, skip config display */ true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function displayEASAndroidAppCredentials(
|
||||
legacyAppCredentials: CommonAndroidAppCredentialsFragment | null,
|
||||
appCredentials: CommonAndroidAppCredentialsFragment
|
||||
): void {
|
||||
if (legacyAppCredentials) {
|
||||
// differentiate between old and new if there's both
|
||||
Log.log(chalk.bold(` EAS Credentials:`));
|
||||
Log.log(` These will take precedence over Expo Classic credentials`);
|
||||
}
|
||||
const sortedBuildCredentialsList = sortBuildCredentials(
|
||||
appCredentials.androidAppBuildCredentialsList
|
||||
);
|
||||
for (const buildCredentials of sortedBuildCredentialsList) {
|
||||
displayAndroidBuildCredentials(buildCredentials);
|
||||
}
|
||||
}
|
||||
|
||||
function formatFingerprint(fingerprint: string | null): string {
|
||||
if (!fingerprint) {
|
||||
return 'unavailable';
|
||||
}
|
||||
const uppercaseFingerprint = fingerprint.toUpperCase();
|
||||
const bytes = [];
|
||||
for (let i = 0; i < uppercaseFingerprint.length; i++) {
|
||||
const halfByte = uppercaseFingerprint.charAt(i);
|
||||
if (i % 2 === 0) {
|
||||
bytes.push(halfByte); // first half of the byte
|
||||
} else {
|
||||
bytes[bytes.length - 1] += halfByte; // second half of the byte
|
||||
}
|
||||
}
|
||||
return bytes.join(':');
|
||||
}
|
||||
function displayAndroidBuildCredentials(
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment,
|
||||
skipConfigurationDisplay?: boolean
|
||||
): void {
|
||||
if (!skipConfigurationDisplay) {
|
||||
const { isDefault, name } = buildCredentials;
|
||||
Log.log(chalk.bold(` Configuration: ${name}${isDefault ? ' (Default)' : ''}`));
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
const maybeKeystore = buildCredentials.androidKeystore;
|
||||
Log.log(` Keystore:`);
|
||||
if (maybeKeystore) {
|
||||
const {
|
||||
keyAlias,
|
||||
type,
|
||||
md5CertificateFingerprint,
|
||||
sha1CertificateFingerprint,
|
||||
sha256CertificateFingerprint,
|
||||
updatedAt,
|
||||
} = maybeKeystore;
|
||||
Log.log(` Type: ${type}`);
|
||||
Log.log(` Key Alias: ${keyAlias}`);
|
||||
|
||||
Log.log(` MD5 Fingerprint: ${formatFingerprint(md5CertificateFingerprint ?? null)}`);
|
||||
Log.log(` SHA1 Fingerprint: ${formatFingerprint(sha1CertificateFingerprint ?? null)}`);
|
||||
Log.log(` SHA256 Fingerprint: ${formatFingerprint(sha256CertificateFingerprint ?? null)}`);
|
||||
Log.log(` Updated ${fromNow(new Date(updatedAt))} ago`);
|
||||
} else {
|
||||
Log.log(` None assigned yet`);
|
||||
}
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
export function displayAndroidAppCredentials({
|
||||
appLookupParams,
|
||||
legacyAppCredentials,
|
||||
appCredentials,
|
||||
}: {
|
||||
appLookupParams: AppLookupParams;
|
||||
legacyAppCredentials: CommonAndroidAppCredentialsFragment | null;
|
||||
appCredentials: CommonAndroidAppCredentialsFragment | null;
|
||||
}): void {
|
||||
const { projectName, androidApplicationIdentifier } = appLookupParams;
|
||||
Log.log(chalk.bold(`Android Credentials`));
|
||||
Log.log(` Project: ${projectName}`);
|
||||
Log.log(` Application Identifier: ${androidApplicationIdentifier}`);
|
||||
Log.newLine();
|
||||
|
||||
if (appCredentials) {
|
||||
displayEASAndroidAppCredentials(legacyAppCredentials, appCredentials);
|
||||
}
|
||||
|
||||
if (legacyAppCredentials) {
|
||||
displayLegacyAndroidAppCredentials(legacyAppCredentials, appCredentials);
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,14 @@ import { getProjectAccountName } from '../project/projectUtils';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { Actor, getActorDisplayName } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import AndroidApi from './android/api/Client';
|
||||
import * as AndroidGraphqlClient from './android/api/GraphqlClient';
|
||||
import * as IosGraphqlClient from './ios/api/GraphqlClient';
|
||||
import AppStoreApi from './ios/appstore/AppStoreApi';
|
||||
|
||||
export interface Action<T = void> {
|
||||
runAsync(ctx: Context): Promise<T>;
|
||||
}
|
||||
|
||||
interface AppleCtxOptions {
|
||||
appleId?: string;
|
||||
appleIdPassword?: string;
|
||||
@@ -26,8 +29,7 @@ export interface Context {
|
||||
readonly projectDir: string;
|
||||
readonly user: Actor;
|
||||
readonly nonInteractive: boolean;
|
||||
readonly android: AndroidApi;
|
||||
readonly newAndroid: typeof AndroidGraphqlClient;
|
||||
readonly android: typeof AndroidGraphqlClient;
|
||||
readonly ios: typeof IosGraphqlClient;
|
||||
readonly appStore: AppStoreApi;
|
||||
readonly hasProjectContext: boolean;
|
||||
@@ -55,8 +57,7 @@ export async function createCredentialsContextAsync(
|
||||
}
|
||||
|
||||
class CredentialsContext implements Context {
|
||||
public readonly android = new AndroidApi();
|
||||
public readonly newAndroid = AndroidGraphqlClient;
|
||||
public readonly android = AndroidGraphqlClient;
|
||||
public readonly ios = IosGraphqlClient;
|
||||
public readonly appStore: AppStoreApi;
|
||||
public readonly nonInteractive: boolean;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { findApplicationTarget } from '../../../project/ios/target';
|
||||
import { testTargets } from '../../__tests__/fixtures-ios';
|
||||
import * as credentialsJsonReader from '../read';
|
||||
|
||||
jest.mock('fs');
|
||||
@@ -37,7 +39,7 @@ describe('credentialsJson', () => {
|
||||
it('should throw error when credentials.json is missing', async () => {
|
||||
const promise = credentialsJsonReader.readAndroidCredentialsAsync('.');
|
||||
await expect(promise).rejects.toThrow(
|
||||
'credentials.json must exist in the project root directory and contain a valid JSON'
|
||||
'credentials.json does not exist in the project root directory'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -47,9 +49,7 @@ describe('credentialsJson', () => {
|
||||
'keystore.jks': 'somebinarydata',
|
||||
});
|
||||
const promise = credentialsJsonReader.readAndroidCredentialsAsync('.');
|
||||
await expect(promise).rejects.toThrow(
|
||||
'Android credentials are missing from credentials.json'
|
||||
);
|
||||
await expect(promise).rejects.toThrow('Android credentials are missing in credentials.json');
|
||||
});
|
||||
it('should throw error when one of the required fields is missing', async () => {
|
||||
vol.fromJSON({
|
||||
@@ -69,7 +69,7 @@ describe('credentialsJson', () => {
|
||||
'credentials.json is not valid [ValidationError: "android.keystore.keystorePath" is required]'
|
||||
);
|
||||
});
|
||||
it('should throw error when file specified in cedentials.json is missing', async () => {
|
||||
it('should throw error when file specified in credentials.json is missing', async () => {
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
android: {
|
||||
@@ -105,19 +105,25 @@ describe('credentialsJson', () => {
|
||||
'./pprofile': 'somebinarycontent',
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
const result = await credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
const applicationTarget = findApplicationTarget(testTargets);
|
||||
const result = await credentialsJsonReader.readIosCredentialsAsync('.', applicationTarget);
|
||||
expect(result).toEqual({
|
||||
provisioningProfile: 'c29tZWJpbmFyeWNvbnRlbnQ=',
|
||||
distributionCertificate: {
|
||||
certP12: 'c29tZWJpbmFyeWNvbnRlbnQy',
|
||||
certPassword: 'certPass',
|
||||
[applicationTarget.targetName]: {
|
||||
provisioningProfile: 'c29tZWJpbmFyeWNvbnRlbnQ=',
|
||||
distributionCertificate: {
|
||||
certificateP12: 'c29tZWJpbmFyeWNvbnRlbnQy',
|
||||
certificatePassword: 'certPass',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should throw error when credentials.json is missing', async () => {
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync(
|
||||
'.',
|
||||
findApplicationTarget(testTargets)
|
||||
);
|
||||
await expect(promise).rejects.toThrow(
|
||||
'credentials.json must exist in the project root directory and contain a valid JSON'
|
||||
'credentials.json does not exist in the project root directory'
|
||||
);
|
||||
});
|
||||
it('should throw error if ios field is missing', async () => {
|
||||
@@ -126,8 +132,11 @@ describe('credentialsJson', () => {
|
||||
'./pprofile': 'somebinarycontent',
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
await expect(promise).rejects.toThrow('iOS credentials are missing from credentials.json');
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync(
|
||||
'.',
|
||||
findApplicationTarget(testTargets)
|
||||
);
|
||||
await expect(promise).rejects.toThrow('iOS credentials are missing in credentials.json');
|
||||
});
|
||||
it('should throw error if some field is missing', async () => {
|
||||
vol.fromJSON({
|
||||
@@ -142,7 +151,10 @@ describe('credentialsJson', () => {
|
||||
'./pprofile': 'somebinarycontent',
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync(
|
||||
'.',
|
||||
findApplicationTarget(testTargets)
|
||||
);
|
||||
await expect(promise).rejects.toThrow(
|
||||
'credentials.json is not valid [ValidationError: "ios" does not match any of the allowed types]'
|
||||
);
|
||||
@@ -160,7 +172,10 @@ describe('credentialsJson', () => {
|
||||
}),
|
||||
'./pprofile': 'somebinarycontent',
|
||||
});
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync(
|
||||
'.',
|
||||
findApplicationTarget(testTargets)
|
||||
);
|
||||
await expect(promise).rejects.toThrow("ENOENT: no such file or directory, open 'cert.p12'");
|
||||
});
|
||||
it('should throw error if provisioningProfile file is missing', async () => {
|
||||
@@ -176,7 +191,10 @@ describe('credentialsJson', () => {
|
||||
}),
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync(
|
||||
'.',
|
||||
findApplicationTarget(testTargets)
|
||||
);
|
||||
await expect(promise).rejects.toThrow("ENOENT: no such file or directory, open 'pprofile'");
|
||||
});
|
||||
});
|
||||
@@ -206,49 +224,27 @@ describe('credentialsJson', () => {
|
||||
'./dist-cert-1.p12': 'cert-1-somebinarycontent',
|
||||
'./dist-cert-2.p12': 'cert-2-somebinarycontent',
|
||||
});
|
||||
const result = await credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
const result = await credentialsJsonReader.readIosCredentialsAsync(
|
||||
'.',
|
||||
findApplicationTarget(testTargets)
|
||||
);
|
||||
expect(result).toEqual({
|
||||
target1: {
|
||||
provisioningProfile: 'cHByb2ZpbGUtMS1zb21lYmluYXJ5Y29udGVudA==',
|
||||
distributionCertificate: {
|
||||
certP12: 'Y2VydC0xLXNvbWViaW5hcnljb250ZW50',
|
||||
certPassword: 'cert-pass-1',
|
||||
certificateP12: 'Y2VydC0xLXNvbWViaW5hcnljb250ZW50',
|
||||
certificatePassword: 'cert-pass-1',
|
||||
},
|
||||
},
|
||||
target2: {
|
||||
provisioningProfile: 'cHByb2ZpbGUtMi1zb21lYmluYXJ5Y29udGVudA==',
|
||||
distributionCertificate: {
|
||||
certP12: 'Y2VydC0yLXNvbWViaW5hcnljb250ZW50',
|
||||
certPassword: 'cert-pass-2',
|
||||
certificateP12: 'Y2VydC0yLXNvbWViaW5hcnljb250ZW50',
|
||||
certificatePassword: 'cert-pass-2',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('readEnvironmentSecretsAsync', () => {
|
||||
it('should read environmentSecrets field correctly', async () => {
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
ios: {
|
||||
provisioningProfilePath: 'pprofile',
|
||||
distributionCertificate: {
|
||||
path: 'cert.p12',
|
||||
password: 'certPass',
|
||||
},
|
||||
},
|
||||
experimental: {
|
||||
npmToken: 'VALUE',
|
||||
},
|
||||
}),
|
||||
'./pprofile': 'somebinarycontent',
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
const result = await credentialsJsonReader.readEnvironmentSecretsAsync('.');
|
||||
expect(result).toEqual({
|
||||
NPM_TOKEN: 'VALUE',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,19 +4,23 @@ import prompts from 'prompts';
|
||||
|
||||
import { asMock } from '../../../__tests__/utils';
|
||||
import { IosDistributionType } from '../../../graphql/generated';
|
||||
import { testKeystore } from '../../__tests__/fixtures-android';
|
||||
import {
|
||||
testKeystore,
|
||||
testLegacyAndroidBuildCredentialsFragment,
|
||||
} from '../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../__tests__/fixtures-context';
|
||||
import {
|
||||
getNewIosApiMockWithoutCredentials,
|
||||
testAllCredentialsForApp,
|
||||
getNewIosApiMock,
|
||||
testCommonIosAppCredentialsFragment,
|
||||
testDistCertFragmentNoDependencies,
|
||||
testProvisioningProfileFragment,
|
||||
} from '../../__tests__/fixtures-ios';
|
||||
import { getAppLookupParamsFromContext } from '../../ios/actions/BuildCredentialsUtils';
|
||||
import { getAppFromContext } from '../../ios/actions/BuildCredentialsUtils';
|
||||
import { Target } from '../../ios/types';
|
||||
import { updateAndroidCredentialsAsync, updateIosCredentialsAsync } from '../update';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('prompts');
|
||||
jest.mock('../../../project/ios/bundleIdentifier');
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
@@ -45,11 +49,7 @@ beforeEach(() => {
|
||||
describe('update credentials.json', () => {
|
||||
describe(updateAndroidCredentialsAsync, () => {
|
||||
it('should update keystore in credentials.json if www returns valid credentials', async () => {
|
||||
const ctx = createCtxMock({
|
||||
android: {
|
||||
fetchKeystoreAsync: jest.fn(() => testKeystore),
|
||||
},
|
||||
});
|
||||
const ctx = createCtxMock();
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
android: {
|
||||
@@ -63,7 +63,7 @@ describe('update credentials.json', () => {
|
||||
}),
|
||||
'keystore.jks': 'somebinarydata',
|
||||
});
|
||||
await updateAndroidCredentialsAsync(ctx);
|
||||
await updateAndroidCredentialsAsync(ctx, testLegacyAndroidBuildCredentialsFragment);
|
||||
const keystore = await fs.readFile('./keystore.jks', 'base64');
|
||||
const credJson = await fs.readJson('./credentials.json');
|
||||
expect(keystore).toEqual(testKeystore.keystore);
|
||||
@@ -71,30 +71,28 @@ describe('update credentials.json', () => {
|
||||
android: {
|
||||
keystore: {
|
||||
keystorePath: 'keystore.jks',
|
||||
keystorePassword: testKeystore.keystorePassword,
|
||||
keyAlias: testKeystore.keyAlias,
|
||||
keyPassword: testKeystore.keyPassword,
|
||||
keystorePassword:
|
||||
testLegacyAndroidBuildCredentialsFragment.androidKeystore?.keystorePassword,
|
||||
keyAlias: testLegacyAndroidBuildCredentialsFragment.androidKeystore?.keyAlias,
|
||||
keyPassword: testLegacyAndroidBuildCredentialsFragment.androidKeystore?.keyPassword,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should create credentials.json and keystore if credentials.json does not exist', async () => {
|
||||
const ctx = createCtxMock({
|
||||
android: {
|
||||
fetchKeystoreAsync: jest.fn(() => testKeystore),
|
||||
},
|
||||
});
|
||||
await updateAndroidCredentialsAsync(ctx);
|
||||
const keystore = await fs.readFile('./android/keystores/keystore.jks', 'base64');
|
||||
const ctx = createCtxMock();
|
||||
await updateAndroidCredentialsAsync(ctx, testLegacyAndroidBuildCredentialsFragment);
|
||||
const keystore = await fs.readFile('./credentials/android/keystore.jks', 'base64');
|
||||
const credJson = await fs.readJson('./credentials.json');
|
||||
expect(keystore).toEqual(testKeystore.keystore);
|
||||
expect(credJson).toEqual({
|
||||
android: {
|
||||
keystore: {
|
||||
keystorePath: 'android/keystores/keystore.jks',
|
||||
keystorePassword: testKeystore.keystorePassword,
|
||||
keyAlias: testKeystore.keyAlias,
|
||||
keyPassword: testKeystore.keyPassword,
|
||||
keystorePath: 'credentials/android/keystore.jks',
|
||||
keystorePassword:
|
||||
testLegacyAndroidBuildCredentialsFragment.androidKeystore?.keystorePassword,
|
||||
keyAlias: testLegacyAndroidBuildCredentialsFragment.androidKeystore?.keyAlias,
|
||||
keyPassword: testLegacyAndroidBuildCredentialsFragment.androidKeystore?.keyPassword,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -116,7 +114,9 @@ describe('update credentials.json', () => {
|
||||
'keystore.jks': 'somebinarydata',
|
||||
});
|
||||
try {
|
||||
await updateAndroidCredentialsAsync(ctx);
|
||||
const buildCredentialsNoKeystore = { ...testLegacyAndroidBuildCredentialsFragment };
|
||||
delete buildCredentialsNoKeystore.androidKeystore;
|
||||
await updateAndroidCredentialsAsync(ctx, buildCredentialsNoKeystore);
|
||||
throw new Error('updateAndroidCredentialsAsync should throw an error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
@@ -129,35 +129,28 @@ describe('update credentials.json', () => {
|
||||
expect(newCredJson).toEqual(credJson);
|
||||
});
|
||||
it('should update keystore and credentials.json if android part of credentials.json is not valid', async () => {
|
||||
const ctx = createCtxMock({
|
||||
android: {
|
||||
fetchKeystoreAsync: jest.fn(() => testKeystore),
|
||||
},
|
||||
});
|
||||
const ctx = createCtxMock();
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({ android: { test: '123' } }),
|
||||
});
|
||||
await updateAndroidCredentialsAsync(ctx);
|
||||
const keystore = await fs.readFile('./android/keystores/keystore.jks', 'base64');
|
||||
await updateAndroidCredentialsAsync(ctx, testLegacyAndroidBuildCredentialsFragment);
|
||||
const keystore = await fs.readFile('./credentials/android/keystore.jks', 'base64');
|
||||
const credJson = await fs.readJson('./credentials.json');
|
||||
expect(keystore).toEqual(testKeystore.keystore);
|
||||
expect(credJson).toEqual({
|
||||
android: {
|
||||
keystore: {
|
||||
keystorePath: 'android/keystores/keystore.jks',
|
||||
keystorePassword: testKeystore.keystorePassword,
|
||||
keyAlias: testKeystore.keyAlias,
|
||||
keyPassword: testKeystore.keyPassword,
|
||||
keystorePath: 'credentials/android/keystore.jks',
|
||||
keystorePassword:
|
||||
testLegacyAndroidBuildCredentialsFragment.androidKeystore?.keystorePassword,
|
||||
keyAlias: testLegacyAndroidBuildCredentialsFragment.androidKeystore?.keyAlias,
|
||||
keyPassword: testLegacyAndroidBuildCredentialsFragment.androidKeystore?.keyPassword,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should update keystore and credentials.json if ios part of credentials.json is not valid', async () => {
|
||||
const ctx = createCtxMock({
|
||||
android: {
|
||||
fetchKeystoreAsync: jest.fn(() => testKeystore),
|
||||
},
|
||||
});
|
||||
const ctx = createCtxMock();
|
||||
const credJson = {
|
||||
android: {
|
||||
keystore: {
|
||||
@@ -175,7 +168,7 @@ describe('update credentials.json', () => {
|
||||
'./credentials.json': JSON.stringify(credJson),
|
||||
'keystore.jks': 'somebinarydata',
|
||||
});
|
||||
await updateAndroidCredentialsAsync(ctx);
|
||||
await updateAndroidCredentialsAsync(ctx, testLegacyAndroidBuildCredentialsFragment);
|
||||
const keystore = await fs.readFile('./keystore.jks', 'base64');
|
||||
const newCredJson = await fs.readJson('./credentials.json');
|
||||
expect(keystore).toEqual(testKeystore.keystore);
|
||||
@@ -195,10 +188,17 @@ describe('update credentials.json', () => {
|
||||
});
|
||||
});
|
||||
describe(updateIosCredentialsAsync, () => {
|
||||
const targets: Target[] = [
|
||||
{
|
||||
targetName: 'testapp',
|
||||
bundleIdentifier: 'com.bundle.id',
|
||||
},
|
||||
];
|
||||
|
||||
it('should update ios credentials in credentials.json if www returns valid credentials', async () => {
|
||||
const ctx = createCtxMock({
|
||||
ios: {
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
...getNewIosApiMock(),
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testCommonIosAppCredentialsFragment
|
||||
),
|
||||
@@ -217,8 +217,9 @@ describe('update credentials.json', () => {
|
||||
'./pprofile': 'somebinarycontent',
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
await updateIosCredentialsAsync(ctx, appLookupParams, IosDistributionType.AppStore);
|
||||
const app = getAppFromContext(ctx);
|
||||
|
||||
await updateIosCredentialsAsync(ctx, app, targets, IosDistributionType.AppStore);
|
||||
const certP12 = await fs.readFile('./cert.p12', 'base64');
|
||||
const pprofile = await fs.readFile('./pprofile', 'base64');
|
||||
const credJson = await fs.readJson('./credentials.json');
|
||||
@@ -235,7 +236,7 @@ describe('update credentials.json', () => {
|
||||
provisioningProfilePath: 'pprofile',
|
||||
distributionCertificate: {
|
||||
path: 'cert.p12',
|
||||
password: testAllCredentialsForApp.distCredentials.certPassword,
|
||||
password: testDistCertFragmentNoDependencies.certificatePassword,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -243,25 +244,27 @@ describe('update credentials.json', () => {
|
||||
it('should create credentials.json provisioning profile and distribution certificate if credentials.json does not exist', async () => {
|
||||
const ctx = createCtxMock({
|
||||
ios: {
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
...getNewIosApiMock(),
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testCommonIosAppCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
await updateIosCredentialsAsync(ctx, appLookupParams, IosDistributionType.AppStore);
|
||||
const certP12 = await fs.readFile('./ios/certs/dist-cert.p12', 'base64');
|
||||
const pprofile = await fs.readFile('./ios/certs/profile.mobileprovision', 'base64');
|
||||
const app = getAppFromContext(ctx);
|
||||
|
||||
await updateIosCredentialsAsync(ctx, app, targets, IosDistributionType.AppStore);
|
||||
|
||||
const certP12 = await fs.readFile('./credentials/ios/dist-cert.p12', 'base64');
|
||||
const pprofile = await fs.readFile('./credentials/ios/profile.mobileprovision', 'base64');
|
||||
const credJson = await fs.readJson('./credentials.json');
|
||||
expect(certP12).toEqual(testAllCredentialsForApp.distCredentials.certP12);
|
||||
expect(pprofile).toEqual(testAllCredentialsForApp.credentials.provisioningProfile);
|
||||
expect(certP12).toEqual(testDistCertFragmentNoDependencies.certificateP12);
|
||||
expect(pprofile).toEqual(testProvisioningProfileFragment.provisioningProfile);
|
||||
expect(credJson).toEqual({
|
||||
ios: {
|
||||
provisioningProfilePath: 'ios/certs/profile.mobileprovision',
|
||||
provisioningProfilePath: `credentials/ios/profile.mobileprovision`,
|
||||
distributionCertificate: {
|
||||
path: 'ios/certs/dist-cert.p12',
|
||||
password: testAllCredentialsForApp.distCredentials.certPassword,
|
||||
path: `credentials/ios/dist-cert.p12`,
|
||||
password: testDistCertFragmentNoDependencies.certificatePassword,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -269,7 +272,7 @@ describe('update credentials.json', () => {
|
||||
it('should not do anything if no credentials are returned from www', async () => {
|
||||
const ctx = createCtxMock({
|
||||
ios: {
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
...getNewIosApiMock(),
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(() => null),
|
||||
},
|
||||
});
|
||||
@@ -287,14 +290,12 @@ describe('update credentials.json', () => {
|
||||
'./pprofile': 'somebinarycontent',
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
const app = getAppFromContext(ctx);
|
||||
try {
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
await updateIosCredentialsAsync(ctx, appLookupParams, IosDistributionType.AppStore);
|
||||
await updateIosCredentialsAsync(ctx, app, targets, IosDistributionType.AppStore);
|
||||
throw new Error('updateIosCredentialsAsync should throw na error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
'There are no credentials configured for the APP_STORE distribution of this project on EAS servers'
|
||||
);
|
||||
expect(e.message).toMatch('There are no credentials configured');
|
||||
}
|
||||
const certP12 = await fs.readFile('./cert.p12', 'base64');
|
||||
const pprofile = await fs.readFile('./pprofile', 'base64');
|
||||
@@ -303,116 +304,5 @@ describe('update credentials.json', () => {
|
||||
expect(pprofile).toEqual('c29tZWJpbmFyeWNvbnRlbnQ='); // base64 "somebinarycontent"
|
||||
expect(newCredJson).toEqual(credJson);
|
||||
});
|
||||
it('should display confirm prompt if some credentials are missing in www (confirm: true)', async () => {
|
||||
// create deep clone the quick and dirty way
|
||||
const testIosAppCredentialsNoDistCert = JSON.parse(
|
||||
JSON.stringify(testCommonIosAppCredentialsFragment)
|
||||
);
|
||||
testIosAppCredentialsNoDistCert.iosAppBuildCredentialsList[0].distributionCertificate = null;
|
||||
const ctx = createCtxMock({
|
||||
ios: {
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(() => testIosAppCredentialsNoDistCert),
|
||||
},
|
||||
});
|
||||
|
||||
(prompts as any)
|
||||
.mockImplementationOnce(() => ({ value: true })) // Continue with partial credentials
|
||||
.mockImplementation(() => {
|
||||
throw new Error("shouldn't happen");
|
||||
});
|
||||
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
ios: {
|
||||
provisioningProfilePath: 'pprofile',
|
||||
distributionCertificate: {
|
||||
path: 'cert.p12',
|
||||
password: 'certPass',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'./pprofile': 'somebinarycontent',
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
await updateIosCredentialsAsync(ctx, appLookupParams, IosDistributionType.AppStore);
|
||||
const certP12Exists = await fs.pathExists('./cert.p12');
|
||||
const pprofile = await fs.readFile('./pprofile', 'base64');
|
||||
const credJson = await fs.readJson('./credentials.json');
|
||||
expect(certP12Exists).toEqual(false);
|
||||
expect(pprofile).toEqual(testAllCredentialsForApp.credentials.provisioningProfile);
|
||||
expect(credJson).toEqual({
|
||||
ios: {
|
||||
provisioningProfilePath: 'pprofile',
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should throw an error if credentials.json contains credentials for multi-target projects', async () => {
|
||||
const ctx = createCtxMock({
|
||||
ios: {
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testCommonIosAppCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
ios: {
|
||||
target1: {
|
||||
provisioningProfilePath: 'pprofile-1.mobileprovision',
|
||||
distributionCertificate: {
|
||||
path: 'dist-cert-1.p12',
|
||||
password: 'cert-pass-1',
|
||||
},
|
||||
},
|
||||
target2: {
|
||||
provisioningProfilePath: 'pprofile-2.mobileprovision',
|
||||
distributionCertificate: {
|
||||
path: 'dist-cert-2.p12',
|
||||
password: 'cert-pass-2',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
'./pprofile-1.mobileprovision': 'pprofile-1-somebinarycontent',
|
||||
'./pprofile-2.mobileprovision': 'pprofile-2-somebinarycontent',
|
||||
'./dist-cert-1.p12': 'cert-1-somebinarycontent',
|
||||
'./dist-cert-2.p12': 'cert-2-somebinarycontent',
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
await expect(() =>
|
||||
updateIosCredentialsAsync(ctx, appLookupParams, IosDistributionType.AppStore)
|
||||
).rejects.toThrow(/Updating credentials.json failed/);
|
||||
});
|
||||
it(`should throw an error if there is a typo in credentials.json (because we can't tell if this is a target name or not)`, async () => {
|
||||
const ctx = createCtxMock({
|
||||
ios: {
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testCommonIosAppCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
ios: {
|
||||
// typo!!!
|
||||
xprovisioningProfilePath: 'pprofile-1.mobileprovision',
|
||||
distributionCertificate: {
|
||||
path: 'dist-cert-1.p12',
|
||||
password: 'cert-pass-1',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'./pprofile-1.mobileprovision': 'pprofile-1-somebinarycontent',
|
||||
'./dist-cert-1.p12': 'cert-1-somebinarycontent',
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
await expect(() =>
|
||||
updateIosCredentialsAsync(ctx, appLookupParams, IosDistributionType.AppStore)
|
||||
).rejects.toThrow(/Updating credentials.json failed/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,79 +1,22 @@
|
||||
import Joi from '@hapi/joi';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { Keystore } from '../android/credentials';
|
||||
|
||||
interface CredentialsJsonIosCredentials {
|
||||
provisioningProfilePath: string;
|
||||
distributionCertificate: {
|
||||
path: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
export interface CredentialsJson {
|
||||
android?: {
|
||||
keystore: {
|
||||
keystorePath: string;
|
||||
keystorePassword: string;
|
||||
keyAlias: string;
|
||||
keyPassword?: string;
|
||||
};
|
||||
};
|
||||
ios?: CredentialsJsonIosCredentials | Record<string, CredentialsJsonIosCredentials>;
|
||||
experimental?: {
|
||||
npmToken?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const IosTargetCredentials = Joi.object({
|
||||
provisioningProfilePath: Joi.string().required(),
|
||||
distributionCertificate: Joi.object({
|
||||
path: Joi.string().required(),
|
||||
password: Joi.string().allow('').required(),
|
||||
}).required(),
|
||||
});
|
||||
|
||||
const CredentialsJsonSchema = Joi.object({
|
||||
android: Joi.object({
|
||||
keystore: Joi.object({
|
||||
keystorePath: Joi.string().required(),
|
||||
keystorePassword: Joi.string().allow('').required(),
|
||||
keyAlias: Joi.string().required(),
|
||||
keyPassword: Joi.string().allow(''),
|
||||
}).required(),
|
||||
}),
|
||||
ios: [
|
||||
IosTargetCredentials,
|
||||
Joi.object().pattern(Joi.string().required(), IosTargetCredentials.required()),
|
||||
],
|
||||
experimental: Joi.object({
|
||||
npmToken: Joi.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
interface AndroidCredentials {
|
||||
keystore: Keystore;
|
||||
}
|
||||
|
||||
export interface IosTargetCredentials {
|
||||
provisioningProfile: string;
|
||||
distributionCertificate: {
|
||||
certP12: string;
|
||||
certPassword: string;
|
||||
};
|
||||
}
|
||||
export type IosTargetCredentialsMap = Record<string, IosTargetCredentials>;
|
||||
export type IosCredentials = IosTargetCredentials | IosTargetCredentialsMap;
|
||||
|
||||
export async function fileExistsAsync(projectDir: string): Promise<boolean> {
|
||||
return await fs.pathExists(path.join(projectDir, 'credentials.json'));
|
||||
}
|
||||
import { Target } from '../ios/types';
|
||||
import {
|
||||
AndroidCredentials,
|
||||
CredentialsJson,
|
||||
CredentialsJsonIosCredentials,
|
||||
CredentialsJsonIosTargetCredentials,
|
||||
CredentialsJsonSchema,
|
||||
IosCredentials,
|
||||
IosTargetCredentials,
|
||||
} from './types';
|
||||
import { getCredentialsJsonPath } from './utils';
|
||||
|
||||
export async function readAndroidCredentialsAsync(projectDir: string): Promise<AndroidCredentials> {
|
||||
const credentialsJson = await readAsync(projectDir);
|
||||
if (!credentialsJson.android) {
|
||||
throw new Error('Android credentials are missing from credentials.json');
|
||||
throw new Error('Android credentials are missing in credentials.json');
|
||||
}
|
||||
const keystoreInfo = credentialsJson.android.keystore;
|
||||
return {
|
||||
@@ -86,40 +29,45 @@ export async function readAndroidCredentialsAsync(projectDir: string): Promise<A
|
||||
};
|
||||
}
|
||||
|
||||
export async function readIosCredentialsAsync(projectDir: string): Promise<IosCredentials> {
|
||||
export async function readIosCredentialsAsync(
|
||||
projectDir: string,
|
||||
applicationTarget: Target
|
||||
): Promise<IosCredentials> {
|
||||
const credentialsJson = await readAsync(projectDir);
|
||||
if (!credentialsJson.ios) {
|
||||
throw new Error('iOS credentials are missing from credentials.json');
|
||||
throw new Error('iOS credentials are missing in credentials.json');
|
||||
}
|
||||
|
||||
if (!isInternalCredentialsMap(credentialsJson.ios)) {
|
||||
return await readCredentialsForTargetAsync(projectDir, credentialsJson.ios);
|
||||
} else {
|
||||
if (isCredentialsMap(credentialsJson.ios)) {
|
||||
const targets = Object.keys(credentialsJson.ios);
|
||||
const targetCredentialsMap: IosTargetCredentialsMap = {};
|
||||
const iosCredentials: IosCredentials = {};
|
||||
for (const target of targets) {
|
||||
targetCredentialsMap[target] = await readCredentialsForTargetAsync(
|
||||
iosCredentials[target] = await readCredentialsForTargetAsync(
|
||||
projectDir,
|
||||
credentialsJson.ios[target]
|
||||
);
|
||||
}
|
||||
return targetCredentialsMap;
|
||||
return iosCredentials;
|
||||
} else {
|
||||
const applicationTargetCredentials = await readCredentialsForTargetAsync(
|
||||
projectDir,
|
||||
credentialsJson.ios
|
||||
);
|
||||
return {
|
||||
[applicationTarget.targetName]: applicationTargetCredentials,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function isInternalCredentialsMap(
|
||||
ios: CredentialsJsonIosCredentials | Record<string, CredentialsJsonIosCredentials>
|
||||
): ios is Record<string, CredentialsJsonIosCredentials> {
|
||||
function isCredentialsMap(
|
||||
ios: CredentialsJsonIosTargetCredentials | CredentialsJsonIosCredentials
|
||||
): ios is CredentialsJsonIosCredentials {
|
||||
return typeof ios.provisioningProfilePath !== 'string';
|
||||
}
|
||||
|
||||
export function isCredentialsMap(ios: IosCredentials): ios is IosTargetCredentialsMap {
|
||||
return typeof ios.provisioningProfile !== 'string';
|
||||
}
|
||||
|
||||
async function readCredentialsForTargetAsync(
|
||||
projectDir: string,
|
||||
targetCredentials: CredentialsJsonIosCredentials
|
||||
targetCredentials: CredentialsJsonIosTargetCredentials
|
||||
): Promise<IosTargetCredentials> {
|
||||
return {
|
||||
provisioningProfile: await fs.readFile(
|
||||
@@ -127,26 +75,15 @@ async function readCredentialsForTargetAsync(
|
||||
'base64'
|
||||
),
|
||||
distributionCertificate: {
|
||||
certP12: await fs.readFile(
|
||||
certificateP12: await fs.readFile(
|
||||
getAbsolutePath(projectDir, targetCredentials.distributionCertificate.path),
|
||||
'base64'
|
||||
),
|
||||
certPassword: targetCredentials.distributionCertificate.password,
|
||||
certificatePassword: targetCredentials.distributionCertificate.password,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function readEnvironmentSecretsAsync(
|
||||
projectDir: string
|
||||
): Promise<Record<string, string> | undefined> {
|
||||
if (!(await fileExistsAsync(projectDir))) {
|
||||
return undefined;
|
||||
}
|
||||
const credentialsJson = await readAsync(projectDir);
|
||||
const npmToken = credentialsJson?.experimental?.npmToken;
|
||||
return npmToken ? { NPM_TOKEN: npmToken } : undefined;
|
||||
}
|
||||
|
||||
async function readAsync(projectDir: string): Promise<CredentialsJson> {
|
||||
const credentialsJSONRaw = await readRawAsync(projectDir);
|
||||
|
||||
@@ -162,8 +99,18 @@ async function readAsync(projectDir: string): Promise<CredentialsJson> {
|
||||
return credentialsJson;
|
||||
}
|
||||
|
||||
export async function readRawAsync(projectDir: string): Promise<any> {
|
||||
const credentialsJsonFilePath = path.join(projectDir, 'credentials.json');
|
||||
export async function readRawAsync(
|
||||
projectDir: string,
|
||||
{ throwIfMissing = true } = {}
|
||||
): Promise<any> {
|
||||
const credentialsJsonFilePath = getCredentialsJsonPath(projectDir);
|
||||
if (!(await fs.pathExists(credentialsJsonFilePath))) {
|
||||
if (throwIfMissing) {
|
||||
throw new Error('credentials.json does not exist in the project root directory');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const credentialsJSONContents = await fs.readFile(credentialsJsonFilePath, 'utf8');
|
||||
return JSON.parse(credentialsJSONContents);
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import Joi from '@hapi/joi';
|
||||
|
||||
import { Keystore } from '../android/credentials';
|
||||
|
||||
export interface CredentialsJson {
|
||||
android?: CredentialsJsonAndroidCredentials;
|
||||
ios?: CredentialsJsonIosTargetCredentials | CredentialsJsonIosCredentials;
|
||||
}
|
||||
|
||||
export interface CredentialsJsonAndroidCredentials {
|
||||
keystore: {
|
||||
keystorePath: string;
|
||||
keystorePassword: string;
|
||||
keyAlias: string;
|
||||
keyPassword?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CredentialsJsonIosTargetCredentials {
|
||||
provisioningProfilePath: string;
|
||||
distributionCertificate: {
|
||||
path: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
export type CredentialsJsonIosCredentials = Record<string, CredentialsJsonIosTargetCredentials>;
|
||||
|
||||
export interface AndroidCredentials {
|
||||
keystore: Keystore;
|
||||
}
|
||||
|
||||
export interface IosTargetCredentials {
|
||||
provisioningProfile: string;
|
||||
distributionCertificate: {
|
||||
certificateP12: string;
|
||||
certificatePassword: string;
|
||||
};
|
||||
}
|
||||
export type IosCredentials = Record<string, IosTargetCredentials>;
|
||||
|
||||
const CredentialsJsonIosTargetCredentialsSchema = Joi.object({
|
||||
provisioningProfilePath: Joi.string().required(),
|
||||
distributionCertificate: Joi.object({
|
||||
path: Joi.string().required(),
|
||||
password: Joi.string().allow('').required(),
|
||||
}).required(),
|
||||
});
|
||||
|
||||
export const CredentialsJsonSchema = Joi.object({
|
||||
android: Joi.object({
|
||||
keystore: Joi.object({
|
||||
keystorePath: Joi.string().required(),
|
||||
keystorePassword: Joi.string().allow('').required(),
|
||||
keyAlias: Joi.string().required(),
|
||||
keyPassword: Joi.string().allow(''),
|
||||
}).required(),
|
||||
}),
|
||||
ios: [
|
||||
CredentialsJsonIosTargetCredentialsSchema,
|
||||
Joi.object().pattern(
|
||||
Joi.string().required(),
|
||||
CredentialsJsonIosTargetCredentialsSchema.required()
|
||||
),
|
||||
],
|
||||
});
|
||||
@@ -1,56 +1,41 @@
|
||||
import fs from 'fs-extra';
|
||||
import zipObject from 'lodash/zipObject';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
|
||||
import { IosDistributionType as IosDistributionTypeGraphql } from '../../graphql/generated';
|
||||
import { AndroidAppBuildCredentialsFragment, IosDistributionType } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { getProjectAccountName } from '../../project/projectUtils';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import { gitStatusAsync } from '../../utils/git';
|
||||
import { findApplicationTarget, findTargetByName } from '../../project/ios/target';
|
||||
import vcs from '../../vcs';
|
||||
import GitClient from '../../vcs/git';
|
||||
import { Context } from '../context';
|
||||
import { AppLookupParams } from '../ios/api/GraphqlClient';
|
||||
import { CredentialsJson } from './read';
|
||||
import { App, Target, TargetCredentials } from '../ios/types';
|
||||
import { readRawAsync } from './read';
|
||||
import {
|
||||
CredentialsJson,
|
||||
CredentialsJsonIosCredentials,
|
||||
CredentialsJsonIosTargetCredentials,
|
||||
} from './types';
|
||||
import { getCredentialsJsonPath } from './utils';
|
||||
|
||||
/**
|
||||
* Update Android credentials.json with values from www, content of credentials.json
|
||||
* is not validated
|
||||
*/
|
||||
export async function updateAndroidCredentialsAsync(ctx: Context): Promise<void> {
|
||||
const credentialsJsonFilePath = path.join(ctx.projectDir, 'credentials.json');
|
||||
let rawCredentialsJsonObject: any = {};
|
||||
if (await fs.pathExists(credentialsJsonFilePath)) {
|
||||
try {
|
||||
// reading raw file without validation (used only to access keystorePath)
|
||||
const rawFile = await fs.readFile(credentialsJsonFilePath, 'utf-8');
|
||||
rawCredentialsJsonObject = JSON.parse(rawFile);
|
||||
} catch (error) {
|
||||
Log.error(`Reading credentials.json failed [${error}]`);
|
||||
Log.error('Make sure that file is correct (or remove it) and rerun this command.');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const accountName = await getProjectAccountName(ctx.exp, ctx.user);
|
||||
const experienceName = `@${accountName}/${ctx.exp.slug}`;
|
||||
const keystore = await ctx.android.fetchKeystoreAsync(experienceName);
|
||||
export async function updateAndroidCredentialsAsync(
|
||||
ctx: Context,
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment
|
||||
): Promise<void> {
|
||||
const rawCredentialsJson: any =
|
||||
(await readRawAsync(ctx.projectDir, { throwIfMissing: false })) ?? {};
|
||||
|
||||
const keystore = buildCredentials.androidKeystore;
|
||||
if (!keystore) {
|
||||
throw new Error('There are no credentials configured for this project on EAS servers');
|
||||
}
|
||||
|
||||
const isKeystoreComplete =
|
||||
keystore.keystore && keystore.keystorePassword && keystore.keyPassword && keystore.keyAlias;
|
||||
|
||||
if (!isKeystoreComplete) {
|
||||
const confirm = await confirmAsync({
|
||||
message:
|
||||
'Credentials on EAS servers might be invalid or incomplete. Are you sure you want to continue?',
|
||||
});
|
||||
if (!confirm) {
|
||||
Log.warn('Aborting...');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const keystorePath =
|
||||
rawCredentialsJsonObject?.android?.keystore?.keystorePath ?? 'android/keystores/keystore.jks';
|
||||
rawCredentialsJson?.android?.keystore?.keystorePath ?? 'credentials/android/keystore.jks';
|
||||
Log.log(`Writing Keystore to ${keystorePath}`);
|
||||
await updateFileAsync(ctx.projectDir, keystorePath, keystore.keystore);
|
||||
const shouldWarnKeystore = await isFileUntrackedAsync(keystorePath);
|
||||
@@ -60,11 +45,11 @@ export async function updateAndroidCredentialsAsync(ctx: Context): Promise<void>
|
||||
keystorePath,
|
||||
keystorePassword: keystore.keystorePassword,
|
||||
keyAlias: keystore.keyAlias,
|
||||
keyPassword: keystore.keyPassword,
|
||||
keyPassword: keystore.keyPassword ?? undefined,
|
||||
},
|
||||
};
|
||||
rawCredentialsJsonObject.android = androidCredentials;
|
||||
await fs.writeJson(credentialsJsonFilePath, rawCredentialsJsonObject, {
|
||||
rawCredentialsJson.android = androidCredentials;
|
||||
await fs.writeJson(getCredentialsJsonPath(ctx.projectDir), rawCredentialsJson, {
|
||||
spaces: 2,
|
||||
});
|
||||
const shouldWarnCredentialsJson = await isFileUntrackedAsync('credentials.json');
|
||||
@@ -80,124 +65,224 @@ export async function updateAndroidCredentialsAsync(ctx: Context): Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Update iOS credentials in credentials.json with values from www, content
|
||||
* of credentials.json is not validated, if www has incomplete credentials
|
||||
* Update iOS credentials in credentials.json with values from www, contents
|
||||
* of credentials.json are not validated, if www has incomplete credentials
|
||||
* credentials.json will be updated partially
|
||||
*/
|
||||
export async function updateIosCredentialsAsync(
|
||||
ctx: Context,
|
||||
appLookupParams: AppLookupParams,
|
||||
iosDistributionTypeGraphql: IosDistributionTypeGraphql
|
||||
app: App,
|
||||
targets: Target[],
|
||||
distributionType: IosDistributionType
|
||||
): Promise<void> {
|
||||
const credentialsJsonFilePath = path.join(ctx.projectDir, 'credentials.json');
|
||||
let rawCredentialsJsonObject: any = {};
|
||||
if (await fs.pathExists(credentialsJsonFilePath)) {
|
||||
try {
|
||||
// reading raw file without validation (used only to access paths)
|
||||
const rawFile = await fs.readFile(credentialsJsonFilePath, 'utf-8');
|
||||
rawCredentialsJsonObject = JSON.parse(rawFile);
|
||||
} catch (error) {
|
||||
Log.error(`There was an error while reading credentials.json [${error}]`);
|
||||
Log.error('Make sure that file is correct (or remove it) and rerun this command.');
|
||||
throw error;
|
||||
}
|
||||
|
||||
// TODO: implement updating credentials.json with multi-target credentials from EAS servers
|
||||
if (rawCredentialsJsonObject?.ios) {
|
||||
const keys = Object.keys(rawCredentialsJsonObject.ios);
|
||||
const maybeUnknownKey = keys.find(
|
||||
key => key !== 'provisioningProfilePath' && key !== 'distributionCertificate'
|
||||
);
|
||||
if (maybeUnknownKey) {
|
||||
Log.error(
|
||||
`It looks like your credentials.json either contains multi-target credentials or you've made a typo.`
|
||||
);
|
||||
Log.error(`- The key in 'ios' object that EAS CLI encountered is: ${maybeUnknownKey}`);
|
||||
Log.error(
|
||||
`- Updating credentials.json (for multi-target iOS projects) with credentials stored on EAS servers is not supported at the moment, sorry!`
|
||||
);
|
||||
throw new Error('Updating credentials.json failed');
|
||||
}
|
||||
}
|
||||
const rawCredentialsJson: any =
|
||||
(await readRawAsync(ctx.projectDir, { throwIfMissing: false })) ?? {};
|
||||
if (typeof rawCredentialsJson.ios?.provisioningProfilePath === 'string') {
|
||||
const applicationTarget = findApplicationTarget(targets);
|
||||
rawCredentialsJson.ios = {
|
||||
[applicationTarget.targetName]: rawCredentialsJson.ios,
|
||||
};
|
||||
}
|
||||
|
||||
const profilePath =
|
||||
rawCredentialsJsonObject?.ios?.provisioningProfilePath ?? 'ios/certs/profile.mobileprovision';
|
||||
const distCertPath =
|
||||
rawCredentialsJsonObject?.ios?.distributionCertificate?.path ?? 'ios/certs/dist-cert.p12';
|
||||
const iosAppCredentials = await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(
|
||||
appLookupParams
|
||||
const targetBuildCredentialsList = await Promise.all(
|
||||
targets.map(target => getTargetBuildCredentialsAsync(ctx, app, target, distributionType))
|
||||
);
|
||||
const targetBuildsCredentialsMap = zipObject(
|
||||
targets.map(({ targetName }) => targetName),
|
||||
targetBuildCredentialsList
|
||||
);
|
||||
const buildCredentials =
|
||||
iosAppCredentials?.iosAppBuildCredentialsList.find(
|
||||
buildCredentials => buildCredentials.iosDistributionType === iosDistributionTypeGraphql
|
||||
) ?? null;
|
||||
if (!buildCredentials) {
|
||||
throw new Error(
|
||||
`There are no credentials configured for the ${iosDistributionTypeGraphql} distribution of this project on EAS servers`
|
||||
);
|
||||
}
|
||||
|
||||
const areCredentialsComplete =
|
||||
buildCredentials.provisioningProfile?.provisioningProfile &&
|
||||
buildCredentials.distributionCertificate?.certificateP12 &&
|
||||
buildCredentials.distributionCertificate.certificatePassword;
|
||||
|
||||
if (!areCredentialsComplete) {
|
||||
const confirm = await confirmAsync({
|
||||
message:
|
||||
'Credentials on EAS servers might be invalid or incomplete. Are you sure you want to continue?',
|
||||
});
|
||||
if (!confirm) {
|
||||
Log.warn('Aborting...');
|
||||
return;
|
||||
let areAllTargetsConfigured = true;
|
||||
const notConfiguredTargetLabels: string[] = [];
|
||||
for (const [targetName, targetAppBuildCredentials] of Object.entries(
|
||||
targetBuildsCredentialsMap
|
||||
)) {
|
||||
if (!targetAppBuildCredentials) {
|
||||
areAllTargetsConfigured = false;
|
||||
const { bundleIdentifier } = findTargetByName(targets, targetName);
|
||||
notConfiguredTargetLabels.push(`${targetName} (Bundle Identifier: ${bundleIdentifier})`);
|
||||
}
|
||||
}
|
||||
const provisioningProfile =
|
||||
buildCredentials.provisioningProfile?.provisioningProfile ?? undefined;
|
||||
const distributionCertificateP12 =
|
||||
buildCredentials.distributionCertificate?.certificateP12 ?? undefined;
|
||||
const distributionCertificatePassword =
|
||||
buildCredentials.distributionCertificate?.certificatePassword ?? undefined;
|
||||
Log.log(`Writing Provisioning Profile to ${profilePath}`);
|
||||
await updateFileAsync(ctx.projectDir, profilePath, provisioningProfile);
|
||||
const shouldWarnPProfile = await isFileUntrackedAsync(profilePath);
|
||||
|
||||
Log.log(`Writing Distribution Certificate to ${distCertPath}`);
|
||||
await updateFileAsync(ctx.projectDir, distCertPath, distributionCertificateP12);
|
||||
const shouldWarnDistCert = await isFileUntrackedAsync(distCertPath);
|
||||
if (!areAllTargetsConfigured) {
|
||||
const errorMessage =
|
||||
targets.length === 1
|
||||
? `There are no credentials configured for the ${distributionType} distribution of this project on EAS servers`
|
||||
: `Some of the build targets don't have credentials configured for the ${distributionType} distribution of this project on EAS servers: ${notConfiguredTargetLabels}`;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const iosCredentials: Partial<CredentialsJson['ios']> = {
|
||||
...(provisioningProfile ? { provisioningProfilePath: profilePath } : {}),
|
||||
...(distributionCertificateP12 && distributionCertificatePassword
|
||||
? {
|
||||
distributionCertificate: {
|
||||
path: distCertPath,
|
||||
password: distributionCertificatePassword,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
rawCredentialsJsonObject.ios = iosCredentials;
|
||||
await fs.writeJson(credentialsJsonFilePath, rawCredentialsJsonObject, {
|
||||
const iosCredentials: CredentialsJsonIosCredentials = {};
|
||||
const targetCredentialsPathsMap = createTargetCredentialsPathsMap(
|
||||
targets,
|
||||
rawCredentialsJson.ios
|
||||
);
|
||||
for (const target of targets) {
|
||||
iosCredentials[target.targetName] = await backupTargetCredentialsAsync(ctx, {
|
||||
// app build credentials must exist for target because otherwise an error must have been thrown earlier
|
||||
targetCredentials: nullthrows(targetBuildsCredentialsMap[target.targetName]),
|
||||
targetCredentialsPaths: targetCredentialsPathsMap[target.targetName],
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.keys(iosCredentials).length === 1) {
|
||||
rawCredentialsJson.ios = iosCredentials[Object.keys(iosCredentials)[0]];
|
||||
} else {
|
||||
rawCredentialsJson.ios = iosCredentials;
|
||||
}
|
||||
|
||||
await fs.writeJson(getCredentialsJsonPath(ctx.projectDir), rawCredentialsJson, {
|
||||
spaces: 2,
|
||||
});
|
||||
const shouldWarnCredentialsJson = await isFileUntrackedAsync('credentials.json');
|
||||
|
||||
const newFilePaths = [];
|
||||
if (shouldWarnPProfile) {
|
||||
newFilePaths.push(profilePath);
|
||||
for (const [, targetCredentials] of Object.entries(iosCredentials)) {
|
||||
if (await isFileUntrackedAsync(targetCredentials.distributionCertificate.path)) {
|
||||
newFilePaths.push(targetCredentials.distributionCertificate.path);
|
||||
}
|
||||
if (await isFileUntrackedAsync(targetCredentials.provisioningProfilePath)) {
|
||||
newFilePaths.push(targetCredentials.provisioningProfilePath);
|
||||
}
|
||||
}
|
||||
if (shouldWarnDistCert) {
|
||||
newFilePaths.push(distCertPath);
|
||||
}
|
||||
if (shouldWarnCredentialsJson) {
|
||||
if (await isFileUntrackedAsync('credentials.json')) {
|
||||
newFilePaths.push('credentials.json');
|
||||
}
|
||||
displayUntrackedFilesWarning(newFilePaths);
|
||||
}
|
||||
|
||||
async function updateFileAsync(projectDir: string, filePath: string, base64Data?: string) {
|
||||
interface TargetCredentialsPaths {
|
||||
provisioningProfilePath: string;
|
||||
distCertPath: string;
|
||||
}
|
||||
type TargetCredentialsPathsMap = Record<string, TargetCredentialsPaths>;
|
||||
function createTargetCredentialsPathsMap(
|
||||
targets: Target[],
|
||||
rawCredentialsJsonMap?: any
|
||||
): TargetCredentialsPathsMap {
|
||||
const hasManyTargets = targets.length > 1;
|
||||
const paths: TargetCredentialsPathsMap = {};
|
||||
|
||||
// 1. Create initial target credentials paths map
|
||||
for (const target of targets) {
|
||||
const rawTargetCredentialsJson = rawCredentialsJsonMap?.[target.targetName];
|
||||
const filePrefix = hasManyTargets ? `${target.targetName}-` : '';
|
||||
|
||||
paths[target.targetName] = {
|
||||
provisioningProfilePath:
|
||||
rawTargetCredentialsJson?.provisioningProfilePath ??
|
||||
`credentials/ios/${filePrefix}profile.mobileprovision`,
|
||||
distCertPath:
|
||||
rawTargetCredentialsJson?.distributionCertificate?.path ??
|
||||
`credentials/ios/${filePrefix}dist-cert.p12`,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Look for duplicates and prefix them with target names
|
||||
const deduplicatedPaths: TargetCredentialsPathsMap = {};
|
||||
const usedProfilePaths = new Set<string>();
|
||||
const usedDistCertPaths = new Set<string>();
|
||||
for (const [targetName, { provisioningProfilePath, distCertPath }] of Object.entries(paths)) {
|
||||
const newProvisioningProfilePath = usedProfilePaths.has(provisioningProfilePath)
|
||||
? path.join(
|
||||
path.dirname(provisioningProfilePath),
|
||||
`${targetName}-${path.basename(provisioningProfilePath)}`
|
||||
)
|
||||
: provisioningProfilePath;
|
||||
usedProfilePaths.add(newProvisioningProfilePath);
|
||||
|
||||
const newDistCertPath = usedDistCertPaths.has(distCertPath)
|
||||
? path.join(path.dirname(distCertPath), `${targetName}-${path.basename(distCertPath)}`)
|
||||
: distCertPath;
|
||||
usedDistCertPaths.add(newDistCertPath);
|
||||
|
||||
deduplicatedPaths[targetName] = {
|
||||
distCertPath: newDistCertPath,
|
||||
provisioningProfilePath: newProvisioningProfilePath,
|
||||
};
|
||||
}
|
||||
|
||||
return deduplicatedPaths;
|
||||
}
|
||||
|
||||
async function getTargetBuildCredentialsAsync(
|
||||
ctx: Context,
|
||||
app: App,
|
||||
target: Target,
|
||||
iosDistributionType: IosDistributionType
|
||||
): Promise<TargetCredentials | null> {
|
||||
const appCredentials = await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync({
|
||||
account: app.account,
|
||||
projectName: app.projectName,
|
||||
bundleIdentifier: target.bundleIdentifier,
|
||||
parentBundleIdentifier: target.parentBundleIdentifier,
|
||||
});
|
||||
const appBuildCredentials =
|
||||
appCredentials?.iosAppBuildCredentialsList.find(
|
||||
appBuildCredentials => appBuildCredentials.iosDistributionType === iosDistributionType
|
||||
) ?? null;
|
||||
if (appBuildCredentials === null) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
!(
|
||||
appBuildCredentials.provisioningProfile?.provisioningProfile !== undefined &&
|
||||
appBuildCredentials.distributionCertificate?.certificateP12 !== undefined &&
|
||||
appBuildCredentials.distributionCertificate?.certificatePassword !== undefined
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
distributionCertificate: {
|
||||
certificateP12: nullthrows(appBuildCredentials.distributionCertificate.certificateP12),
|
||||
certificatePassword: nullthrows(
|
||||
appBuildCredentials.distributionCertificate.certificatePassword
|
||||
),
|
||||
},
|
||||
provisioningProfile: nullthrows(appBuildCredentials.provisioningProfile.provisioningProfile),
|
||||
};
|
||||
}
|
||||
|
||||
async function backupTargetCredentialsAsync(
|
||||
ctx: Context,
|
||||
{
|
||||
targetCredentials,
|
||||
targetCredentialsPaths,
|
||||
}: {
|
||||
targetCredentials: TargetCredentials;
|
||||
targetCredentialsPaths: TargetCredentialsPaths;
|
||||
}
|
||||
): Promise<CredentialsJsonIosTargetCredentials> {
|
||||
const { provisioningProfilePath, distCertPath } = targetCredentialsPaths;
|
||||
|
||||
Log.log(`Writing Provisioning Profile to ${provisioningProfilePath}`);
|
||||
await updateFileAsync(
|
||||
ctx.projectDir,
|
||||
provisioningProfilePath,
|
||||
targetCredentials.provisioningProfile
|
||||
);
|
||||
|
||||
Log.log(`Writing Distribution Certificate to ${distCertPath}`);
|
||||
await updateFileAsync(
|
||||
ctx.projectDir,
|
||||
distCertPath,
|
||||
targetCredentials.distributionCertificate.certificateP12
|
||||
);
|
||||
|
||||
return {
|
||||
distributionCertificate: {
|
||||
path: distCertPath,
|
||||
password: targetCredentials.distributionCertificate.certificatePassword,
|
||||
},
|
||||
provisioningProfilePath,
|
||||
};
|
||||
}
|
||||
|
||||
async function updateFileAsync(
|
||||
projectDir: string,
|
||||
filePath: string,
|
||||
base64Data?: string
|
||||
): Promise<void> {
|
||||
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
|
||||
if (await fs.pathExists(absolutePath)) {
|
||||
await fs.remove(absolutePath);
|
||||
@@ -209,25 +294,22 @@ async function updateFileAsync(projectDir: string, filePath: string, base64Data?
|
||||
}
|
||||
|
||||
async function isFileUntrackedAsync(path: string): Promise<boolean> {
|
||||
const withUntrackedFiles = await gitStatusAsync({ showUntracked: true });
|
||||
const trackedFiles = await gitStatusAsync({ showUntracked: false });
|
||||
const pathWithoutLeadingDot = path.replace(/^\.\//, ''); // remove leading './' from path
|
||||
return (
|
||||
withUntrackedFiles.includes(pathWithoutLeadingDot) &&
|
||||
!trackedFiles.includes(pathWithoutLeadingDot)
|
||||
);
|
||||
if (vcs instanceof GitClient) {
|
||||
return await vcs.isFileUntrackedAsync(path);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function displayUntrackedFilesWarning(newFilePaths: string[]) {
|
||||
if (newFilePaths.length === 1) {
|
||||
Log.warn(
|
||||
`File ${newFilePaths[0]} is currently untracked, remember to add it to .gitignore or to encrypt it. (e.g. with git-crypt)`
|
||||
`File ${newFilePaths[0]} is currently untracked, remember to add it to .gitignore, or to encrypt it (e.g. with git-crypt).`
|
||||
);
|
||||
} else if (newFilePaths.length > 1) {
|
||||
Log.warn(
|
||||
`Files ${newFilePaths.join(
|
||||
', '
|
||||
)} are currently untracked, remember to add them to .gitignore or to encrypt them. (e.g. with git-crypt)`
|
||||
)} are currently untracked, remember to add them to .gitignore, or to encrypt them (e.g. with git-crypt).`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import path from 'path';
|
||||
|
||||
import { Target } from '../ios/types';
|
||||
import { IosCredentials } from './types';
|
||||
|
||||
export function getCredentialsJsonPath(projectDir: string): string {
|
||||
return path.join(projectDir, 'credentials.json');
|
||||
}
|
||||
|
||||
export function ensureAllTargetsAreConfigured(
|
||||
targets: Target[],
|
||||
credentialsJson: IosCredentials
|
||||
): void {
|
||||
const notConfiguredTargets: string[] = [];
|
||||
for (const target of targets) {
|
||||
if (!(target.targetName in credentialsJson)) {
|
||||
notConfiguredTargets.push(target.targetName);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (notConfiguredTargets.length > 0) {
|
||||
const errorMessage =
|
||||
targets.length === 1
|
||||
? 'Credentials are not defined in credentials.json'
|
||||
: `Credentials for target${
|
||||
notConfiguredTargets.length === 1 ? '' : 's'
|
||||
} ${notConfiguredTargets
|
||||
.map(i => `'${i}'`)
|
||||
.join(',')} are not defined in credentials.json`;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,20 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource, IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
|
||||
import { CredentialsManager } from '../CredentialsManager';
|
||||
import { findApplicationTarget } from '../../project/ios/target';
|
||||
import { Context } from '../context';
|
||||
import * as credentialsJsonReader from '../credentialsJson/read';
|
||||
import type { IosCredentials } from '../credentialsJson/read';
|
||||
import { ensureAllTargetsAreConfigured } from '../credentialsJson/utils';
|
||||
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
|
||||
import { AppLookupParams } from './api/GraphqlClient';
|
||||
import { IosCapabilitiesOptions } from './appstore/ensureAppExists';
|
||||
import { App, IosCredentials, Target } from './types';
|
||||
import { isAdHocProfile } from './utils/provisioningProfile';
|
||||
|
||||
export type { IosCredentials };
|
||||
|
||||
interface Options {
|
||||
app: AppLookupParams;
|
||||
app: App;
|
||||
targets: Target[];
|
||||
distribution: IosDistributionType;
|
||||
enterpriseProvisioning?: IosEnterpriseProvisioning;
|
||||
skipCredentialsCheck?: boolean;
|
||||
iosCapabilitiesOptions?: IosCapabilitiesOptions;
|
||||
}
|
||||
|
||||
@@ -36,19 +34,33 @@ export default class IosCredentialsProvider {
|
||||
}
|
||||
|
||||
private async getLocalAsync(): Promise<IosCredentials> {
|
||||
const credentials = await credentialsJsonReader.readIosCredentialsAsync(this.ctx.projectDir);
|
||||
if (credentialsJsonReader.isCredentialsMap(credentials)) {
|
||||
for (const targetName of Object.keys(credentials)) {
|
||||
this.assertProvisioningProfileType(credentials[targetName].provisioningProfile, targetName);
|
||||
}
|
||||
} else {
|
||||
this.assertProvisioningProfileType(credentials.provisioningProfile);
|
||||
const applicationTarget = findApplicationTarget(this.options.targets);
|
||||
const iosCredentials = await credentialsJsonReader.readIosCredentialsAsync(
|
||||
this.ctx.projectDir,
|
||||
applicationTarget
|
||||
);
|
||||
ensureAllTargetsAreConfigured(this.options.targets, iosCredentials);
|
||||
for (const target of this.options.targets) {
|
||||
this.assertProvisioningProfileType(
|
||||
iosCredentials[target.targetName].provisioningProfile,
|
||||
target.targetName
|
||||
);
|
||||
}
|
||||
return credentials;
|
||||
return iosCredentials;
|
||||
}
|
||||
|
||||
private assertProvisioningProfileType(provisionigProfile: string, targetName?: string) {
|
||||
const isAdHoc = isAdHocProfile(provisionigProfile);
|
||||
private async getRemoteAsync(): Promise<IosCredentials> {
|
||||
return await new SetupBuildCredentials({
|
||||
app: this.options.app,
|
||||
targets: this.options.targets,
|
||||
distribution: this.options.distribution,
|
||||
enterpriseProvisioning: this.options.enterpriseProvisioning,
|
||||
iosCapabilitiesOptions: this.options.iosCapabilitiesOptions,
|
||||
}).runAsync(this.ctx);
|
||||
}
|
||||
|
||||
private assertProvisioningProfileType(provisioningProfile: string, targetName?: string): void {
|
||||
const isAdHoc = isAdHocProfile(provisioningProfile);
|
||||
if (this.options.distribution === 'internal' && !isAdHoc) {
|
||||
throw new Error(
|
||||
`You must use an adhoc provisioning profile${
|
||||
@@ -63,21 +75,4 @@ export default class IosCredentialsProvider {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async getRemoteAsync(): Promise<IosCredentials> {
|
||||
const manager = new CredentialsManager(this.ctx);
|
||||
const { provisioningProfile, distributionCertificate } = await new SetupBuildCredentials({
|
||||
app: this.options.app,
|
||||
distribution: this.options.distribution,
|
||||
enterpriseProvisioning: this.options.enterpriseProvisioning,
|
||||
iosCapabilitiesOptions: this.options.iosCapabilitiesOptions,
|
||||
}).runAsync(manager, this.ctx);
|
||||
return {
|
||||
provisioningProfile,
|
||||
distributionCertificate: {
|
||||
certP12: distributionCertificate.certificateP12,
|
||||
certPassword: distributionCertificate.certificatePassword,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@ import { CredentialsSource } from '@expo/eas-json';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { IosAppBuildCredentialsFragment } from '../../../graphql/generated';
|
||||
import { findApplicationTarget } from '../../../project/ios/target';
|
||||
import { getAppstoreMock } from '../../__tests__/fixtures-appstore';
|
||||
import { createCtxMock } from '../../__tests__/fixtures-context';
|
||||
import {
|
||||
getNewIosApiMockWithoutCredentials,
|
||||
testIosAppCredentialsWithBuildCredentialsQueryResult,
|
||||
getNewIosApiMock,
|
||||
testCommonIosAppCredentialsFragment,
|
||||
testTargets,
|
||||
} from '../../__tests__/fixtures-ios';
|
||||
import IosCredentialsProvider from '../IosCredentialsProvider';
|
||||
import { getAppLookupParamsFromContext } from '../actions/BuildCredentialsUtils';
|
||||
@@ -54,20 +56,23 @@ describe(IosCredentialsProvider, () => {
|
||||
appStore: getAppstoreMock(),
|
||||
projectDir: '/app',
|
||||
ios: {
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
...getNewIosApiMock(),
|
||||
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(() => null),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const appLookupParams = getAppLookupParamsFromContext(
|
||||
ctx,
|
||||
findApplicationTarget(testTargets)
|
||||
);
|
||||
const provider = new IosCredentialsProvider(ctx, {
|
||||
app: {
|
||||
account: {
|
||||
id: `id-${appLookupParams.account.name}`,
|
||||
name: appLookupParams.account.name,
|
||||
},
|
||||
bundleIdentifier: appLookupParams.bundleIdentifier,
|
||||
projectName: appLookupParams.projectName,
|
||||
},
|
||||
targets: [{ targetName: 'testapp', bundleIdentifier: appLookupParams.bundleIdentifier }],
|
||||
distribution: 'store',
|
||||
});
|
||||
await expect(provider.getCredentialsAsync(CredentialsSource.REMOTE)).rejects.toThrowError(
|
||||
@@ -81,39 +86,43 @@ describe(IosCredentialsProvider, () => {
|
||||
appStore: getAppstoreMock(),
|
||||
projectDir: '/app',
|
||||
ios: {
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
...getNewIosApiMock(),
|
||||
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
|
||||
() => testIosAppCredentialsWithBuildCredentialsQueryResult
|
||||
() => testCommonIosAppCredentialsFragment
|
||||
),
|
||||
getDistributionCertificateForAppAsync: jest.fn(
|
||||
() =>
|
||||
testIosAppCredentialsWithBuildCredentialsQueryResult.iosAppBuildCredentialsList[0]
|
||||
testCommonIosAppCredentialsFragment.iosAppBuildCredentialsList[0]
|
||||
.distributionCertificate
|
||||
),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const appLookupParams = getAppLookupParamsFromContext(
|
||||
ctx,
|
||||
findApplicationTarget(testTargets)
|
||||
);
|
||||
const provider = new IosCredentialsProvider(ctx, {
|
||||
app: {
|
||||
account: {
|
||||
id: `id-${appLookupParams.account.name}`,
|
||||
name: appLookupParams.account.name,
|
||||
},
|
||||
bundleIdentifier: appLookupParams.bundleIdentifier,
|
||||
projectName: appLookupParams.projectName,
|
||||
},
|
||||
targets: [{ targetName: 'testapp', bundleIdentifier: appLookupParams.bundleIdentifier }],
|
||||
distribution: 'store',
|
||||
});
|
||||
|
||||
const buildCredentials =
|
||||
testIosAppCredentialsWithBuildCredentialsQueryResult.iosAppBuildCredentialsList[0];
|
||||
const buildCredentials = testCommonIosAppCredentialsFragment.iosAppBuildCredentialsList[0];
|
||||
await expect(provider.getCredentialsAsync(CredentialsSource.REMOTE)).resolves.toMatchObject(
|
||||
{
|
||||
distributionCertificate: {
|
||||
certP12: buildCredentials.distributionCertificate?.certificateP12,
|
||||
certPassword: buildCredentials.distributionCertificate?.certificatePassword,
|
||||
testapp: {
|
||||
distributionCertificate: {
|
||||
certificateP12: buildCredentials.distributionCertificate?.certificateP12,
|
||||
certificatePassword: buildCredentials.distributionCertificate?.certificatePassword,
|
||||
},
|
||||
provisioningProfile: buildCredentials.provisioningProfile?.provisioningProfile,
|
||||
},
|
||||
provisioningProfile: buildCredentials.provisioningProfile?.provisioningProfile,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -14,3 +14,7 @@ export async function resolveAppleTeamIfAuthenticatedAsync(
|
||||
appleTeamName: ctx.appStore.authCtx.team.name,
|
||||
});
|
||||
}
|
||||
|
||||
export function formatAppleTeam({ appleTeamIdentifier, appleTeamName }: AppleTeamFragment): string {
|
||||
return `Team ID: ${appleTeamIdentifier}${appleTeamName ? `, Team name: ${appleTeamName}` : ''}`;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ApplePushKeyFragment, CommonIosAppCredentialsFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Context } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
|
||||
|
||||
export class AssignPushKey {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
public async runAsync(
|
||||
ctx: Context,
|
||||
pushKey: ApplePushKeyFragment
|
||||
): Promise<CommonIosAppCredentialsFragment> {
|
||||
const appleTeam =
|
||||
(await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app)) ?? pushKey.appleTeam ?? null;
|
||||
const appCredentials = await ctx.ios.createOrGetIosAppCredentialsWithCommonFieldsAsync(
|
||||
this.app,
|
||||
{ appleTeam: appleTeam ?? undefined }
|
||||
);
|
||||
const updatedAppCredentials = await ctx.ios.updateIosAppCredentialsAsync(appCredentials, {
|
||||
applePushKeyId: pushKey.id,
|
||||
});
|
||||
Log.succeed(`Push Key assigned to ${this.app.projectName}: ${this.app.bundleIdentifier}`);
|
||||
return updatedAppCredentials;
|
||||
}
|
||||
}
|
||||
Loaded 100 of 184 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user