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 |
No files matched your search
+48
-1
@@ -8,6 +8,53 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 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))
|
||||
@@ -24,7 +71,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
- 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 multitarget iOS projects. ([#414](https://github.com/expo/eas-cli/pull/414) by [@dsokal](https://github.com/dsokal))
|
||||
- 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
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.17.0"
|
||||
"version": "0.18.2"
|
||||
}
|
||||
+24
-27
@@ -69,7 +69,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.17.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.17.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.17.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.17.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.17.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.17.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.17.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.17.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.17.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.17.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.17.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.17.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.17.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.17.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.17.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.17.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)`
|
||||
|
||||
@@ -316,8 +314,7 @@ OPTIONS
|
||||
|
||||
iOS specific options=asc-app-id App Store Connect unique application Apple ID
|
||||
number. Providing this param results in skipping
|
||||
app creation step. Learn more:
|
||||
https://expo.fyi/asc-app-id
|
||||
app creation step. Learn more.
|
||||
|
||||
iOS specific options=bundle-identifier Your iOS Bundle Identifier (default:
|
||||
expo.ios.bundleIdentifier from app config)
|
||||
@@ -370,7 +367,7 @@ EXAMPLES
|
||||
and provide its App ID
|
||||
```
|
||||
|
||||
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.17.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`
|
||||
|
||||
@@ -389,7 +386,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.17.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]`
|
||||
|
||||
@@ -403,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.17.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`
|
||||
|
||||
@@ -417,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.17.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`
|
||||
|
||||
@@ -437,7 +434,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.17.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`
|
||||
|
||||
@@ -451,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.17.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",
|
||||
@@ -3863,6 +3875,12 @@
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "NEW",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "IN_QUEUE",
|
||||
"description": "",
|
||||
@@ -4160,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": "",
|
||||
@@ -6564,6 +6594,18 @@
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "appleAppSpecificPasswordId",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
@@ -7898,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,
|
||||
@@ -14950,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
|
||||
}
|
||||
@@ -15294,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",
|
||||
@@ -15580,6 +15673,16 @@
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "updates",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "BuildUpdatesInput",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "secrets",
|
||||
"description": "",
|
||||
@@ -15703,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",
|
||||
@@ -16044,6 +16168,16 @@
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "channel",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "distribution",
|
||||
"description": "",
|
||||
@@ -16304,6 +16438,16 @@
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "updates",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "BuildUpdatesInput",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "secrets",
|
||||
"description": "",
|
||||
@@ -16432,6 +16576,16 @@
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "updates",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "BuildUpdatesInput",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "distribution",
|
||||
"description": "",
|
||||
@@ -16806,6 +16960,16 @@
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "updates",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "BuildUpdatesInput",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "distribution",
|
||||
"description": "",
|
||||
@@ -16933,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
|
||||
@@ -16974,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
|
||||
@@ -17015,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
|
||||
@@ -17134,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
|
||||
@@ -17175,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
|
||||
@@ -17198,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
|
||||
},
|
||||
@@ -17217,6 +17442,16 @@
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "appSpecificPasswordId",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
@@ -17612,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,
|
||||
@@ -17699,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",
|
||||
@@ -19897,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.17.0",
|
||||
"version": "0.18.2",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -13,8 +13,8 @@
|
||||
"@expo/apple-utils": "0.0.0-alpha.20",
|
||||
"@expo/config": "3.3.42",
|
||||
"@expo/config-plugins": "2.0.2",
|
||||
"@expo/eas-build-job": "0.2.35",
|
||||
"@expo/eas-json": "^0.17.0",
|
||||
"@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,12 +168,12 @@
|
||||
"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",
|
||||
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -136,7 +136,6 @@ async function ensureAndroidCredentialsAsync(
|
||||
projectName: ctx.commandCtx.projectName,
|
||||
androidApplicationIdentifier,
|
||||
},
|
||||
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
|
||||
}
|
||||
);
|
||||
const { credentialsSource } = ctx.buildProfile;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
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';
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,13 +17,7 @@ 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;
|
||||
@@ -66,7 +61,7 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await isGitStatusCleanAsync())) {
|
||||
if (await vcs.hasUncommittedChangesAsync()) {
|
||||
Log.addNewLineIfNone();
|
||||
await reviewAndCommitChangesAsync(
|
||||
`[EAS Build] Run EAS Build for ${
|
||||
@@ -225,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;
|
||||
}
|
||||
@@ -258,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}`,
|
||||
|
||||
@@ -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,18 +1,14 @@
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
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 { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
|
||||
import { XcodeBuildContext, resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { sanitizedProjectName } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
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';
|
||||
@@ -31,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(
|
||||
@@ -43,14 +40,21 @@ export async function prepareIosBuildAsync(
|
||||
);
|
||||
}
|
||||
|
||||
if (buildCtx.buildProfile.workflow === Workflow.MANAGED) {
|
||||
if (buildProfile.workflow === Workflow.MANAGED) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(
|
||||
commandCtx.projectDir,
|
||||
commandCtx.exp
|
||||
);
|
||||
}
|
||||
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(buildCtx);
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||
{
|
||||
projectDir: commandCtx.projectDir,
|
||||
nonInteractive: commandCtx.nonInteractive,
|
||||
exp: commandCtx.exp,
|
||||
},
|
||||
buildProfile
|
||||
);
|
||||
const targets = await resolveTargetsAsync(
|
||||
{
|
||||
projectDir: commandCtx.projectDir,
|
||||
@@ -68,7 +72,7 @@ export async function prepareIosBuildAsync(
|
||||
await validateAndSyncProjectConfigurationAsync({
|
||||
projectDir: commandCtx.projectDir,
|
||||
exp: commandCtx.exp,
|
||||
buildProfile: buildCtx.buildProfile,
|
||||
buildProfile,
|
||||
});
|
||||
},
|
||||
prepareJobAsync: async (
|
||||
@@ -101,72 +105,3 @@ export async function prepareIosBuildAsync(
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveXcodeBuildContextAsync(
|
||||
buildCtx: BuildContext<Platform.IOS>
|
||||
): Promise<XcodeBuildContext> {
|
||||
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
|
||||
const buildScheme = buildCtx.buildProfile.scheme ?? (await resolveSchemeAsync(buildCtx));
|
||||
return {
|
||||
buildScheme,
|
||||
buildConfiguration:
|
||||
buildCtx.buildProfile.schemeBuildConfiguration ??
|
||||
(await IOSConfig.BuildScheme.getArchiveBuildConfigurationForSchemeAsync(
|
||||
buildCtx.commandCtx.projectDir,
|
||||
buildScheme
|
||||
)),
|
||||
applicationTarget: await IOSConfig.BuildScheme.getApplicationTargetNameForSchemeAsync(
|
||||
buildCtx.commandCtx.projectDir,
|
||||
buildScheme
|
||||
),
|
||||
};
|
||||
} else {
|
||||
const expoName = buildCtx.commandCtx.exp.name;
|
||||
if (!expoName) {
|
||||
throw new Error('"expo.name" is required in your app.json');
|
||||
}
|
||||
const sanitizedExpoName = sanitizedProjectName(expoName);
|
||||
if (!sanitizedExpoName) {
|
||||
throw new Error('"expo.name" needs to contain some alphanumeric characters');
|
||||
}
|
||||
return {
|
||||
buildScheme: sanitizedExpoName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { createCredentialsContextAsync } from '../../credentials/context';
|
||||
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
|
||||
import { resolveEntitlementsJsonAsync } from '../../credentials/ios/appstore/entitlements';
|
||||
import { IosCredentials, Target } from '../../credentials/ios/types';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { CredentialsResult } from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
@@ -25,13 +23,7 @@ export async function ensureIosCredentialsAsync(
|
||||
});
|
||||
|
||||
const provider = new IosCredentialsProvider(ctx, {
|
||||
app: {
|
||||
account: nullthrows(
|
||||
findAccountByName(ctx.user.accounts, buildCtx.commandCtx.accountName),
|
||||
`You do not have access to account: ${buildCtx.commandCtx.accountName}`
|
||||
),
|
||||
projectName: buildCtx.commandCtx.projectName,
|
||||
},
|
||||
app: getAppFromContext(ctx),
|
||||
targets,
|
||||
iosCapabilitiesOptions: {
|
||||
entitlements: await resolveEntitlementsJsonAsync(
|
||||
|
||||
@@ -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,12 +1,12 @@
|
||||
import { ArchiveSource, Cache, Ios, Job, Workflow, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { IosGenericBuildProfile, IosManagedBuildProfile } from '@expo/eas-json';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
|
||||
import { 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';
|
||||
|
||||
@@ -35,7 +35,6 @@ interface CommonJobProperties {
|
||||
platform: Platform.IOS;
|
||||
projectArchive: ArchiveSource;
|
||||
builderEnvironment: Ios.BuilderEnvironment;
|
||||
releaseChannel?: string;
|
||||
distribution?: Ios.DistributionType;
|
||||
cache: Cache;
|
||||
secrets: {
|
||||
@@ -48,8 +47,6 @@ async function prepareJobCommonAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
{ credentials, projectArchive }: { credentials?: IosCredentials; projectArchive: ArchiveSource }
|
||||
): Promise<Partial<CommonJobProperties>> {
|
||||
const environmentSecrets = await readEnvironmentSecretsAsync(ctx.commandCtx.projectDir);
|
||||
|
||||
const buildCredentials: CommonJobProperties['secrets']['buildCredentials'] = {};
|
||||
if (credentials) {
|
||||
const targetNames = Object.keys(credentials);
|
||||
@@ -63,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,
|
||||
@@ -77,12 +74,22 @@ async function prepareJobCommonAsync(
|
||||
clear: ctx.commandCtx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
...(environmentSecrets ? { environmentSecrets } : {}),
|
||||
buildCredentials,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -99,7 +106,7 @@ 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,
|
||||
@@ -110,6 +117,7 @@ async function prepareGenericJobAsync(
|
||||
buildConfiguration: buildProfile.schemeBuildConfiguration,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
updates: { channel: buildProfile.channel },
|
||||
projectRootDirectory,
|
||||
};
|
||||
}
|
||||
@@ -120,7 +128,7 @@ 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, {
|
||||
@@ -131,6 +139,7 @@ async function prepareManagedJobAsync(
|
||||
buildType: buildProfile.buildType,
|
||||
username,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
updates: { channel: buildProfile.channel },
|
||||
projectRootDirectory,
|
||||
};
|
||||
}
|
||||
@@ -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',
|
||||
await getGitRootFullPathAsync(),
|
||||
shallowClonePath,
|
||||
]);
|
||||
await vcs.makeShallowCopyAsync(shallowClonePath);
|
||||
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
|
||||
'.',
|
||||
]);
|
||||
@@ -151,56 +121,3 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
|
||||
|
||||
return { size, path: tarPath };
|
||||
}
|
||||
|
||||
async function getGitRootFullPathAsync() {
|
||||
if (process.platform === 'win32') {
|
||||
// getRootDirectoryAsync() will return C:/path/to/repo on Windows and path
|
||||
// prefix should be file:///
|
||||
return `file:///${await gitRootDirectoryAsync()}`;
|
||||
} else {
|
||||
// getRootDirectoryAsync() will /path/to/repo, and path prefix should be
|
||||
// file:/// so only file:// needs to be prepended
|
||||
return `file://${await gitRootDirectoryAsync()}`;
|
||||
}
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
@@ -284,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,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,11 +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,
|
||||
@@ -39,56 +47,88 @@ export const testPKCS12EmptyPasswordKeystore: Keystore = {
|
||||
keyAlias: 'test-alias',
|
||||
};
|
||||
|
||||
export const testPushCredentials = {
|
||||
fcmApiKey: 'examplefcmapikey',
|
||||
export const testAppFragment: AppFragment = {
|
||||
id: 'test-app-id',
|
||||
fullName: '@testuser/testapp',
|
||||
slug: 'testapp',
|
||||
};
|
||||
|
||||
export const testAppCredentials = {
|
||||
experienceName: testExperienceName,
|
||||
keystore: testKeystore,
|
||||
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 testJester2AppCredentials = {
|
||||
experienceName: testJester2ExperienceName,
|
||||
keystore: testKeystore2,
|
||||
pushCredentials: testPushCredentials,
|
||||
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 testAllCredentials: { [key: string]: AndroidCredentials } = {
|
||||
[testExperienceName]: testAppCredentials,
|
||||
export const testLegacyAndroidBuildCredentialsFragment: AndroidAppBuildCredentialsFragment = {
|
||||
id: 'test-id',
|
||||
isDefault: true,
|
||||
isLegacy: true,
|
||||
name: 'legacy',
|
||||
androidKeystore: testJksAndroidKeystoreFragment,
|
||||
};
|
||||
|
||||
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 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,
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import nullthrows from 'nullthrows';
|
||||
import { AndroidAppBuildCredentialsFragment } from '../../graphql/generated';
|
||||
import { Context } from '../context';
|
||||
import * as credentialsJsonReader from '../credentialsJson/read';
|
||||
import { SetupBuildCredentials } from './actions/new/SetupBuildCredentials';
|
||||
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
|
||||
import { AppLookupParams } from './api/GraphqlClient';
|
||||
import { Keystore } from './credentials';
|
||||
|
||||
@@ -15,7 +15,6 @@ export interface AndroidCredentials {
|
||||
|
||||
interface Options {
|
||||
app: AppLookupParams;
|
||||
skipCredentialsCheck?: boolean;
|
||||
}
|
||||
|
||||
export default class AndroidCredentialsProvider {
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
import assert from 'assert';
|
||||
import { nanoid } from 'nanoid';
|
||||
import ora from 'ora';
|
||||
|
||||
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
|
||||
import { getApplicationId } from '../../../project/android/applicationId';
|
||||
@@ -9,6 +10,82 @@ 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;
|
||||
@@ -43,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',
|
||||
@@ -59,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"`;
|
||||
+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: {
|
||||
certificateP12: 'c29tZWJpbmFyeWNvbnRlbnQy',
|
||||
certificatePassword: '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,7 +224,10 @@ 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==',
|
||||
@@ -226,29 +247,4 @@ describe('credentialsJson', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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: {
|
||||
certificateP12: string;
|
||||
certificatePassword: 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(
|
||||
@@ -136,17 +84,6 @@ async function readCredentialsForTargetAsync(
|
||||
};
|
||||
}
|
||||
|
||||
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,9 +1,10 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource, IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { findApplicationTarget } from '../../project/ios/target';
|
||||
import { Context } from '../context';
|
||||
import * as credentialsJsonReader from '../credentialsJson/read';
|
||||
import { ensureAllTargetsAreConfigured } from '../credentialsJson/utils';
|
||||
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
|
||||
import { IosCapabilitiesOptions } from './appstore/ensureAppExists';
|
||||
import { App, IosCredentials, Target } from './types';
|
||||
@@ -33,37 +34,19 @@ export default class IosCredentialsProvider {
|
||||
}
|
||||
|
||||
private async getLocalAsync(): Promise<IosCredentials> {
|
||||
const mainTarget = nullthrows(
|
||||
this.options.targets.find(({ parentBundleIdentifier }) => !parentBundleIdentifier),
|
||||
'Could not find the application target'
|
||||
const applicationTarget = findApplicationTarget(this.options.targets);
|
||||
const iosCredentials = await credentialsJsonReader.readIosCredentialsAsync(
|
||||
this.ctx.projectDir,
|
||||
applicationTarget
|
||||
);
|
||||
|
||||
const iosTargetCredentialsMap = this.enforceIosTargetCredentialsMap(
|
||||
await credentialsJsonReader.readIosCredentialsAsync(this.ctx.projectDir),
|
||||
mainTarget
|
||||
);
|
||||
|
||||
const notConfiguredTargets: string[] = [];
|
||||
ensureAllTargetsAreConfigured(this.options.targets, iosCredentials);
|
||||
for (const target of this.options.targets) {
|
||||
if (!(target.targetName in iosTargetCredentialsMap)) {
|
||||
notConfiguredTargets.push(target.targetName);
|
||||
continue;
|
||||
}
|
||||
this.assertProvisioningProfileType(
|
||||
iosTargetCredentialsMap[target.targetName].provisioningProfile,
|
||||
iosCredentials[target.targetName].provisioningProfile,
|
||||
target.targetName
|
||||
);
|
||||
}
|
||||
|
||||
if (notConfiguredTargets.length > 0) {
|
||||
throw new Error(
|
||||
`Credentials for target${
|
||||
notConfiguredTargets.length === 1 ? '' : 's'
|
||||
} ${notConfiguredTargets.map(i => `'${i}'`).join(',')} are not defined in credentials.json`
|
||||
);
|
||||
}
|
||||
|
||||
return iosTargetCredentialsMap;
|
||||
return iosCredentials;
|
||||
}
|
||||
|
||||
private async getRemoteAsync(): Promise<IosCredentials> {
|
||||
@@ -76,19 +59,6 @@ export default class IosCredentialsProvider {
|
||||
}).runAsync(this.ctx);
|
||||
}
|
||||
|
||||
private enforceIosTargetCredentialsMap(
|
||||
iosCredentials: credentialsJsonReader.IosCredentials,
|
||||
mainTarget: Target
|
||||
): credentialsJsonReader.IosTargetCredentialsMap {
|
||||
if (credentialsJsonReader.isCredentialsMap(iosCredentials)) {
|
||||
return iosCredentials;
|
||||
} else {
|
||||
return {
|
||||
[mainTarget.targetName]: iosCredentials,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private assertProvisioningProfileType(provisioningProfile: string, targetName?: string): void {
|
||||
const isAdHoc = isAdHocProfile(provisioningProfile);
|
||||
if (this.options.distribution === 'internal' && !isAdHoc) {
|
||||
|
||||
@@ -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,11 +56,14 @@ 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: {
|
||||
@@ -81,18 +86,21 @@ 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: {
|
||||
@@ -105,8 +113,7 @@ describe(IosCredentialsProvider, () => {
|
||||
distribution: 'store',
|
||||
});
|
||||
|
||||
const buildCredentials =
|
||||
testIosAppCredentialsWithBuildCredentialsQueryResult.iosAppBuildCredentialsList[0];
|
||||
const buildCredentials = testCommonIosAppCredentialsFragment.iosAppBuildCredentialsList[0];
|
||||
await expect(provider.getCredentialsAsync(CredentialsSource.REMOTE)).resolves.toMatchObject(
|
||||
{
|
||||
testapp: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
IosDistributionType as GraphQLIosDistributionType,
|
||||
IosAppBuildCredentialsFragment,
|
||||
} from '../../../graphql/generated';
|
||||
import { getBundleIdentifier } from '../../../project/ios/bundleIdentifier';
|
||||
import { getProjectAccountName } from '../../../project/projectUtils';
|
||||
import { findAccountByName } from '../../../user/Account';
|
||||
import { Context } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
import { App, Target } from '../types';
|
||||
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
|
||||
|
||||
export async function getAllBuildCredentialsAsync(
|
||||
@@ -82,7 +82,7 @@ export async function assignBuildCredentialsAsync(
|
||||
});
|
||||
}
|
||||
|
||||
export function getAppLookupParamsFromContext(ctx: Context): AppLookupParams {
|
||||
export function getAppFromContext(ctx: Context): App {
|
||||
ctx.ensureProjectContext();
|
||||
const projectName = ctx.exp.slug;
|
||||
const accountName = getProjectAccountName(ctx.exp, ctx.user);
|
||||
@@ -90,8 +90,13 @@ export function getAppLookupParamsFromContext(ctx: Context): AppLookupParams {
|
||||
if (!account) {
|
||||
throw new Error(`You do not have access to account: ${accountName}`);
|
||||
}
|
||||
return {
|
||||
account,
|
||||
projectName,
|
||||
};
|
||||
}
|
||||
|
||||
const bundleIdentifier = getBundleIdentifier(ctx.projectDir, ctx.exp);
|
||||
|
||||
return { account, projectName, bundleIdentifier };
|
||||
export function getAppLookupParamsFromContext(ctx: Context, target: Target): AppLookupParams {
|
||||
const app = getAppFromContext(ctx);
|
||||
return { ...app, bundleIdentifier: target.bundleIdentifier };
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ApplePushKeyFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { Context } from '../../context';
|
||||
import { provideOrGeneratePushKeyAsync } from './PushKeyUtils';
|
||||
|
||||
export class CreatePushKey {
|
||||
constructor(private account: Account) {}
|
||||
|
||||
public async runAsync(ctx: Context): Promise<ApplePushKeyFragment> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(`A new push key cannot be created in non-interactive mode.`);
|
||||
}
|
||||
|
||||
const pushKey = await provideOrGeneratePushKeyAsync(ctx, this.account.name);
|
||||
const result = await ctx.ios.createPushKeyAsync(this.account, pushKey);
|
||||
Log.succeed('Created push key');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,7 @@ import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import dateformat from 'dateformat';
|
||||
|
||||
import {
|
||||
AppleDistributionCertificateFragment,
|
||||
AppleTeamFragment,
|
||||
} from '../../../graphql/generated';
|
||||
import { AppleDistributionCertificateFragment } from '../../../graphql/generated';
|
||||
import Log, { learnMore } from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { Account } from '../../../user/Account';
|
||||
@@ -21,6 +18,7 @@ import {
|
||||
import { filterRevokedDistributionCertsFromEasServers } from '../appstore/CredentialsUtils';
|
||||
import { distributionCertificateSchema } from '../credentials';
|
||||
import { validateDistributionCertificateAsync } from '../validators/validateDistributionCertificate';
|
||||
import { formatAppleTeam } from './AppleTeamUtils';
|
||||
|
||||
export function formatDistributionCertificate(
|
||||
distributionCertificate: AppleDistributionCertificateFragment,
|
||||
@@ -63,10 +61,6 @@ export function formatDistributionCertificate(
|
||||
return line;
|
||||
}
|
||||
|
||||
function formatAppleTeam({ appleTeamIdentifier, appleTeamName }: AppleTeamFragment): string {
|
||||
return `Team ID: ${appleTeamIdentifier}${appleTeamName ? `, Team name: ${appleTeamName}` : ''}`;
|
||||
}
|
||||
|
||||
async function _selectDistributionCertificateAsync(
|
||||
distCerts: AppleDistributionCertificateFragment[],
|
||||
validDistributionCertificates?: AppleDistributionCertificateFragment[]
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { ApplePushKeyFragment } from '../../../graphql/generated';
|
||||
import Log, { learnMore } from '../../../log';
|
||||
import { confirmAsync, promptAsync } from '../../../prompts';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { fromNow } from '../../../utils/date';
|
||||
import { Context } from '../../context';
|
||||
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
|
||||
import { PushKey, PushKeyStoreInfo } from '../appstore/Credentials.types';
|
||||
import { filterRevokedAndUntrackedPushKeysFromEasServers } from '../appstore/CredentialsUtils';
|
||||
import { APPLE_KEYS_TOO_MANY_GENERATED_ERROR } from '../appstore/pushKey';
|
||||
import { pushKeySchema } from '../credentials';
|
||||
import { isPushKeyValidAndTrackedAsync } from '../validators/validatePushKey';
|
||||
import { formatAppleTeam } from './AppleTeamUtils';
|
||||
|
||||
export async function provideOrGeneratePushKeyAsync(
|
||||
ctx: Context,
|
||||
accountName: string
|
||||
): Promise<PushKey> {
|
||||
if (!ctx.nonInteractive) {
|
||||
const userProvided = await promptForPushKeyAsync(ctx);
|
||||
if (userProvided) {
|
||||
if (!ctx.appStore.authCtx) {
|
||||
Log.warn('Unable to validate push key due to insufficient Apple Credentials');
|
||||
return userProvided;
|
||||
} else {
|
||||
const isValidAndTracked = await isPushKeyValidAndTrackedAsync(ctx, userProvided);
|
||||
if (isValidAndTracked) {
|
||||
return userProvided;
|
||||
}
|
||||
// user could've just input the id wrong, and the p8 could still be valid
|
||||
const useUserProvided = await confirmAsync({
|
||||
message: `Push Key with ID ${userProvided.apnsKeyId} could not be found on Apple's servers. Proceed anyway?`,
|
||||
});
|
||||
if (useUserProvided) {
|
||||
return userProvided;
|
||||
}
|
||||
return await provideOrGeneratePushKeyAsync(ctx, accountName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return await generatePushKeyAsync(ctx, accountName);
|
||||
}
|
||||
|
||||
async function promptForPushKeyAsync(ctx: Context): Promise<PushKey | null> {
|
||||
let initialValues: { teamId?: string } = {};
|
||||
if (ctx.appStore.authCtx) {
|
||||
initialValues = {
|
||||
teamId: ctx.appStore.authCtx.team.id,
|
||||
};
|
||||
}
|
||||
const userProvided = await askForUserProvidedAsync(pushKeySchema, initialValues);
|
||||
if (!userProvided) {
|
||||
return null;
|
||||
}
|
||||
if (ctx.appStore.authCtx && userProvided.teamId === initialValues.teamId) {
|
||||
return {
|
||||
...userProvided,
|
||||
teamName: ctx.appStore.authCtx.team.name,
|
||||
};
|
||||
}
|
||||
return userProvided;
|
||||
}
|
||||
|
||||
async function generatePushKeyAsync(ctx: Context, accountName: string): Promise<PushKey> {
|
||||
await ctx.appStore.ensureAuthenticatedAsync();
|
||||
try {
|
||||
return await ctx.appStore.createPushKeyAsync();
|
||||
} catch (e) {
|
||||
if (e.message === APPLE_KEYS_TOO_MANY_GENERATED_ERROR) {
|
||||
const pushKeys = await ctx.appStore.listPushKeysAsync();
|
||||
Log.warn('Maximum number of Push Notifications Keys generated on Apple Developer Portal.');
|
||||
Log.warn(APPLE_KEYS_TOO_MANY_GENERATED_ERROR);
|
||||
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(
|
||||
"Start the CLI without the '--non-interactive' flag to revoke existing push notification keys."
|
||||
);
|
||||
}
|
||||
|
||||
Log.log(chalk.grey(`⚠️ Revoking a Push Key will affect other apps that rely on it`));
|
||||
Log.log(learnMore('https://docs.expo.io/distribution/app-signing/#push-notification-keys'));
|
||||
Log.log();
|
||||
|
||||
const { pushKeysToRevoke } = await promptAsync({
|
||||
type: 'multiselect',
|
||||
name: 'pushKeysToRevoke',
|
||||
message: 'Select Push Notifications Key to revoke.',
|
||||
// @ts-expect-error property missing from `@types/prompts`
|
||||
optionsPerPage: 20,
|
||||
choices: pushKeys.map(pushKey => ({
|
||||
value: pushKey,
|
||||
title: formatPushKeyFromApple(pushKey),
|
||||
})),
|
||||
});
|
||||
|
||||
if (pushKeysToRevoke.length > 0) {
|
||||
const ids = pushKeysToRevoke.map(({ id }: PushKeyStoreInfo) => id);
|
||||
await ctx.appStore.revokePushKeyAsync(ids);
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return await generatePushKeyAsync(ctx, accountName);
|
||||
}
|
||||
|
||||
function formatPushKeyFromApple(pushKey: PushKeyStoreInfo): string {
|
||||
const { name, id } = pushKey;
|
||||
const keyIdText = id ? ` - Key ID: ${id}` : ``;
|
||||
return `${name}${keyIdText}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* select a push key from an account (validity status shown on a best effort basis)
|
||||
* */
|
||||
export async function selectPushKeyAsync(
|
||||
ctx: Context,
|
||||
account: Account
|
||||
): Promise<ApplePushKeyFragment | null> {
|
||||
const pushKeysForAccount = await ctx.ios.getPushKeysForAccountAsync(account);
|
||||
if (pushKeysForAccount.length === 0) {
|
||||
Log.warn(`There are no Push Keys available in your EAS account.`);
|
||||
return null;
|
||||
}
|
||||
if (!ctx.appStore.authCtx) {
|
||||
return selectPushKeysAsync(pushKeysForAccount);
|
||||
}
|
||||
|
||||
const validPushKeys = await getValidAndTrackedPushKeysOnEasServersAsync(ctx, pushKeysForAccount);
|
||||
return selectPushKeysAsync(pushKeysForAccount, validPushKeys);
|
||||
}
|
||||
|
||||
export async function getValidAndTrackedPushKeysOnEasServersAsync(
|
||||
ctx: Context,
|
||||
pushKeysForAccount: ApplePushKeyFragment[]
|
||||
): Promise<ApplePushKeyFragment[]> {
|
||||
const pushInfoFromApple = await ctx.appStore.listPushKeysAsync();
|
||||
return await filterRevokedAndUntrackedPushKeysFromEasServers(
|
||||
pushKeysForAccount,
|
||||
pushInfoFromApple
|
||||
);
|
||||
}
|
||||
|
||||
async function selectPushKeysAsync(
|
||||
pushKeys: ApplePushKeyFragment[],
|
||||
validPushKeys?: ApplePushKeyFragment[]
|
||||
): Promise<ApplePushKeyFragment | null> {
|
||||
const validPushKeyIdentifiers = validPushKeys?.map(pushKey => pushKey.keyIdentifier);
|
||||
const sortedPushKeys = sortPushKeys(pushKeys, validPushKeys);
|
||||
const { chosenPushKey } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'chosenPushKey',
|
||||
message: 'Select a push key from the list.',
|
||||
choices: sortedPushKeys.map(pushKey => ({
|
||||
title: formatPushKey(pushKey, validPushKeyIdentifiers),
|
||||
value: pushKey,
|
||||
})),
|
||||
});
|
||||
return chosenPushKey;
|
||||
}
|
||||
|
||||
export function sortPushKeys(
|
||||
pushKeys: ApplePushKeyFragment[],
|
||||
validPushKeys?: ApplePushKeyFragment[]
|
||||
): ApplePushKeyFragment[] {
|
||||
const validPushKeyIdentifiers = validPushKeys?.map(pushKey => pushKey.keyIdentifier);
|
||||
return pushKeys.sort((pushKeyA, pushKeyB) => {
|
||||
if (validPushKeyIdentifiers?.includes(pushKeyA.keyIdentifier)) {
|
||||
return -1;
|
||||
} else if (validPushKeyIdentifiers?.includes(pushKeyB.keyIdentifier)) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
export function formatPushKey(
|
||||
pushKey: ApplePushKeyFragment,
|
||||
validPushKeyIdentifiers?: string[]
|
||||
): string {
|
||||
const { keyIdentifier, appleTeam, updatedAt } = pushKey;
|
||||
let line: string = '';
|
||||
|
||||
line += `Push Key ID: ${pushKey.keyIdentifier}`;
|
||||
line += ` ${appleTeam ? `, ${formatAppleTeam(appleTeam)}` : ''}`;
|
||||
line += chalk.gray(`\n Updated: ${fromNow(new Date(updatedAt))} ago,`);
|
||||
|
||||
const apps = pushKey.iosAppCredentialsList.map(appCredentials => appCredentials.app);
|
||||
if (apps.length) {
|
||||
const appFullNames = apps.map(app => app.fullName).join(',');
|
||||
line += chalk.gray(`\n 📲 Used by: ${appFullNames}`);
|
||||
}
|
||||
|
||||
if (validPushKeyIdentifiers?.includes(keyIdentifier)) {
|
||||
line += chalk.gray("\n ✅ Currently valid on Apple's servers.");
|
||||
} else {
|
||||
line += '';
|
||||
}
|
||||
return line;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { ApplePushKeyFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { Context } from '../../context';
|
||||
import { selectPushKeyAsync } from './PushKeyUtils';
|
||||
|
||||
export class SelectAndRemovePushKey {
|
||||
constructor(private account: Account) {}
|
||||
|
||||
async runAsync(ctx: Context): Promise<void> {
|
||||
const selected = await selectPushKeyAsync(ctx, this.account);
|
||||
if (selected) {
|
||||
await new RemovePushKey(this.account, selected).runAsync(ctx);
|
||||
Log.succeed('Removed push key');
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RemovePushKey {
|
||||
constructor(private account: Account, private pushKey: ApplePushKeyFragment) {}
|
||||
|
||||
public async runAsync(ctx: Context): Promise<void> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(`Cannot remove push keys in non-interactive mode`);
|
||||
}
|
||||
|
||||
const apps = this.pushKey.iosAppCredentialsList.map(appCredentials => appCredentials.app);
|
||||
if (apps.length !== 0) {
|
||||
const appFullNames = apps.map(app => app.fullName).join(',');
|
||||
const confirm = await confirmAsync({
|
||||
message: `Removing this push key will disable push notifications for ${appFullNames}. Do you want to continue?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
Log.log('Aborting');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Log.log('Removing Push Key');
|
||||
await ctx.ios.deletePushKeyAsync(this.pushKey.id);
|
||||
|
||||
let shouldRevoke = false;
|
||||
if (!ctx.nonInteractive) {
|
||||
shouldRevoke = await confirmAsync({
|
||||
message: `Do you also want to revoke this Push Key on the Apple Developer Portal?`,
|
||||
});
|
||||
} else if (ctx.nonInteractive) {
|
||||
Log.log('Skipping certificate revocation on the Apple Developer Portal.');
|
||||
}
|
||||
if (shouldRevoke) {
|
||||
await ctx.appStore.revokePushKeyAsync([this.pushKey.keyIdentifier]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import Log from '../../../log';
|
||||
import { CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
|
||||
import { App, IosAppBuildCredentialsMap, IosCredentials, Target } from '../types';
|
||||
@@ -22,8 +21,6 @@ export class SetupBuildCredentials {
|
||||
constructor(private options: Options) {}
|
||||
|
||||
async runAsync(ctx: Context): Promise<IosCredentials> {
|
||||
const manager = new CredentialsManager(ctx);
|
||||
|
||||
const hasManyTargets = this.options.targets.length > 1;
|
||||
const iosAppBuildCredentialsMap: IosAppBuildCredentialsMap = {};
|
||||
if (hasManyTargets) {
|
||||
@@ -60,7 +57,7 @@ export class SetupBuildCredentials {
|
||||
parentBundleIdentifier: target.parentBundleIdentifier,
|
||||
},
|
||||
});
|
||||
iosAppBuildCredentialsMap[target.targetName] = await action.runAsync(manager, ctx);
|
||||
iosAppBuildCredentialsMap[target.targetName] = await action.runAsync(ctx);
|
||||
}
|
||||
|
||||
const appInfo = formatAppInfo(this.options.app, this.options.targets);
|
||||
|
||||
Loaded 100 of 172 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user