mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47784ca6d3 | ||
|
|
3e5a3aca27 | ||
|
|
89b0227594 | ||
|
|
b98a30e7c6 | ||
|
|
5c7862a685 | ||
|
|
82833a9eb4 | ||
|
|
66171c3c4b | ||
|
|
179def91c7 | ||
|
|
9f63ad4527 |
No files matched your search
+16
-2
@@ -8,10 +8,26 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Use env variables from `eas.json` when evaluating `app.config.js`. ([#532](https://github.com/expo/eas-cli/pull/532) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix workflow detection when xcodeproj is an empty directory. ([#531](https://github.com/expo/eas-cli/pull/531) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix Android Keystore upload. ([#538](https://github.com/expo/eas-cli/pull/538) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade typescript to 4.3.5. ([#533](https://github.com/expo/eas-cli/pull/533) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.22.0](https://github.com/expo/eas-cli/releases/tag/v0.22.0) - 2021-07-23
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Added support for automatically creating and linking iOS capability identifiers (Apple Pay, iCloud Containers, App Groups). ([#524](https://github.com/expo/eas-cli/pull/524) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add `secret:create --force` command to overwrite existing secrets. ([#513](https://github.com/expo/eas-cli/pull/513) by [@bycedric](https://github.com/bycedric))
|
||||
- Fall back to APK when building for internal distribution. ([#527](https://github.com/expo/eas-cli/pull/527) by [@dsokal](https://github.com/dsokal))
|
||||
- Add `iosEnterpriseProvisioning` to build metadata. ([#522](https://github.com/expo/eas-cli/pull/522) by [@dsokal](https://github.com/dsokal))
|
||||
- Autodetect Google Services JSON key path in `eas submit -p android`. ([#520](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
@@ -19,8 +35,6 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
- Fix unhandled error when amplitude domains are blocked. ([#512](https://github.com/expo/eas-cli/pull/512) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Use default value for `appBuildVersion` in build metadata when building an Android managed project. ([#526](https://github.com/expo/eas-cli/pull/526) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [0.21.0](https://github.com/expo/eas-cli/releases/tag/v0.21.0) - 2021-07-12
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.22.0"
|
||||
"version": "0.22.1"
|
||||
}
|
||||
+4
-4
@@ -17,12 +17,12 @@
|
||||
"devDependencies": {
|
||||
"@expo/oclif-dev-cli": "1.24.0-expo",
|
||||
"@types/jest": "^26.0.13",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-config-universe": "^4.0.0",
|
||||
"eslint-plugin-graphql": "^4.0.0",
|
||||
"eslint": "7.31.0",
|
||||
"eslint-config-universe": "7.0.1",
|
||||
"eslint-plugin-graphql": "4.0.0",
|
||||
"jest": "^26.4.2",
|
||||
"jest-watch-typeahead": "^0.6.1",
|
||||
"lerna": "^3.22.1",
|
||||
"prettier": "^2.0.5"
|
||||
"prettier": "2.3.2"
|
||||
}
|
||||
}
|
||||
+24
-24
@@ -71,7 +71,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -85,7 +85,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -99,7 +99,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -110,7 +110,7 @@ USAGE
|
||||
$ eas analytics [STATUS]
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -130,7 +130,7 @@ OPTIONS
|
||||
--[no-]wait Wait for build(s) to complete
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -141,7 +141,7 @@ USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -155,7 +155,7 @@ OPTIONS
|
||||
-p, --platform=(android|ios|all) Platform to configure
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -171,7 +171,7 @@ OPTIONS
|
||||
--status=(new|in-queue|in-progress|errored|finished|canceled)
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -182,7 +182,7 @@ USAGE
|
||||
$ eas build:view [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -193,7 +193,7 @@ USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/credentials.ts)_
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -204,7 +204,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -218,7 +218,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -229,7 +229,7 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -257,7 +257,7 @@ USAGE
|
||||
$ eas project:info
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -271,7 +271,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
@@ -288,7 +288,7 @@ OPTIONS
|
||||
--value=value Value of the secret
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -305,7 +305,7 @@ DESCRIPTION
|
||||
Unsure where to find the secret's ID? Run eas secrets:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -316,7 +316,7 @@ USAGE
|
||||
$ eas secret:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
|
||||
## `eas submit --platform=(android|ios)`
|
||||
|
||||
@@ -396,7 +396,7 @@ EXAMPLES
|
||||
and provide its App ID
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -415,7 +415,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -429,7 +429,7 @@ ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -443,7 +443,7 @@ OPTIONS
|
||||
--event=(BUILD) Event type that triggers the webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -463,7 +463,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -477,5 +477,5 @@ ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.22.1/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
Generated
+111
@@ -3969,6 +3969,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "cancelingActor",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "INTERFACE",
|
||||
"name": "Actor",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "artifacts",
|
||||
"description": "",
|
||||
@@ -9276,6 +9288,30 @@
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "mostRecentUpdateForEachPlatform",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Update",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
@@ -21692,6 +21728,81 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "Deployment",
|
||||
"description": "Represents a Deployment - a set of Builds with the same Runtime Version and Channel",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "runtimeVersion",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "channel",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "recentBuilds",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Build",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "branchToReceive",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "UpdateBranch",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "CacheControlScope",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.22.0",
|
||||
"version": "0.22.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -14,7 +14,7 @@
|
||||
"@expo/config": "3.3.42",
|
||||
"@expo/config-plugins": "3.0.3",
|
||||
"@expo/eas-build-job": "0.2.44",
|
||||
"@expo/eas-json": "^0.22.0",
|
||||
"@expo/eas-json": "^0.22.1",
|
||||
"@expo/json-file": "8.2.25",
|
||||
"@expo/pkcs12": "0.0.4",
|
||||
"@expo/plist": "0.0.11",
|
||||
@@ -33,7 +33,7 @@
|
||||
"cli-table3": "0.6.0",
|
||||
"dateformat": "3.0.3",
|
||||
"env-paths": "2.2.0",
|
||||
"fast-glob": "3.2.4",
|
||||
"fast-glob": "3.2.7",
|
||||
"figures": "3.2.0",
|
||||
"form-data": "3.0.0",
|
||||
"fs-extra": "9.0.1",
|
||||
@@ -64,7 +64,7 @@
|
||||
"tempy": "0.7.0",
|
||||
"terminal-link": "2.1.1",
|
||||
"timeago.js": "4.0.2",
|
||||
"tslib": "1.14.1",
|
||||
"tslib": "2.3.0",
|
||||
"turndown": "7.0.0",
|
||||
"untildify": "4.0.0",
|
||||
"uuid": "8.3.0",
|
||||
@@ -79,7 +79,6 @@
|
||||
"@graphql-codegen/introspection": "^1.18.0",
|
||||
"@graphql-codegen/typescript": "^1.17.11",
|
||||
"@graphql-codegen/typescript-operations": "^1.17.10",
|
||||
"@types/color-string": "^1.5.0",
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/form-data": "^2.5.0",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
@@ -93,18 +92,15 @@
|
||||
"@types/prompts": "^2.0.9",
|
||||
"@types/semver": "^7.3.4",
|
||||
"@types/tar": "^4.0.3",
|
||||
"@types/tough-cookie": "^4.0.0",
|
||||
"@types/tough-cookie": "4.0.1",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/wrap-ansi": "^3.0.0",
|
||||
"@vercel/ncc": "^0.23.0",
|
||||
"axios": "^0.21.0",
|
||||
"axios": "0.21.1",
|
||||
"babel-jest": "^26.3.0",
|
||||
"memfs": "^3.2.0",
|
||||
"mockdate": "^3.0.2",
|
||||
"nock": "^13.0.5",
|
||||
"ts-node": "^8",
|
||||
"typescript": "^3.9.7",
|
||||
"wonka": "^4.0.14"
|
||||
"typescript": "4.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
|
||||
@@ -14,10 +14,11 @@ export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig)
|
||||
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
|
||||
|
||||
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
|
||||
const updatedBuildGradleContents = AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
|
||||
projectDir,
|
||||
buildGradleContents
|
||||
);
|
||||
const updatedBuildGradleContents =
|
||||
AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
|
||||
projectDir,
|
||||
buildGradleContents
|
||||
);
|
||||
await fs.writeFile(buildGradlePath, updatedBuildGradleContents);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Android, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
import { EasConfig } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
@@ -15,8 +14,12 @@ import {
|
||||
} from '../../project/android/applicationId';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { CredentialsResult, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommandContext, createBuildContextAsync } from '../context';
|
||||
import {
|
||||
BuildRequestSender,
|
||||
CredentialsResult,
|
||||
prepareBuildRequestForPlatformAsync,
|
||||
} from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { Platform } from '../types';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
@@ -25,15 +28,9 @@ import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
|
||||
export async function prepareAndroidBuildAsync(
|
||||
commandCtx: CommandContext,
|
||||
easConfig: EasConfig
|
||||
): Promise<() => Promise<string | undefined>> {
|
||||
const buildCtx = await createBuildContextAsync<Platform.ANDROID>({
|
||||
commandCtx,
|
||||
platform: Platform.ANDROID,
|
||||
easConfig,
|
||||
});
|
||||
const { buildProfile } = buildCtx;
|
||||
ctx: BuildContext<Platform.ANDROID>
|
||||
): Promise<BuildRequestSender> {
|
||||
const { buildProfile } = ctx;
|
||||
|
||||
if (buildProfile.distribution === 'internal' && buildProfile.gradleCommand?.match(/bundle/)) {
|
||||
Log.addNewLineIfNone();
|
||||
@@ -51,16 +48,16 @@ This means that it will most likely produce an AAB and you will not be able to i
|
||||
}
|
||||
}
|
||||
|
||||
if (buildCtx.workflow === Workflow.MANAGED) {
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx: buildCtx,
|
||||
ctx,
|
||||
ensureCredentialsAsync: ensureAndroidCredentialsAsync,
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
if (buildCtx.workflow === Workflow.GENERIC) {
|
||||
await validateAndSyncProjectConfigurationAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
if (ctx.workflow === Workflow.GENERIC) {
|
||||
await validateAndSyncProjectConfigurationAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
},
|
||||
prepareJobAsync,
|
||||
@@ -90,21 +87,19 @@ async function ensureAndroidCredentialsAsync(
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
return;
|
||||
}
|
||||
const androidApplicationIdentifier = await getApplicationIdAsync(
|
||||
ctx.commandCtx.projectDir,
|
||||
ctx.commandCtx.exp
|
||||
);
|
||||
const androidApplicationIdentifier = await getApplicationIdAsync(ctx.projectDir, ctx.exp);
|
||||
const provider = new AndroidCredentialsProvider(
|
||||
await createCredentialsContextAsync(ctx.commandCtx.projectDir, {
|
||||
nonInteractive: ctx.commandCtx.nonInteractive,
|
||||
await createCredentialsContextAsync(ctx.projectDir, {
|
||||
exp: ctx.exp,
|
||||
nonInteractive: ctx.nonInteractive,
|
||||
}),
|
||||
{
|
||||
app: {
|
||||
account: nullthrows(
|
||||
findAccountByName(ctx.commandCtx.user.accounts, ctx.commandCtx.accountName),
|
||||
`You do not have access to account: ${ctx.commandCtx.accountName}`
|
||||
findAccountByName(ctx.user.accounts, ctx.accountName),
|
||||
`You do not have access to account: ${ctx.accountName}`
|
||||
),
|
||||
projectName: ctx.commandCtx.projectName,
|
||||
projectName: ctx.projectName,
|
||||
androidApplicationIdentifier,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -17,9 +17,8 @@ export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData
|
||||
): Promise<Job> {
|
||||
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
|
||||
const projectRootDirectory =
|
||||
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
|
||||
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
|
||||
const { credentials } = jobData;
|
||||
const buildCredentials = credentials
|
||||
? {
|
||||
@@ -55,7 +54,7 @@ export async function prepareJobAsync(
|
||||
},
|
||||
cache: {
|
||||
...ctx.buildProfile.cache,
|
||||
clear: ctx.commandCtx.clearCache,
|
||||
clear: ctx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
...buildCredentials,
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { ArchiveSource, ArchiveSourceType, Job, Metadata } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
import ora from 'ora';
|
||||
|
||||
import { UploadSessionType } from '../graphql/generated';
|
||||
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
|
||||
import { BuildResult } from '../graphql/mutations/BuildMutation';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import Log from '../log';
|
||||
import { promptAsync } from '../prompts';
|
||||
import { uploadAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
import { sleep } from '../utils/promise';
|
||||
import vcs from '../vcs';
|
||||
import { requestedPlatformDisplayNames } from './constants';
|
||||
import { BuildContext } from './context';
|
||||
@@ -40,11 +45,13 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
|
||||
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
|
||||
}
|
||||
|
||||
export type BuildRequestSender = () => Promise<BuildFragment | undefined>;
|
||||
|
||||
export async function prepareBuildRequestForPlatformAsync<
|
||||
TPlatform extends Platform,
|
||||
Credentials,
|
||||
TJob extends Job
|
||||
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<() => Promise<string | undefined>> {
|
||||
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
|
||||
const credentialsResult = await withAnalyticsAsync(
|
||||
async () => await builder.ensureCredentialsAsync(builder.ctx),
|
||||
{
|
||||
@@ -53,7 +60,7 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
trackingCtx: builder.ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
if (!builder.ctx.commandCtx.skipProjectConfiguration) {
|
||||
if (!builder.ctx.skipProjectConfiguration) {
|
||||
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(builder.ctx), {
|
||||
successEvent: Event.CONFIGURE_PROJECT_SUCCESS,
|
||||
failureEvent: Event.CONFIGURE_PROJECT_FAIL,
|
||||
@@ -68,12 +75,12 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
requestedPlatformDisplayNames[builder.ctx.platform as Platform]
|
||||
}`,
|
||||
{
|
||||
nonInteractive: builder.ctx.commandCtx.nonInteractive,
|
||||
nonInteractive: builder.ctx.nonInteractive,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const projectArchive = builder.ctx.commandCtx.local
|
||||
const projectArchive = builder.ctx.local
|
||||
? ({
|
||||
type: ArchiveSourceType.PATH,
|
||||
path: (await makeProjectTarballAsync()).path,
|
||||
@@ -92,7 +99,7 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
});
|
||||
|
||||
return async () => {
|
||||
if (builder.ctx.commandCtx.local) {
|
||||
if (builder.ctx.local) {
|
||||
await runLocalBuildAsync(job);
|
||||
return undefined;
|
||||
} else {
|
||||
@@ -177,7 +184,7 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
|
||||
builder: Builder<TPlatform, Credentials, TJob>,
|
||||
job: TJob,
|
||||
metadata: Metadata
|
||||
): Promise<string> {
|
||||
): Promise<BuildFragment> {
|
||||
const { ctx } = builder;
|
||||
return await withAnalyticsAsync(
|
||||
async () => {
|
||||
@@ -186,13 +193,13 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
|
||||
}
|
||||
|
||||
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
|
||||
ctx.commandCtx.projectId,
|
||||
ctx.projectId,
|
||||
job,
|
||||
metadata
|
||||
);
|
||||
|
||||
printDeprecationWarnings(deprecationInfo);
|
||||
return build.id;
|
||||
return build;
|
||||
},
|
||||
{
|
||||
successEvent: Event.BUILD_REQUEST_SUCCESS,
|
||||
@@ -274,3 +281,98 @@ async function reviewAndCommitChangesAsync(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function waitForBuildEndAsync(
|
||||
buildIds: string[],
|
||||
{ timeoutSec = 3600, intervalSec = 30 } = {}
|
||||
): Promise<(BuildFragment | null)[]> {
|
||||
Log.log(
|
||||
`Waiting for build${buildIds.length > 1 ? 's' : ''} to complete. You can press Ctrl+C to exit.`
|
||||
);
|
||||
const spinner = ora().start();
|
||||
let time = new Date().getTime();
|
||||
const endTime = time + timeoutSec * 1000;
|
||||
while (time <= endTime) {
|
||||
const builds: (BuildFragment | null)[] = await Promise.all(
|
||||
buildIds.map(async buildId => {
|
||||
try {
|
||||
return await BuildQuery.byIdAsync(buildId, { useCache: false });
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
if (builds.length === 1) {
|
||||
const build = nullthrows(builds[0]);
|
||||
switch (build.status) {
|
||||
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;
|
||||
case BuildStatus.Canceled:
|
||||
spinner.text = 'Build canceled';
|
||||
spinner.stopAndPersist();
|
||||
return builds;
|
||||
case BuildStatus.InProgress:
|
||||
spinner.text = 'Build in progress...';
|
||||
break;
|
||||
case BuildStatus.Errored:
|
||||
spinner.fail('Build failed');
|
||||
if (build.error) {
|
||||
return builds;
|
||||
} else {
|
||||
throw new Error(`Standalone build failed!`);
|
||||
}
|
||||
default:
|
||||
spinner.warn('Unknown status.');
|
||||
throw new Error(`Unknown status: ${builds} - aborting!`);
|
||||
}
|
||||
} else {
|
||||
if (builds.filter(build => build?.status === BuildStatus.Finished).length === builds.length) {
|
||||
spinner.succeed('All builds have finished');
|
||||
return builds;
|
||||
} else if (
|
||||
builds.filter(build =>
|
||||
build?.status
|
||||
? [BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(
|
||||
build.status
|
||||
)
|
||||
: false
|
||||
).length === builds.length
|
||||
) {
|
||||
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;
|
||||
const finished = builds.filter(build => build?.status === BuildStatus.Finished).length;
|
||||
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}`,
|
||||
errored && chalk.red(`Builds failed: ${errored}`),
|
||||
finished && chalk.green(`Builds finished: ${finished}`),
|
||||
unknownState && chalk.red(`Builds in unknown state: ${unknownState}`),
|
||||
]
|
||||
.filter(i => i)
|
||||
.join('\t');
|
||||
}
|
||||
}
|
||||
time = new Date().getTime();
|
||||
await sleep(intervalSec * 1000);
|
||||
}
|
||||
spinner.warn('Timed out');
|
||||
throw new Error(
|
||||
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { error } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log from '../log';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { promptAsync } from '../prompts';
|
||||
import { confirmAsync, promptAsync } from '../prompts';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import vcs from '../vcs';
|
||||
import { configureAndroidAsync } from './android/configure';
|
||||
@@ -21,6 +22,45 @@ const configureCommitMessage = {
|
||||
[RequestedPlatform.All]: 'Configure EAS Build',
|
||||
};
|
||||
|
||||
export async function ensureProjectConfiguredAsync(
|
||||
projectDir: string,
|
||||
requestedPlatform: RequestedPlatform
|
||||
): Promise<void> {
|
||||
const platformToConfigure = await getPlatformToConfigureAsync(projectDir, requestedPlatform);
|
||||
if (!platformToConfigure) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the prompt is consistent with the platforms we need to configure
|
||||
let message = 'This project is not configured to build with EAS. Set it up now?';
|
||||
if (platformToConfigure === RequestedPlatform.Ios) {
|
||||
message = 'Your iOS project is not configured to build with EAS. Set it up now?';
|
||||
} else if (platformToConfigure === RequestedPlatform.Android) {
|
||||
message = 'Your Android project is not configured to build with EAS. Set it up now?';
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({ message });
|
||||
if (confirm) {
|
||||
await configureAsync({
|
||||
projectDir,
|
||||
platform: platformToConfigure,
|
||||
});
|
||||
if (await vcs.hasUncommittedChangesAsync()) {
|
||||
error(
|
||||
'Build process requires clean working tree, please commit all your changes and run `eas build` again',
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
error(
|
||||
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
|
||||
'https://docs.expo.io/build/eas-json'
|
||||
)})`,
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function configureAsync(options: {
|
||||
platform: RequestedPlatform;
|
||||
projectDir: string;
|
||||
@@ -103,7 +143,7 @@ const IOS_GENERIC_DEFAULTS = {
|
||||
};
|
||||
|
||||
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
|
||||
const easJsonPath = path.join(ctx.projectDir, 'eas.json');
|
||||
const easJsonPath = EasJsonReader.formatEasJsonPath(ctx.projectDir);
|
||||
let existingEasJson;
|
||||
|
||||
if (await fs.pathExists(easJsonPath)) {
|
||||
@@ -188,3 +228,30 @@ async function reviewAndCommitChangesAsync(
|
||||
await reviewAndCommitChangesAsync(initialCommitMessage, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function getPlatformToConfigureAsync(
|
||||
projectDir: string,
|
||||
platform: RequestedPlatform
|
||||
): Promise<RequestedPlatform | null> {
|
||||
if (!(await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir)))) {
|
||||
return platform;
|
||||
}
|
||||
|
||||
const easConfig = await new EasJsonReader(projectDir, platform).readRawAsync();
|
||||
if (platform === RequestedPlatform.All) {
|
||||
if (easConfig.builds?.android && easConfig.builds?.ios) {
|
||||
return null;
|
||||
} else if (easConfig.builds?.ios) {
|
||||
return RequestedPlatform.Android;
|
||||
} else if (easConfig.builds?.android) {
|
||||
return RequestedPlatform.Ios;
|
||||
}
|
||||
} else if (
|
||||
(platform === RequestedPlatform.Android || platform === RequestedPlatform.Ios) &&
|
||||
easConfig.builds?.[platform]
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return platform;
|
||||
}
|
||||
@@ -5,74 +5,14 @@ import JsonFile from '@expo/json-file';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { getProjectAccountName } from '../project/projectUtils';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { findAccountByName } from '../user/Account';
|
||||
import { Actor } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { Platform, RequestedPlatform, TrackingContext } from './types';
|
||||
import Analytics, { Event } from './utils/analytics';
|
||||
|
||||
export interface CommandContext {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
profile: string;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
user: Actor;
|
||||
accountName: string;
|
||||
projectName: string;
|
||||
exp: ExpoConfig;
|
||||
nonInteractive: boolean;
|
||||
local: boolean;
|
||||
clearCache: boolean;
|
||||
skipProjectConfiguration: boolean;
|
||||
waitForBuildEnd: boolean;
|
||||
}
|
||||
|
||||
export async function createCommandContextAsync({
|
||||
requestedPlatform,
|
||||
profile,
|
||||
exp,
|
||||
projectDir,
|
||||
projectId,
|
||||
nonInteractive = false,
|
||||
local,
|
||||
clearCache = false,
|
||||
skipProjectConfiguration = false,
|
||||
waitForBuildEnd,
|
||||
}: {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
profile: string;
|
||||
exp: ExpoConfig;
|
||||
projectId: string;
|
||||
projectDir: string;
|
||||
nonInteractive: boolean;
|
||||
local: boolean;
|
||||
clearCache: boolean;
|
||||
skipProjectConfiguration: boolean;
|
||||
waitForBuildEnd: boolean;
|
||||
}): Promise<CommandContext> {
|
||||
const user = await ensureLoggedInAsync();
|
||||
const accountName = getProjectAccountName(exp, user);
|
||||
const projectName = exp.slug;
|
||||
|
||||
return {
|
||||
requestedPlatform,
|
||||
profile,
|
||||
projectDir,
|
||||
projectId,
|
||||
user,
|
||||
accountName,
|
||||
projectName,
|
||||
exp,
|
||||
nonInteractive,
|
||||
local,
|
||||
clearCache,
|
||||
skipProjectConfiguration,
|
||||
waitForBuildEnd,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ConfigureContext {
|
||||
user: Actor;
|
||||
projectDir: string;
|
||||
@@ -89,51 +29,87 @@ type PlatformBuildProfile<T extends Platform> = T extends Platform.ANDROID
|
||||
: IosBuildProfile;
|
||||
|
||||
export interface BuildContext<T extends Platform> {
|
||||
commandCtx: CommandContext;
|
||||
trackingCtx: TrackingContext;
|
||||
platform: T;
|
||||
accountName: string;
|
||||
buildProfile: PlatformBuildProfile<T>;
|
||||
buildProfileName: string;
|
||||
clearCache: boolean;
|
||||
exp: ExpoConfig;
|
||||
local: boolean;
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
skipProjectConfiguration: boolean;
|
||||
trackingCtx: TrackingContext;
|
||||
user: Actor;
|
||||
workflow: Workflow;
|
||||
}
|
||||
|
||||
export async function createBuildContextAsync<T extends Platform>({
|
||||
platform,
|
||||
buildProfileName,
|
||||
clearCache = false,
|
||||
easConfig,
|
||||
commandCtx,
|
||||
local,
|
||||
nonInteractive = false,
|
||||
platform,
|
||||
projectDir,
|
||||
skipProjectConfiguration = false,
|
||||
}: {
|
||||
platform: T;
|
||||
buildProfileName: string;
|
||||
clearCache: boolean;
|
||||
easConfig: EasConfig;
|
||||
commandCtx: CommandContext;
|
||||
local: boolean;
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
skipProjectConfiguration: boolean;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const buildProfile = easConfig.builds[platform] as PlatformBuildProfile<T> | undefined;
|
||||
if (!buildProfile) {
|
||||
throw new Error(`${platform} build profile does not exist`);
|
||||
}
|
||||
|
||||
const workflow = await resolveWorkflowAsync(commandCtx.projectDir, platform);
|
||||
const accountId = findAccountByName(commandCtx.user.accounts, commandCtx.accountName)?.id;
|
||||
const exp = getExpoConfig(projectDir, buildProfile.env);
|
||||
|
||||
const user = await ensureLoggedInAsync();
|
||||
const accountName = getProjectAccountName(exp, user);
|
||||
const projectName = exp.slug;
|
||||
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
|
||||
|
||||
const workflow = await resolveWorkflowAsync(projectDir, platform);
|
||||
const accountId = findAccountByName(user.accounts, accountName)?.id;
|
||||
const devClientProperties = getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir: commandCtx.projectDir,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
workflow,
|
||||
});
|
||||
const trackingCtx = {
|
||||
tracking_id: uuidv4(),
|
||||
platform,
|
||||
...(accountId && { account_id: accountId }),
|
||||
account_name: commandCtx.accountName,
|
||||
project_id: commandCtx.projectId,
|
||||
account_name: accountName,
|
||||
project_id: projectId,
|
||||
project_type: workflow,
|
||||
...devClientProperties,
|
||||
};
|
||||
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
|
||||
return {
|
||||
commandCtx,
|
||||
workflow,
|
||||
trackingCtx,
|
||||
platform,
|
||||
accountName,
|
||||
buildProfile,
|
||||
buildProfileName,
|
||||
clearCache,
|
||||
exp,
|
||||
local,
|
||||
nonInteractive,
|
||||
platform,
|
||||
projectDir,
|
||||
projectId,
|
||||
projectName,
|
||||
skipProjectConfiguration,
|
||||
trackingCtx,
|
||||
user,
|
||||
workflow,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -141,12 +117,10 @@ function getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
workflow,
|
||||
}: {
|
||||
platform: Platform;
|
||||
projectDir: string;
|
||||
buildProfile: AndroidBuildProfile | IosBuildProfile;
|
||||
workflow: Workflow;
|
||||
}): Partial<TrackingContext> {
|
||||
let includesDevClient;
|
||||
const version = tryGetDevClientVersion(projectDir);
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
import ora from 'ora';
|
||||
|
||||
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 { ensureRepoIsCleanAsync } from './utils/repository';
|
||||
|
||||
export async function buildAsync(commandCtx: CommandContext): Promise<void> {
|
||||
await vcs.ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(commandCtx.nonInteractive);
|
||||
|
||||
const scheduledBuilds = await startBuildsAsync(commandCtx);
|
||||
if (commandCtx.local) {
|
||||
return;
|
||||
}
|
||||
Log.newLine();
|
||||
printLogsUrls(commandCtx.accountName, scheduledBuilds);
|
||||
Log.newLine();
|
||||
|
||||
if (commandCtx.waitForBuildEnd) {
|
||||
const builds = await waitForBuildEndAsync(scheduledBuilds.map(i => i.buildId));
|
||||
printBuildResults(commandCtx.accountName, builds);
|
||||
exitWithNonZeroCodeIfSomeBuildsFailed(builds);
|
||||
}
|
||||
}
|
||||
|
||||
async function startBuildsAsync(
|
||||
commandCtx: CommandContext
|
||||
): Promise<{ platform: Platform; buildId: string }[]> {
|
||||
const shouldBuildAndroid = [RequestedPlatform.Android, RequestedPlatform.All].includes(
|
||||
commandCtx.requestedPlatform
|
||||
);
|
||||
const shouldBuildIos = [RequestedPlatform.Ios, RequestedPlatform.All].includes(
|
||||
commandCtx.requestedPlatform
|
||||
);
|
||||
const easConfig = await new EasJsonReader(
|
||||
commandCtx.projectDir,
|
||||
commandCtx.requestedPlatform
|
||||
).readAsync(commandCtx.profile);
|
||||
|
||||
const builds: {
|
||||
platform: Platform;
|
||||
sendBuildRequestAsync: () => Promise<string | undefined>;
|
||||
}[] = [];
|
||||
if (shouldBuildAndroid) {
|
||||
const sendBuildRequestAsync = await prepareAndroidBuildAsync(commandCtx, easConfig);
|
||||
builds.push({ platform: Platform.ANDROID, sendBuildRequestAsync });
|
||||
}
|
||||
if (shouldBuildIos) {
|
||||
const sendBuildRequestAsync = await prepareIosBuildAsync(commandCtx, easConfig);
|
||||
builds.push({ platform: Platform.IOS, sendBuildRequestAsync });
|
||||
}
|
||||
return (
|
||||
await Promise.all(
|
||||
builds.map(async ({ platform, sendBuildRequestAsync }) => ({
|
||||
platform,
|
||||
buildId: await sendBuildRequestAsync(),
|
||||
}))
|
||||
)
|
||||
).filter((build): build is { platform: Platform; buildId: string } => !!build.buildId);
|
||||
}
|
||||
|
||||
async function waitForBuildEndAsync(
|
||||
buildIds: string[],
|
||||
{ timeoutSec = 3600, intervalSec = 30 } = {}
|
||||
): Promise<(BuildFragment | null)[]> {
|
||||
Log.log('Waiting for build to complete. You can press Ctrl+C to exit.');
|
||||
const spinner = ora().start();
|
||||
let time = new Date().getTime();
|
||||
const endTime = time + timeoutSec * 1000;
|
||||
while (time <= endTime) {
|
||||
const builds: (BuildFragment | null)[] = await Promise.all(
|
||||
buildIds.map(async buildId => {
|
||||
try {
|
||||
return await BuildQuery.byIdAsync(buildId, { useCache: false });
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
if (builds.length === 1) {
|
||||
const build = nullthrows(builds[0]);
|
||||
switch (build.status) {
|
||||
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;
|
||||
case BuildStatus.Canceled:
|
||||
spinner.text = 'Build canceled';
|
||||
spinner.stopAndPersist();
|
||||
return builds;
|
||||
case BuildStatus.InProgress:
|
||||
spinner.text = 'Build in progress...';
|
||||
break;
|
||||
case BuildStatus.Errored:
|
||||
spinner.fail('Build failed');
|
||||
if (build.error) {
|
||||
return builds;
|
||||
} else {
|
||||
throw new Error(`Standalone build failed!`);
|
||||
}
|
||||
default:
|
||||
spinner.warn('Unknown status.');
|
||||
throw new Error(`Unknown status: ${builds} - aborting!`);
|
||||
}
|
||||
} else {
|
||||
if (builds.filter(build => build?.status === BuildStatus.Finished).length === builds.length) {
|
||||
spinner.succeed('All builds have finished');
|
||||
return builds;
|
||||
} else if (
|
||||
builds.filter(build =>
|
||||
build?.status
|
||||
? [BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(
|
||||
build.status
|
||||
)
|
||||
: false
|
||||
).length === builds.length
|
||||
) {
|
||||
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;
|
||||
const finished = builds.filter(build => build?.status === BuildStatus.Finished).length;
|
||||
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}`,
|
||||
errored && chalk.red(`Builds failed: ${errored}`),
|
||||
finished && chalk.green(`Builds finished: ${finished}`),
|
||||
unknownState && chalk.red(`Builds in unknown state: ${unknownState}`),
|
||||
]
|
||||
.filter(i => i)
|
||||
.join('\t');
|
||||
}
|
||||
}
|
||||
time = new Date().getTime();
|
||||
await sleep(intervalSec * 1000);
|
||||
}
|
||||
spinner.warn('Timed out');
|
||||
throw new Error(
|
||||
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
|
||||
);
|
||||
}
|
||||
|
||||
function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
|
||||
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
|
||||
i => i.status === BuildStatus.Errored
|
||||
);
|
||||
if (failedBuilds.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Ios, Job, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
import { EasConfig } from '@expo/eas-json';
|
||||
|
||||
import { IosCredentials } from '../../credentials/ios/types';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
|
||||
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
|
||||
import { resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommandContext, createBuildContextAsync } from '../context';
|
||||
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { Platform } from '../types';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
@@ -16,48 +15,39 @@ import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
|
||||
export async function prepareIosBuildAsync(
|
||||
commandCtx: CommandContext,
|
||||
easConfig: EasConfig
|
||||
): Promise<() => Promise<string | undefined>> {
|
||||
const buildCtx = await createBuildContextAsync<Platform.IOS>({
|
||||
commandCtx,
|
||||
platform: Platform.IOS,
|
||||
easConfig,
|
||||
});
|
||||
const { buildProfile } = buildCtx;
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
): Promise<BuildRequestSender> {
|
||||
const { buildProfile } = ctx;
|
||||
|
||||
if (buildCtx.workflow === Workflow.MANAGED) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(
|
||||
commandCtx.projectDir,
|
||||
commandCtx.exp
|
||||
);
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||
{
|
||||
projectDir: commandCtx.projectDir,
|
||||
nonInteractive: commandCtx.nonInteractive,
|
||||
exp: commandCtx.exp,
|
||||
projectDir: ctx.projectDir,
|
||||
nonInteractive: ctx.nonInteractive,
|
||||
exp: ctx.exp,
|
||||
},
|
||||
buildProfile
|
||||
);
|
||||
const targets = await resolveTargetsAsync(
|
||||
{
|
||||
projectDir: commandCtx.projectDir,
|
||||
exp: commandCtx.exp,
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
},
|
||||
xcodeBuildContext
|
||||
);
|
||||
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx: buildCtx,
|
||||
ctx,
|
||||
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
|
||||
return ensureIosCredentialsAsync(ctx, targets);
|
||||
},
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
await validateAndSyncProjectConfigurationAsync({
|
||||
projectDir: commandCtx.projectDir,
|
||||
exp: commandCtx.exp,
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
buildProfile,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -16,20 +16,16 @@ export async function ensureIosCredentialsAsync(
|
||||
return;
|
||||
}
|
||||
|
||||
const { projectDir } = buildCtx.commandCtx;
|
||||
|
||||
const ctx = await createCredentialsContextAsync(projectDir, {
|
||||
nonInteractive: buildCtx.commandCtx.nonInteractive,
|
||||
const credentialsCtx = await createCredentialsContextAsync(buildCtx.projectDir, {
|
||||
exp: buildCtx.exp,
|
||||
nonInteractive: buildCtx.nonInteractive,
|
||||
});
|
||||
|
||||
const provider = new IosCredentialsProvider(ctx, {
|
||||
app: getAppFromContext(ctx),
|
||||
const provider = new IosCredentialsProvider(credentialsCtx, {
|
||||
app: getAppFromContext(credentialsCtx),
|
||||
targets,
|
||||
iosCapabilitiesOptions: {
|
||||
entitlements: await resolveEntitlementsJsonAsync(
|
||||
buildCtx.commandCtx.projectDir,
|
||||
buildCtx.workflow
|
||||
),
|
||||
entitlements: await resolveEntitlementsJsonAsync(buildCtx.projectDir, buildCtx.workflow),
|
||||
},
|
||||
distribution: buildCtx.buildProfile.distribution ?? 'store',
|
||||
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
|
||||
|
||||
@@ -19,9 +19,8 @@ export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData
|
||||
): Promise<Job> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
|
||||
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
|
||||
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
|
||||
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
|
||||
if (jobData.credentials) {
|
||||
const targetNames = Object.keys(jobData.credentials);
|
||||
@@ -48,7 +47,7 @@ export async function prepareJobAsync(
|
||||
},
|
||||
cache: {
|
||||
...ctx.buildProfile.cache,
|
||||
clear: ctx.commandCtx.clearCache,
|
||||
clear: ctx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
buildCredentials,
|
||||
@@ -69,8 +68,8 @@ export async function prepareJobAsync(
|
||||
|
||||
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 (!ctx.buildProfile.image && ctx.exp.sdkVersion) {
|
||||
const majorSdkVersion = semver.major(ctx.exp.sdkVersion);
|
||||
if (majorSdkVersion <= 41) {
|
||||
return 'macos-catalina-10.15-xcode-12.4';
|
||||
}
|
||||
|
||||
@@ -43,15 +43,15 @@ export async function collectMetadata<T extends Platform>(
|
||||
cliVersion: packageJSON.version,
|
||||
workflow: ctx.workflow,
|
||||
credentialsSource,
|
||||
sdkVersion: ctx.commandCtx.exp.sdkVersion,
|
||||
runtimeVersion: ctx.commandCtx.exp.runtimeVersion,
|
||||
sdkVersion: ctx.exp.sdkVersion,
|
||||
runtimeVersion: ctx.exp.runtimeVersion,
|
||||
...channelOrReleaseChannel,
|
||||
distribution: ctx.buildProfile.distribution ?? 'store',
|
||||
appName: ctx.commandCtx.exp.name,
|
||||
appName: ctx.exp.name,
|
||||
appIdentifier: await resolveAppIdentifierAsync(ctx),
|
||||
buildProfile: ctx.commandCtx.profile,
|
||||
buildProfile: ctx.buildProfileName,
|
||||
gitCommitHash: await vcs.getCommitHashAsync(),
|
||||
username: getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync()),
|
||||
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
|
||||
...(ctx.platform === Platform.IOS && {
|
||||
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
|
||||
ctx as BuildContext<Platform.IOS>
|
||||
@@ -64,9 +64,9 @@ async function resolveAppVersionAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string | undefined> {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
return await readShortVersionAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
return await readShortVersionAsync(ctx.projectDir, ctx.exp);
|
||||
} else {
|
||||
return await readVersionNameAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
return await readVersionNameAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,9 +74,9 @@ async function resolveAppBuildVersionAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string | undefined> {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
return await readBuildNumberAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
return await readBuildNumberAsync(ctx.projectDir, ctx.exp);
|
||||
} else {
|
||||
const versionCode = await readVersionCodeAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
const versionCode = await readVersionCodeAsync(ctx.projectDir, ctx.exp);
|
||||
return versionCode !== undefined ? String(versionCode) : undefined;
|
||||
}
|
||||
}
|
||||
@@ -85,16 +85,16 @@ async function resolveAppIdentifierAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string> {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
return await getBundleIdentifierAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
return await getBundleIdentifierAsync(ctx.projectDir, ctx.exp);
|
||||
} else {
|
||||
return await getApplicationIdAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
return await getApplicationIdAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<{ channel: string } | { releaseChannel: string } | null> {
|
||||
if (!isExpoUpdatesInstalled(ctx.commandCtx.projectDir)) {
|
||||
if (!isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
return null;
|
||||
}
|
||||
if (ctx.buildProfile.channel) {
|
||||
@@ -116,10 +116,10 @@ async function getNativeReleaseChannelAsync<T extends Platform>(
|
||||
): Promise<string> {
|
||||
switch (ctx.platform) {
|
||||
case Platform.ANDROID: {
|
||||
return (await readAndroidReleaseChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? 'default';
|
||||
return (await readAndroidReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
|
||||
}
|
||||
case Platform.IOS: {
|
||||
return (await readIosReleaseChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? 'default';
|
||||
return (await readIosReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
|
||||
}
|
||||
default:
|
||||
return 'default';
|
||||
@@ -131,10 +131,10 @@ async function getNativeChannelAsync<T extends Platform>(
|
||||
): Promise<string | undefined> {
|
||||
switch (ctx.platform) {
|
||||
case Platform.ANDROID: {
|
||||
return (await readAndroidChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? undefined;
|
||||
return (await readAndroidChannelSafelyAsync(ctx.projectDir)) ?? undefined;
|
||||
}
|
||||
case Platform.IOS: {
|
||||
return (await readIosChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? undefined;
|
||||
return (await readIosChannelSafelyAsync(ctx.projectDir)) ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import { getBuildLogsUrl } from './url';
|
||||
|
||||
export function formatGraphQLBuild(build: BuildFragment) {
|
||||
const actor = getActorName(build);
|
||||
const account = build.project.__typename === 'App' ? build.project.ownerAccount.name : 'unknown';
|
||||
const fields: { label: string; value?: string | null }[] = [
|
||||
{ label: 'ID', value: build.id },
|
||||
{
|
||||
@@ -77,7 +76,7 @@ export function formatGraphQLBuild(build: BuildFragment) {
|
||||
},
|
||||
{
|
||||
label: 'Logs',
|
||||
value: getBuildLogsUrl({ buildId: build.id, account }),
|
||||
value: getBuildLogsUrl(build),
|
||||
},
|
||||
{
|
||||
label: 'Artifact',
|
||||
|
||||
@@ -12,51 +12,34 @@ import {
|
||||
EasBuildDeprecationInfoType,
|
||||
} from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import {
|
||||
appPlatformDisplayNames,
|
||||
appPlatformEmojis,
|
||||
requestedPlatformDisplayNames,
|
||||
} from '../constants';
|
||||
import { appPlatformDisplayNames, appPlatformEmojis } from '../constants';
|
||||
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
|
||||
|
||||
export function printLogsUrls(
|
||||
accountName: string,
|
||||
builds: { platform: 'android' | 'ios'; buildId: string }[]
|
||||
): void {
|
||||
export function printLogsUrls(builds: BuildFragment[]): void {
|
||||
if (builds.length === 1) {
|
||||
const { buildId } = builds[0];
|
||||
const logsUrl = getBuildLogsUrl({
|
||||
buildId,
|
||||
account: accountName,
|
||||
});
|
||||
Log.log(`Build details: ${chalk.underline(logsUrl)}`);
|
||||
Log.log(`Build details: ${chalk.underline(getBuildLogsUrl(builds[0]))}`);
|
||||
} else {
|
||||
builds.forEach(({ buildId, platform }) => {
|
||||
const logsUrl = getBuildLogsUrl({
|
||||
buildId,
|
||||
account: accountName,
|
||||
});
|
||||
builds.forEach(build => {
|
||||
const logsUrl = getBuildLogsUrl(build);
|
||||
Log.log(
|
||||
`${requestedPlatformDisplayNames[platform]} build details: ${chalk.underline(logsUrl)}`
|
||||
`${appPlatformDisplayNames[build.platform]} build details: ${chalk.underline(logsUrl)}`
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function printBuildResults(accountName: string, builds: (BuildFragment | null)[]): void {
|
||||
export function printBuildResults(builds: (BuildFragment | null)[]): void {
|
||||
Log.newLine();
|
||||
if (builds.length === 1) {
|
||||
const [build] = builds;
|
||||
assert(build, 'Build should be defined');
|
||||
printBuildResult(accountName, build);
|
||||
printBuildResult(build);
|
||||
} else {
|
||||
(builds.filter(i => i) as BuildFragment[]).forEach(build =>
|
||||
printBuildResult(accountName, build)
|
||||
);
|
||||
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
|
||||
}
|
||||
}
|
||||
|
||||
function printBuildResult(accountName: string, build: BuildFragment): void {
|
||||
function printBuildResult(build: BuildFragment): void {
|
||||
Log.addNewLineIfNone();
|
||||
if (build.status === BuildStatus.Errored) {
|
||||
const userError = build.error;
|
||||
@@ -80,10 +63,7 @@ function printBuildResult(accountName: string, build: BuildFragment): void {
|
||||
}
|
||||
|
||||
if (build.distribution === DistributionType.Internal) {
|
||||
const logsUrl = getBuildLogsUrl({
|
||||
buildId: build.id,
|
||||
account: accountName,
|
||||
});
|
||||
const logsUrl = getBuildLogsUrl(build);
|
||||
const installUrl = getInternalDistributionInstallUrl(build);
|
||||
qrcodeTerminal.generate(installUrl, code => Log.log(`${indentString(code, 2)}\n`));
|
||||
Log.log(
|
||||
|
||||
@@ -3,14 +3,15 @@ import assert from 'assert';
|
||||
import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { AppPlatform, BuildFragment } from '../../graphql/generated';
|
||||
|
||||
export function getBuildLogsUrl({
|
||||
buildId,
|
||||
account,
|
||||
}: {
|
||||
buildId: string;
|
||||
account: string;
|
||||
}): string {
|
||||
return `${getExpoWebsiteBaseUrl()}/accounts/${account}/builds/${buildId}`;
|
||||
export function getBuildLogsUrl(build: BuildFragment): string {
|
||||
const { project } = build;
|
||||
if (project.__typename === 'App') {
|
||||
return `${getExpoWebsiteBaseUrl()}/accounts/${project.ownerAccount.name}/projects/${
|
||||
project.name
|
||||
}/builds/${build.id}`;
|
||||
} else {
|
||||
return `${getExpoWebsiteBaseUrl()}/builds/${build.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getArtifactUrl(artifactId: string): string {
|
||||
|
||||
@@ -1,24 +1,47 @@
|
||||
import { ExpoConfig, getConfig } from '@expo/config';
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { error, exit } from '@oclif/errors';
|
||||
|
||||
import { configureAsync } from '../../build/configure';
|
||||
import { createCommandContextAsync } from '../../build/context';
|
||||
import { buildAsync } from '../../build/create';
|
||||
import { RequestedPlatform } from '../../build/types';
|
||||
import { prepareAndroidBuildAsync } from '../../build/android/build';
|
||||
import { BuildRequestSender, waitForBuildEndAsync } from '../../build/build';
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import { BuildContext, createBuildContextAsync } from '../../build/context';
|
||||
import { prepareIosBuildAsync } from '../../build/ios/build';
|
||||
import { Platform, RequestedPlatform } from '../../build/types';
|
||||
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
|
||||
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { BuildFragment, BuildStatus } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
EAS_UNAVAILABLE_MESSAGE,
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import vcs from '../../vcs';
|
||||
|
||||
interface RawBuildFlags {
|
||||
platform?: string;
|
||||
'skip-credentials-check': boolean;
|
||||
'skip-project-configuration': boolean;
|
||||
profile: string;
|
||||
'non-interactive': boolean;
|
||||
local: boolean;
|
||||
wait: boolean;
|
||||
'clear-cache': boolean;
|
||||
}
|
||||
|
||||
interface BuildFlags {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
skipProjectConfiguration: boolean;
|
||||
profile: string;
|
||||
nonInteractive: boolean;
|
||||
local: boolean;
|
||||
wait: boolean;
|
||||
clearCache: boolean;
|
||||
}
|
||||
|
||||
export default class Build extends EasCommand {
|
||||
static description = 'Start a build';
|
||||
|
||||
@@ -56,155 +79,134 @@ export default class Build extends EasCommand {
|
||||
};
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { flags } = this.parse(Build);
|
||||
const { flags: rawFlags } = this.parse(Build);
|
||||
|
||||
const flags = await this.sanitizeFlagsAsync(rawFlags);
|
||||
const { requestedPlatform } = flags;
|
||||
|
||||
await vcs.ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(flags.nonInteractive);
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
|
||||
|
||||
const easConfig = await new EasJsonReader(projectDir, requestedPlatform).readAsync(
|
||||
flags.profile
|
||||
);
|
||||
const platformsToBuild = this.getPlatformsToBuild(requestedPlatform);
|
||||
|
||||
const startedBuilds: BuildFragment[] = [];
|
||||
for (const platform of platformsToBuild) {
|
||||
const ctx = await createBuildContextAsync({
|
||||
buildProfileName: flags.profile,
|
||||
clearCache: flags.clearCache,
|
||||
easConfig,
|
||||
local: flags.local,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
platform,
|
||||
projectDir,
|
||||
skipProjectConfiguration: flags.skipProjectConfiguration,
|
||||
});
|
||||
|
||||
if (!ctx.local && !(await isEasEnabledForProjectAsync(ctx.projectId))) {
|
||||
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
|
||||
}
|
||||
|
||||
const maybeBuild = await this.startBuildAsync(ctx);
|
||||
if (maybeBuild) {
|
||||
startedBuilds.push(maybeBuild);
|
||||
}
|
||||
}
|
||||
if (flags.local) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
printLogsUrls(startedBuilds);
|
||||
Log.newLine();
|
||||
|
||||
if (flags.wait) {
|
||||
const builds = await waitForBuildEndAsync(startedBuilds.map(build => build.id));
|
||||
printBuildResults(builds);
|
||||
this.exitWithNonZeroCodeIfSomeBuildsFailed(builds);
|
||||
}
|
||||
}
|
||||
|
||||
private async sanitizeFlagsAsync(flags: RawBuildFlags): Promise<BuildFlags> {
|
||||
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 requestedPlatform =
|
||||
(flags.platform as RequestedPlatform | undefined) ?? (await this.promptForPlatformAsync());
|
||||
|
||||
if (flags.local) {
|
||||
if (requestedPlatform === RequestedPlatform.All) {
|
||||
error('Builds for multiple platforms are not supported with flag --local', { exit: 1 });
|
||||
} else if (process.platform !== 'darwin' && requestedPlatform === RequestedPlatform.Ios) {
|
||||
error('Unsupported platform, macOS is required to build apps for iOS', { exit: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
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.'
|
||||
'Build credentials 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, platform)) ?? exp;
|
||||
|
||||
const commandCtx = await createCommandContextAsync({
|
||||
requestedPlatform: platform,
|
||||
profile: flags.profile,
|
||||
exp,
|
||||
projectDir,
|
||||
projectId,
|
||||
nonInteractive,
|
||||
clearCache: flags['clear-cache'],
|
||||
local: flags.local,
|
||||
return {
|
||||
requestedPlatform,
|
||||
skipProjectConfiguration: flags['skip-project-configuration'],
|
||||
waitForBuildEnd: flags.wait,
|
||||
profile: flags['profile'],
|
||||
nonInteractive,
|
||||
local: flags['local'],
|
||||
wait: flags['wait'],
|
||||
clearCache: flags['clear-cache'],
|
||||
};
|
||||
}
|
||||
|
||||
private async promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Build for platforms',
|
||||
name: 'platform',
|
||||
choices: [
|
||||
{ title: 'All', value: RequestedPlatform.All },
|
||||
{ title: 'iOS', value: RequestedPlatform.Ios },
|
||||
{ title: 'Android', value: RequestedPlatform.Android },
|
||||
],
|
||||
});
|
||||
|
||||
await buildAsync(commandCtx);
|
||||
}
|
||||
}
|
||||
|
||||
function verifyOptionsForLocalBuilds(platform: RequestedPlatform): boolean {
|
||||
if (platform === RequestedPlatform.All) {
|
||||
Log.error('Builds for multiple platforms are not supported with flag --local');
|
||||
return false;
|
||||
} else if (process.platform !== 'darwin' && platform === RequestedPlatform.Ios) {
|
||||
Log.error('Unsupported platform, macOS is required to build apps for iOS');
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Build for platforms',
|
||||
name: 'platform',
|
||||
choices: [
|
||||
{
|
||||
title: 'All',
|
||||
value: RequestedPlatform.All,
|
||||
},
|
||||
{
|
||||
title: 'iOS',
|
||||
value: RequestedPlatform.Ios,
|
||||
},
|
||||
{
|
||||
title: 'Android',
|
||||
value: RequestedPlatform.Android,
|
||||
},
|
||||
],
|
||||
});
|
||||
return platform;
|
||||
}
|
||||
|
||||
async function ensureProjectConfiguredAsync(
|
||||
projectDir: string,
|
||||
platform: RequestedPlatform
|
||||
): Promise<ExpoConfig | null> {
|
||||
const platformsToConfigure = await getPlatformsToConfigureAsync(projectDir, platform);
|
||||
|
||||
if (!platformsToConfigure) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure the prompt is consistent with the platforms we need to configure
|
||||
let message = 'This project is not configured to build with EAS. Set it up now?';
|
||||
if (platformsToConfigure === RequestedPlatform.Ios) {
|
||||
message = 'Your iOS project is not configured to build with EAS. Set it up now?';
|
||||
} else if (platformsToConfigure === RequestedPlatform.Android) {
|
||||
message = 'Your Android project is not configured to build with EAS. Set it up now?';
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({ message });
|
||||
if (confirm) {
|
||||
await configureAsync({
|
||||
projectDir,
|
||||
platform: platformsToConfigure,
|
||||
});
|
||||
if (await vcs.hasUncommittedChangesAsync()) {
|
||||
throw new Error(
|
||||
'Build process requires clean working tree, please commit all your changes and run `eas build` again'
|
||||
);
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
return exp;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
|
||||
'https://docs.expo.io/build/eas-json'
|
||||
)})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function getPlatformsToConfigureAsync(
|
||||
projectDir: string,
|
||||
platform: RequestedPlatform
|
||||
): Promise<RequestedPlatform | null> {
|
||||
if (!(await fs.pathExists(path.join(projectDir, 'eas.json')))) {
|
||||
return platform;
|
||||
}
|
||||
|
||||
const easConfig = await new EasJsonReader(projectDir, platform).readRawAsync();
|
||||
if (platform === RequestedPlatform.All) {
|
||||
if (easConfig.builds?.android && easConfig.builds?.ios) {
|
||||
return null;
|
||||
} else if (easConfig.builds?.ios) {
|
||||
return RequestedPlatform.Android;
|
||||
} else if (easConfig.builds?.android) {
|
||||
return RequestedPlatform.Ios;
|
||||
private getPlatformsToBuild(requestedPlatform: RequestedPlatform): Platform[] {
|
||||
if (requestedPlatform === RequestedPlatform.All) {
|
||||
return [Platform.ANDROID, Platform.IOS];
|
||||
} else if (requestedPlatform === RequestedPlatform.Android) {
|
||||
return [Platform.ANDROID];
|
||||
} else {
|
||||
return [Platform.IOS];
|
||||
}
|
||||
} else if (
|
||||
(platform === RequestedPlatform.Android || platform === RequestedPlatform.Ios) &&
|
||||
easConfig.builds?.[platform]
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return platform;
|
||||
private async startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
|
||||
let sendBuildRequestAsync: BuildRequestSender;
|
||||
if (ctx.platform === Platform.ANDROID) {
|
||||
sendBuildRequestAsync = await prepareAndroidBuildAsync(ctx as BuildContext<Platform.ANDROID>);
|
||||
} else {
|
||||
sendBuildRequestAsync = await prepareIosBuildAsync(ctx as BuildContext<Platform.IOS>);
|
||||
}
|
||||
return await sendBuildRequestAsync();
|
||||
}
|
||||
|
||||
private exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
|
||||
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
|
||||
i => i.status === BuildStatus.Errored
|
||||
);
|
||||
if (failedBuilds.length > 0) {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,9 +35,11 @@ export default class BuildList extends Command {
|
||||
};
|
||||
|
||||
async run() {
|
||||
const { platform: requestedPlatform, status: buildStatus, limit = 10 } = this.parse(
|
||||
BuildList
|
||||
).flags;
|
||||
const {
|
||||
platform: requestedPlatform,
|
||||
status: buildStatus,
|
||||
limit = 10,
|
||||
} = this.parse(BuildList).flags;
|
||||
|
||||
const platform = toAppPlatform(requestedPlatform);
|
||||
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
|
||||
|
||||
@@ -37,9 +37,7 @@ async function promptForRolloutPercentAsync({
|
||||
return rolloutPercent;
|
||||
}
|
||||
|
||||
function getRolloutInfo(
|
||||
getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery
|
||||
): {
|
||||
function getRolloutInfo(getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery): {
|
||||
newBranch: Pick<UpdateBranch, 'name' | 'id'>;
|
||||
oldBranch: Pick<UpdateBranch, 'name' | 'id'>;
|
||||
currentPercent: number;
|
||||
@@ -48,12 +46,14 @@ function getRolloutInfo(
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.branchMapping
|
||||
);
|
||||
const [newBranchId, oldBranchId] = branchMapping.data.map(d => d.branchId);
|
||||
const newBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === newBranchId
|
||||
)[0];
|
||||
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === oldBranchId
|
||||
)[0];
|
||||
const newBranch =
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === newBranchId
|
||||
)[0];
|
||||
const oldBranch =
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === oldBranchId
|
||||
)[0];
|
||||
|
||||
if (!newBranch || !oldBranch) {
|
||||
throw new Error(
|
||||
@@ -140,9 +140,10 @@ async function startRolloutAsync({
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
|
||||
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === oldBranchId
|
||||
)[0];
|
||||
const oldBranch =
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === oldBranchId
|
||||
)[0];
|
||||
if (!oldBranch) {
|
||||
throw new Error(
|
||||
`Branch mapping is missing its only branch for channel "${channelName}" on app "${fullName}"`
|
||||
|
||||
@@ -37,9 +37,11 @@ export type BranchMapping = {
|
||||
* Get the branch mapping and determine whether it is a rollout.
|
||||
* Ensure that the branch mapping is properly formatted.
|
||||
*/
|
||||
export function getBranchMapping(
|
||||
branchMappingString?: string
|
||||
): { branchMapping: BranchMapping; isRollout: boolean; rolloutPercent?: number } {
|
||||
export function getBranchMapping(branchMappingString?: string): {
|
||||
branchMapping: BranchMapping;
|
||||
isRollout: boolean;
|
||||
rolloutPercent?: number;
|
||||
} {
|
||||
if (!branchMappingString) {
|
||||
throw new Error('Missing branch mapping.');
|
||||
}
|
||||
|
||||
@@ -13,9 +13,8 @@ export class AssignFcm {
|
||||
ctx: Context,
|
||||
fcm: AndroidFcmFragment
|
||||
): Promise<CommonAndroidAppCredentialsFragment> {
|
||||
const appCredentials = await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
|
||||
this.app
|
||||
);
|
||||
const appCredentials =
|
||||
await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(this.app);
|
||||
const updatedAppCredentials = await ctx.android.updateAndroidAppCredentialsAsync(
|
||||
appCredentials,
|
||||
{
|
||||
|
||||
@@ -23,9 +23,8 @@ export async function canCopyLegacyCredentialsAsync(
|
||||
return false; // modern credentials already exist
|
||||
}
|
||||
|
||||
const legacyAppCredentials = await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(
|
||||
app
|
||||
);
|
||||
const legacyAppCredentials =
|
||||
await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(app);
|
||||
return !!legacyAppCredentials; // user has some legacy credentials
|
||||
}
|
||||
|
||||
@@ -41,16 +40,14 @@ export async function promptUserAndCopyLegacyCredentialsAsync(
|
||||
const spinner = ora('Classic credentials detected, copying to EAS...').start();
|
||||
|
||||
try {
|
||||
const legacyAppCredentials = await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(
|
||||
app
|
||||
);
|
||||
const legacyAppCredentials =
|
||||
await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(app);
|
||||
if (!legacyAppCredentials) {
|
||||
return;
|
||||
}
|
||||
|
||||
const appCredentials = await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
|
||||
app
|
||||
);
|
||||
const appCredentials =
|
||||
await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(app);
|
||||
const legacyFcm = legacyAppCredentials.androidFcm;
|
||||
if (legacyFcm) {
|
||||
const clonedFcm = await ctx.android.createFcmAsync(
|
||||
@@ -120,9 +117,8 @@ export async function createOrUpdateDefaultAndroidAppBuildCredentialsAsync(
|
||||
!ctx.nonInteractive,
|
||||
'createOrUpdateDefaultAndroidAppBuildCredentialsAsync must be run in interactive mode'
|
||||
);
|
||||
const existingDefaultBuildCredentials = await ctx.android.getDefaultAndroidAppBuildCredentialsAsync(
|
||||
appLookupParams
|
||||
);
|
||||
const existingDefaultBuildCredentials =
|
||||
await ctx.android.getDefaultAndroidAppBuildCredentialsAsync(appLookupParams);
|
||||
if (existingDefaultBuildCredentials) {
|
||||
return await ctx.android.updateAndroidAppBuildCredentialsAsync(
|
||||
existingDefaultBuildCredentials,
|
||||
|
||||
@@ -25,6 +25,7 @@ export class CreateKeystore {
|
||||
if (providedKeystore) {
|
||||
const providedKeystoreWithType = getKeystoreWithType(providedKeystore);
|
||||
validateKeystore(providedKeystoreWithType);
|
||||
return providedKeystoreWithType;
|
||||
}
|
||||
return await generateRandomKeystoreAsync();
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import { testKeystore } from '../../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { askForUserProvidedAsync } from '../../../utils/promptForCredentials';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { CreateKeystore } from '../CreateKeystore';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
|
||||
jest.mock('../../../utils/promptForCredentials');
|
||||
|
||||
describe('CreateKeystore', () => {
|
||||
it('creates a keystore in Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: false });
|
||||
@@ -16,6 +20,20 @@ describe('CreateKeystore', () => {
|
||||
// expect keystore to be created on expo servers
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('creates a keystore by uploading', async () => {
|
||||
(askForUserProvidedAsync as jest.Mock).mockImplementation(() => testKeystore);
|
||||
const ctx = createCtxMock({ nonInteractive: false });
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
|
||||
await createKeystoreAction.runAsync(ctx);
|
||||
|
||||
// expect keystore to be created on expo servers
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledWith(
|
||||
appLookupParams.account,
|
||||
expect.objectContaining(testKeystore)
|
||||
);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: true });
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
|
||||
@@ -120,9 +120,8 @@ export async function createAndroidAppBuildCredentialsAsync(
|
||||
androidKeystoreId: string;
|
||||
}
|
||||
): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
const androidAppCredentials = await createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
|
||||
appLookupParams
|
||||
);
|
||||
const androidAppCredentials =
|
||||
await createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(appLookupParams);
|
||||
const buildCredentialsList = androidAppCredentials.androidAppBuildCredentialsList;
|
||||
const existingDefaultBuildCredentials =
|
||||
buildCredentialsList.find(buildCredentials => buildCredentials.isDefault) ?? null;
|
||||
|
||||
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
||||
import pick from 'lodash/pick';
|
||||
|
||||
import Log from '../log';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getProjectAccountName } from '../project/projectUtils';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { Actor, getActorDisplayName } from '../user/User';
|
||||
@@ -23,6 +24,7 @@ interface AppleCtxOptions {
|
||||
|
||||
interface Options extends AppleCtxOptions {
|
||||
nonInteractive?: boolean;
|
||||
exp?: ExpoConfig;
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
@@ -45,12 +47,13 @@ export async function createCredentialsContextAsync(
|
||||
): Promise<Context> {
|
||||
const user = await ensureLoggedInAsync();
|
||||
|
||||
let expoConfig: ExpoConfig | undefined;
|
||||
try {
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
expoConfig = exp;
|
||||
} catch (error) {
|
||||
// ignore error, context might be created outside of expo project
|
||||
let expoConfig: ExpoConfig | undefined = options.exp;
|
||||
if (!expoConfig) {
|
||||
try {
|
||||
expoConfig = getExpoConfig(projectDir);
|
||||
} catch (error) {
|
||||
// ignore error, context might be created outside of expo project
|
||||
}
|
||||
}
|
||||
|
||||
return new CredentialsContext(projectDir, user, expoConfig, options);
|
||||
|
||||
@@ -67,12 +67,8 @@ export class ConfigureProvisioningProfile {
|
||||
app: AppLookupParams,
|
||||
profileFromApple: ProvisioningProfileStoreInfo
|
||||
): Promise<AppleProvisioningProfileMutationResult> {
|
||||
const {
|
||||
developerPortalIdentifier,
|
||||
certificateP12,
|
||||
certificatePassword,
|
||||
serialNumber,
|
||||
} = this.distributionCertificate;
|
||||
const { developerPortalIdentifier, certificateP12, certificatePassword, serialNumber } =
|
||||
this.distributionCertificate;
|
||||
assert(
|
||||
certificateP12 && certificatePassword,
|
||||
'Distribution Certificate P12 and Password is required'
|
||||
|
||||
@@ -101,11 +101,12 @@ export class SetupAdhocProvisioningProfile {
|
||||
const chosenDevices = await chooseDevices(registeredAppleDevices, provisionedDeviceIdentifiers);
|
||||
|
||||
// 4. Reuse or create the profile on Apple Developer Portal
|
||||
const provisioningProfileStoreInfo = await ctx.appStore.createOrReuseAdhocProvisioningProfileAsync(
|
||||
chosenDevices.map(({ identifier }) => identifier),
|
||||
this.app.bundleIdentifier,
|
||||
distCert.serialNumber
|
||||
);
|
||||
const provisioningProfileStoreInfo =
|
||||
await ctx.appStore.createOrReuseAdhocProvisioningProfileAsync(
|
||||
chosenDevices.map(({ identifier }) => identifier),
|
||||
this.app.bundleIdentifier,
|
||||
distCert.serialNumber
|
||||
);
|
||||
|
||||
// 5. Create or update the profile on servers
|
||||
const appleAppIdentifier = await ctx.ios.createOrGetExistingAppleAppIdentifierAsync(
|
||||
|
||||
+24
-20
@@ -52,11 +52,12 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupBuildCredentialsFromCredentialsJsonAction = new SetupTargetBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore,
|
||||
targetCredentials
|
||||
);
|
||||
const setupBuildCredentialsFromCredentialsJsonAction =
|
||||
new SetupTargetBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore,
|
||||
targetCredentials
|
||||
);
|
||||
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
|
||||
|
||||
// expect build credentials not to be created or updated on expo servers
|
||||
@@ -80,11 +81,12 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupBuildCredentialsFromCredentialsJsonAction = new SetupTargetBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore,
|
||||
targetCredentials
|
||||
);
|
||||
const setupBuildCredentialsFromCredentialsJsonAction =
|
||||
new SetupTargetBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore,
|
||||
targetCredentials
|
||||
);
|
||||
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
|
||||
|
||||
// expect build credentials to be created or updated on expo servers
|
||||
@@ -112,11 +114,12 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupBuildCredentialsFromCredentialsJsonAction = new SetupTargetBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore,
|
||||
targetCredentials
|
||||
);
|
||||
const setupBuildCredentialsFromCredentialsJsonAction =
|
||||
new SetupTargetBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore,
|
||||
targetCredentials
|
||||
);
|
||||
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
|
||||
|
||||
// expect build credentials to be created or updated on expo servers
|
||||
@@ -145,11 +148,12 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
||||
const setupBuildCredentialsFromCredentialsJsonAction = new SetupTargetBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore,
|
||||
targetCredentials
|
||||
);
|
||||
const setupBuildCredentialsFromCredentialsJsonAction =
|
||||
new SetupTargetBuildCredentialsFromCredentialsJson(
|
||||
appLookupParams,
|
||||
IosDistributionType.AppStore,
|
||||
targetCredentials
|
||||
);
|
||||
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
|
||||
|
||||
// expect no prompts to be called
|
||||
|
||||
@@ -25,24 +25,30 @@ const validateBooleanOptions = (options: any): boolean => {
|
||||
return typeof options === 'boolean';
|
||||
};
|
||||
|
||||
const validatePrefixedStringArrayOptions = (prefix: string) => (options: any): boolean => {
|
||||
return (
|
||||
Array.isArray(options) &&
|
||||
options.every(option => typeof option === 'string' && option.startsWith(prefix))
|
||||
);
|
||||
};
|
||||
const validatePrefixedStringArrayOptions =
|
||||
(prefix: string) =>
|
||||
(options: any): boolean => {
|
||||
return (
|
||||
Array.isArray(options) &&
|
||||
options.every(option => typeof option === 'string' && option.startsWith(prefix))
|
||||
);
|
||||
};
|
||||
|
||||
const validateStringArrayOptions = (options: any): boolean => {
|
||||
return Array.isArray(options) && options.every(option => typeof option === 'string');
|
||||
};
|
||||
|
||||
const createValidateStringOptions = (allowed: string[]) => (options: any): boolean => {
|
||||
return allowed.includes(options);
|
||||
};
|
||||
const createValidateStringOptions =
|
||||
(allowed: string[]) =>
|
||||
(options: any): boolean => {
|
||||
return allowed.includes(options);
|
||||
};
|
||||
|
||||
const createValidateStringArrayOptions = (allowed: string[]) => (options: any): boolean => {
|
||||
return Array.isArray(options) && options.every(option => allowed.includes(option));
|
||||
};
|
||||
const createValidateStringArrayOptions =
|
||||
(allowed: string[]) =>
|
||||
(options: any): boolean => {
|
||||
return Array.isArray(options) && options.every(option => allowed.includes(option));
|
||||
};
|
||||
|
||||
const validateDevProdString = createValidateStringOptions(['development', 'production']);
|
||||
|
||||
|
||||
@@ -73,9 +73,8 @@ async function findProfileByBundleIdAsync(
|
||||
// there is an expo managed profile with our desired certificate
|
||||
// return the profile that will be valid for the longest duration
|
||||
return {
|
||||
profile: expoProfilesWithCertificate.sort(sortByExpiration)[
|
||||
expoProfilesWithCertificate.length - 1
|
||||
],
|
||||
profile:
|
||||
expoProfilesWithCertificate.sort(sortByExpiration)[expoProfilesWithCertificate.length - 1],
|
||||
didUpdate: false,
|
||||
};
|
||||
} else if (expoProfiles) {
|
||||
@@ -228,16 +227,12 @@ export async function createOrReuseAdhocProvisioningProfileAsync(
|
||||
const spinner = ora(`Handling Apple ad hoc provisioning profiles`).start();
|
||||
try {
|
||||
const context = getRequestContext(authCtx);
|
||||
const {
|
||||
didUpdate,
|
||||
didCreate,
|
||||
profileName,
|
||||
...adhocProvisioningProfile
|
||||
} = await manageAdHocProfilesAsync(context, {
|
||||
udids,
|
||||
bundleId: bundleIdentifier,
|
||||
certSerialNumber: distCertSerialNumber,
|
||||
});
|
||||
const { didUpdate, didCreate, profileName, ...adhocProvisioningProfile } =
|
||||
await manageAdHocProfilesAsync(context, {
|
||||
udids,
|
||||
bundleId: bundleIdentifier,
|
||||
certSerialNumber: distCertSerialNumber,
|
||||
});
|
||||
|
||||
if (didCreate) {
|
||||
spinner.succeed(`Created new profile: ${profileName}`);
|
||||
|
||||
@@ -21,12 +21,10 @@ describe('print credentials', () => {
|
||||
},
|
||||
projectName: 'test52',
|
||||
};
|
||||
const testIosAppCredentialsData = await IosAppCredentialsQuery.withCommonFieldsByAppIdentifierIdAsync(
|
||||
'@quinlanj/test52',
|
||||
{
|
||||
const testIosAppCredentialsData =
|
||||
await IosAppCredentialsQuery.withCommonFieldsByAppIdentifierIdAsync('@quinlanj/test52', {
|
||||
appleAppIdentifierId: 'test-id',
|
||||
}
|
||||
);
|
||||
});
|
||||
const appCredentials = {
|
||||
test52: nullthrows(testIosAppCredentialsData),
|
||||
};
|
||||
|
||||
@@ -162,14 +162,8 @@ function displayIosAppBuildCredentials(buildCredentials: IosAppBuildCredentialsF
|
||||
const maybeProvProf = buildCredentials.provisioningProfile;
|
||||
Log.log(` Provisioning Profile:`);
|
||||
if (maybeProvProf) {
|
||||
const {
|
||||
expiration,
|
||||
updatedAt,
|
||||
status,
|
||||
developerPortalIdentifier,
|
||||
appleTeam,
|
||||
appleDevices,
|
||||
} = maybeProvProf;
|
||||
const { expiration, updatedAt, status, developerPortalIdentifier, appleTeam, appleDevices } =
|
||||
maybeProvProf;
|
||||
if (developerPortalIdentifier) {
|
||||
Log.log(` Developer Portal ID: ${developerPortalIdentifier}`);
|
||||
}
|
||||
|
||||
@@ -201,9 +201,8 @@ export class ManageIos {
|
||||
const iosAppCredentialsMap: IosAppCredentialsMap = {};
|
||||
for (const target of targets) {
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx, target);
|
||||
iosAppCredentialsMap[
|
||||
target.targetName
|
||||
] = await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(appLookupParams);
|
||||
iosAppCredentialsMap[target.targetName] =
|
||||
await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(appLookupParams);
|
||||
}
|
||||
displayIosCredentials(app, iosAppCredentialsMap, targets);
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@ export enum SelectAndroidBuildCredentialsResultType {
|
||||
export class SelectAndroidBuildCredentials {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
async runAsync(
|
||||
ctx: Context
|
||||
): Promise<
|
||||
async runAsync(ctx: Context): Promise<
|
||||
| {
|
||||
resultType: SelectAndroidBuildCredentialsResultType.CREATE_REQUEST;
|
||||
result: AndroidAppBuildCredentialsMetadataInput;
|
||||
|
||||
+14
-21
@@ -13,9 +13,8 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
});
|
||||
const selectIosDistributionTypeGraphqlFromBuildProfileAction = new SelectIosDistributionTypeGraphqlFromBuildProfile(
|
||||
buildProfile
|
||||
);
|
||||
const selectIosDistributionTypeGraphqlFromBuildProfileAction =
|
||||
new SelectIosDistributionTypeGraphqlFromBuildProfile(buildProfile);
|
||||
await expect(
|
||||
selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(ctx)
|
||||
).rejects.toThrowError();
|
||||
@@ -28,12 +27,10 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
});
|
||||
const selectIosDistributionTypeGraphqlFromBuildProfileAction = new SelectIosDistributionTypeGraphqlFromBuildProfile(
|
||||
buildProfile
|
||||
);
|
||||
const iosDistributionTypeGraphql = await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(
|
||||
ctx
|
||||
);
|
||||
const selectIosDistributionTypeGraphqlFromBuildProfileAction =
|
||||
new SelectIosDistributionTypeGraphqlFromBuildProfile(buildProfile);
|
||||
const iosDistributionTypeGraphql =
|
||||
await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(ctx);
|
||||
expect(iosDistributionTypeGraphql).toBe(IosDistributionTypeGraphql.AppStore);
|
||||
});
|
||||
it('returns ENTERPRISE type with an internal distribution with the universal provisioningEnterprise configuration', async () => {
|
||||
@@ -45,12 +42,10 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
});
|
||||
const selectIosDistributionTypeGraphqlFromBuildProfileAction = new SelectIosDistributionTypeGraphqlFromBuildProfile(
|
||||
buildProfile
|
||||
);
|
||||
const iosDistributionTypeGraphql = await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(
|
||||
ctx
|
||||
);
|
||||
const selectIosDistributionTypeGraphqlFromBuildProfileAction =
|
||||
new SelectIosDistributionTypeGraphqlFromBuildProfile(buildProfile);
|
||||
const iosDistributionTypeGraphql =
|
||||
await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(ctx);
|
||||
expect(iosDistributionTypeGraphql).toBe(IosDistributionTypeGraphql.Enterprise);
|
||||
});
|
||||
it('returns ADHOC type with an internal distribution with the adhoc provisioningEnterprise configuration', async () => {
|
||||
@@ -62,12 +57,10 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
});
|
||||
const selectIosDistributionTypeGraphqlFromBuildProfileAction = new SelectIosDistributionTypeGraphqlFromBuildProfile(
|
||||
buildProfile
|
||||
);
|
||||
const iosDistributionTypeGraphql = await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(
|
||||
ctx
|
||||
);
|
||||
const selectIosDistributionTypeGraphqlFromBuildProfileAction =
|
||||
new SelectIosDistributionTypeGraphqlFromBuildProfile(buildProfile);
|
||||
const iosDistributionTypeGraphql =
|
||||
await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(ctx);
|
||||
expect(iosDistributionTypeGraphql).toBe(IosDistributionTypeGraphql.AdHoc);
|
||||
});
|
||||
});
|
||||
@@ -26,10 +26,11 @@ async function generateDeviceRegistrationURLAsync(
|
||||
accountId: string,
|
||||
appleTeam: Pick<AppleTeam, 'id'>
|
||||
) {
|
||||
const appleDeviceRegistrationRequest = await AppleDeviceRegistrationRequestMutation.createAppleDeviceRegistrationRequestAsync(
|
||||
appleTeam.id,
|
||||
accountId
|
||||
);
|
||||
const appleDeviceRegistrationRequest =
|
||||
await AppleDeviceRegistrationRequestMutation.createAppleDeviceRegistrationRequestAsync(
|
||||
appleTeam.id,
|
||||
accountId
|
||||
);
|
||||
return formatRegistrationURL(appleDeviceRegistrationRequest.id);
|
||||
}
|
||||
|
||||
|
||||
+16
-2
@@ -629,6 +629,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
|
||||
/** @deprecated User type is deprecated */
|
||||
initiatingUser?: Maybe<User>;
|
||||
initiatingActor?: Maybe<Actor>;
|
||||
cancelingActor?: Maybe<Actor>;
|
||||
artifacts?: Maybe<BuildArtifacts>;
|
||||
logFiles: Array<Scalars['String']>;
|
||||
updatedAt?: Maybe<Scalars['DateTime']>;
|
||||
@@ -1247,6 +1248,7 @@ export type UpdateBranch = {
|
||||
createdAt: Scalars['DateTime'];
|
||||
updatedAt: Scalars['DateTime'];
|
||||
updates: Array<Update>;
|
||||
mostRecentUpdateForEachPlatform: Array<Update>;
|
||||
};
|
||||
|
||||
|
||||
@@ -3422,6 +3424,16 @@ export type DeleteWebhookResult = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
|
||||
export type Deployment = {
|
||||
__typename?: 'Deployment';
|
||||
id?: Maybe<Scalars['String']>;
|
||||
runtimeVersion?: Maybe<Scalars['String']>;
|
||||
channel?: Maybe<Scalars['String']>;
|
||||
recentBuilds?: Maybe<Array<Maybe<Build>>>;
|
||||
branchToReceive?: Maybe<UpdateBranch>;
|
||||
};
|
||||
|
||||
export enum CacheControlScope {
|
||||
Public = 'PUBLIC',
|
||||
Private = 'PRIVATE'
|
||||
@@ -4630,6 +4642,7 @@ export type CreateAndroidBuildMutation = (
|
||||
& { build: (
|
||||
{ __typename?: 'Build' }
|
||||
& Pick<Build, 'id'>
|
||||
& BuildFragment
|
||||
), deprecationInfo?: Maybe<(
|
||||
{ __typename?: 'EASBuildDeprecationInfo' }
|
||||
& Pick<EasBuildDeprecationInfo, 'type' | 'message'>
|
||||
@@ -4654,6 +4667,7 @@ export type CreateIosBuildMutation = (
|
||||
& { build: (
|
||||
{ __typename?: 'Build' }
|
||||
& Pick<Build, 'id'>
|
||||
& BuildFragment
|
||||
), deprecationInfo?: Maybe<(
|
||||
{ __typename?: 'EASBuildDeprecationInfo' }
|
||||
& Pick<EasBuildDeprecationInfo, 'type' | 'message'>
|
||||
@@ -5078,10 +5092,10 @@ export type BuildFragment = (
|
||||
& Pick<Robot, 'firstName' | 'id'>
|
||||
)>, project: (
|
||||
{ __typename: 'Snack' }
|
||||
& Pick<Snack, 'id'>
|
||||
& Pick<Snack, 'id' | 'name'>
|
||||
) | (
|
||||
{ __typename: 'App' }
|
||||
& Pick<App, 'id'>
|
||||
& Pick<App, 'id' | 'name'>
|
||||
& { ownerAccount: (
|
||||
{ __typename?: 'Account' }
|
||||
& Pick<Account, 'id' | 'name'>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
AndroidJobInput,
|
||||
BuildFragment,
|
||||
BuildMetadataInput,
|
||||
CreateAndroidBuildMutation,
|
||||
CreateAndroidBuildMutationVariables,
|
||||
@@ -12,9 +14,10 @@ import {
|
||||
EasBuildDeprecationInfo,
|
||||
IosJobInput,
|
||||
} from '../generated';
|
||||
import { BuildFragmentNode } from '../types/Build';
|
||||
|
||||
export interface BuildResult {
|
||||
build: { id: string };
|
||||
build: BuildFragment;
|
||||
deprecationInfo?: EasBuildDeprecationInfo | null;
|
||||
}
|
||||
|
||||
@@ -37,6 +40,7 @@ const BuildMutation = {
|
||||
createAndroidBuild(appId: $appId, job: $job, metadata: $metadata) {
|
||||
build {
|
||||
id
|
||||
...BuildFragment
|
||||
}
|
||||
deprecationInfo {
|
||||
type
|
||||
@@ -45,6 +49,7 @@ const BuildMutation = {
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(BuildFragmentNode)}
|
||||
`,
|
||||
input
|
||||
)
|
||||
@@ -70,6 +75,7 @@ const BuildMutation = {
|
||||
createIosBuild(appId: $appId, job: $job, metadata: $metadata) {
|
||||
build {
|
||||
id
|
||||
...BuildFragment
|
||||
}
|
||||
deprecationInfo {
|
||||
type
|
||||
@@ -78,6 +84,7 @@ const BuildMutation = {
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(BuildFragmentNode)}
|
||||
`,
|
||||
input
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@ export const BuildFragmentNode = gql`
|
||||
project {
|
||||
__typename
|
||||
id
|
||||
name
|
||||
... on App {
|
||||
ownerAccount {
|
||||
id
|
||||
|
||||
@@ -309,7 +309,9 @@ describe(filterOutAssetsThatAlreadyExistAsync, () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
await (await filterOutAssetsThatAlreadyExistAsync([{ storageKey: 'blah' } as any])).length
|
||||
await (
|
||||
await filterOutAssetsThatAlreadyExistAsync([{ storageKey: 'blah' } as any])
|
||||
).length
|
||||
).toBe(1);
|
||||
});
|
||||
it('ignores an asset that exists', async () => {
|
||||
@@ -323,7 +325,9 @@ describe(filterOutAssetsThatAlreadyExistAsync, () => {
|
||||
];
|
||||
});
|
||||
expect(
|
||||
await (await filterOutAssetsThatAlreadyExistAsync([{ storageKey: 'blah' } as any])).length
|
||||
await (
|
||||
await filterOutAssetsThatAlreadyExistAsync([{ storageKey: 'blah' } as any])
|
||||
).length
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ExpoConfig, getConfig } from '@expo/config';
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
|
||||
export function getExpoConfig(projectDir: string, env: Env = {}): ExpoConfig {
|
||||
const originalProcessEnv: NodeJS.ProcessEnv = process.env;
|
||||
try {
|
||||
process.env = {
|
||||
...process.env,
|
||||
...env,
|
||||
};
|
||||
const { exp } = getConfig(projectDir, {
|
||||
skipSDKVersionRequirement: true,
|
||||
});
|
||||
return exp;
|
||||
} finally {
|
||||
process.env = originalProcessEnv;
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,11 @@ export async function selectSchemeAsync({
|
||||
nonInteractive?: boolean;
|
||||
}): Promise<string> {
|
||||
const schemes = IOSConfig.BuildScheme.getSchemesFromXcodeproj(projectDir);
|
||||
if (schemes.length === 0) {
|
||||
throw new Error(
|
||||
`We did not find any schemes in the Xcode project, make sure that at least one scheme is marked as "shared" in Xcode, and that it's listed in the output of "xcodebuild -list" command`
|
||||
);
|
||||
}
|
||||
if (schemes.length === 1) {
|
||||
return schemes[0];
|
||||
}
|
||||
|
||||
@@ -23,10 +23,10 @@ export async function isEasEnabledForProjectAsync(projectId: string): Promise<bo
|
||||
}
|
||||
}
|
||||
|
||||
export const EAS_UNAVAILABLE_MESSAGE = `Your account doesn't have access to Expo Application Services (EAS) features. Enroll in EAS to give it a try: ${chalk.underline(
|
||||
'https://expo.io/eas'
|
||||
)}`;
|
||||
|
||||
export function warnEasUnavailable() {
|
||||
Log.warn(
|
||||
`Your account doesn't have access to Expo Application Services (EAS) features. Enroll in EAS to give it a try: ${chalk.underline(
|
||||
'https://expo.io/eas'
|
||||
)}`
|
||||
);
|
||||
Log.warn(EAS_UNAVAILABLE_MESSAGE);
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
import {
|
||||
AppJSONConfig,
|
||||
ExpoConfig,
|
||||
getConfig,
|
||||
getConfigFilePaths,
|
||||
modifyConfigAsync,
|
||||
} from '@expo/config';
|
||||
import { AppJSONConfig, ExpoConfig, getConfigFilePaths, modifyConfigAsync } from '@expo/config';
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
import path from 'path';
|
||||
@@ -20,6 +15,7 @@ import {
|
||||
ensureProjectExistsAsync,
|
||||
findProjectIdByAccountNameAndSlugNullableAsync,
|
||||
} from './ensureProjectExists';
|
||||
import { getExpoConfig } from './expoConfig';
|
||||
|
||||
export function getProjectAccountName(exp: ExpoConfig, user: Actor): string {
|
||||
switch (user.__typename) {
|
||||
@@ -61,8 +57,11 @@ export async function findProjectRootAsync(cwd?: string): Promise<string | null>
|
||||
return projectRootDir ?? null;
|
||||
}
|
||||
|
||||
export async function setProjectIdAsync(projectDir: string): Promise<ExpoConfig | undefined> {
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
export async function setProjectIdAsync(
|
||||
projectDir: string,
|
||||
options: { env?: Env } = {}
|
||||
): Promise<ExpoConfig | undefined> {
|
||||
const exp = getExpoConfig(projectDir, options.env);
|
||||
|
||||
const privacy = toAppPrivacy(exp.privacy);
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
@@ -111,7 +110,10 @@ export async function setProjectIdAsync(projectDir: string): Promise<ExpoConfig
|
||||
return (result.config as AppJSONConfig)?.expo;
|
||||
}
|
||||
|
||||
export async function getProjectIdAsync(exp: ExpoConfig): Promise<string> {
|
||||
export async function getProjectIdAsync(
|
||||
exp: ExpoConfig,
|
||||
options: { env?: Env } = {}
|
||||
): Promise<string> {
|
||||
if (!process.env.EAS_ENABLE_PROJECT_ID) {
|
||||
const privacy = toAppPrivacy(exp.privacy);
|
||||
return await ensureProjectExistsAsync({
|
||||
@@ -120,6 +122,7 @@ export async function getProjectIdAsync(exp: ExpoConfig): Promise<string> {
|
||||
privacy,
|
||||
});
|
||||
}
|
||||
|
||||
const localProjectId = exp.extra?.eas?.projectId;
|
||||
if (localProjectId) {
|
||||
return localProjectId;
|
||||
@@ -130,7 +133,7 @@ export async function getProjectIdAsync(exp: ExpoConfig): Promise<string> {
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const newExp = await setProjectIdAsync(projectDir);
|
||||
const newExp = await setProjectIdAsync(projectDir, options);
|
||||
|
||||
const newLocalProjectId = newExp?.extra?.eas?.projectId;
|
||||
if (!newLocalProjectId) {
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function resolveWorkflowAsync(
|
||||
platformWorkflowMarker =
|
||||
platform === Platform.ANDROID
|
||||
? await AndroidConfig.Paths.getAndroidManifestAsync(projectDir)
|
||||
: IOSConfig.Paths.getXcodeProjectPath(projectDir);
|
||||
: IOSConfig.Paths.getPBXProjectPath(projectDir);
|
||||
} catch {
|
||||
return Workflow.MANAGED;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ async function startSubmissionAsync(
|
||||
const { submission } = await SubmissionMutation.createSubmissionAsync({
|
||||
appId: projectId,
|
||||
platform,
|
||||
config: (config as unknown) as JSONObject,
|
||||
config: config as unknown as JSONObject,
|
||||
submittedBuildId: buildId,
|
||||
});
|
||||
return submission.id;
|
||||
|
||||
@@ -155,7 +155,7 @@ class AndroidSubmitCommand {
|
||||
});
|
||||
} else {
|
||||
return result({
|
||||
sourceType: ServiceAccountSourceType.prompt,
|
||||
sourceType: ServiceAccountSourceType.detect,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import chalk from 'chalk';
|
||||
import glob from 'fast-glob';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { filterAsync } from '../../utils/filterAsync';
|
||||
import { isExistingFile } from '../utils/files';
|
||||
|
||||
export enum ServiceAccountSourceType {
|
||||
path,
|
||||
prompt,
|
||||
detect,
|
||||
// credentialsService,
|
||||
// ...
|
||||
}
|
||||
@@ -25,7 +30,14 @@ interface ServiceAccountPromptSource extends ServiceAccountSourceBase {
|
||||
sourceType: ServiceAccountSourceType.prompt;
|
||||
}
|
||||
|
||||
export type ServiceAccountSource = ServiceAccountPathSource | ServiceAccountPromptSource;
|
||||
interface ServiceAccountDetectSource extends ServiceAccountSourceBase {
|
||||
sourceType: ServiceAccountSourceType.detect;
|
||||
}
|
||||
|
||||
export type ServiceAccountSource =
|
||||
| ServiceAccountPathSource
|
||||
| ServiceAccountPromptSource
|
||||
| ServiceAccountDetectSource;
|
||||
|
||||
export async function getServiceAccountAsync(source: ServiceAccountSource): Promise<string> {
|
||||
switch (source.sourceType) {
|
||||
@@ -33,6 +45,8 @@ export async function getServiceAccountAsync(source: ServiceAccountSource): Prom
|
||||
return await handlePathSourceAsync(source);
|
||||
case ServiceAccountSourceType.prompt:
|
||||
return await handlePromptSourceAsync(source);
|
||||
case ServiceAccountSourceType.detect:
|
||||
return await handleDetectSourceAsync(source);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +58,35 @@ async function handlePathSourceAsync(source: ServiceAccountPathSource): Promise<
|
||||
return source.path;
|
||||
}
|
||||
|
||||
async function handleDetectSourceAsync(_source: ServiceAccountDetectSource): Promise<string> {
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const foundFilePaths = await glob('**/*.json', {
|
||||
cwd: projectDir,
|
||||
ignore: ['app.json', 'package*.json', 'tsconfig.json', 'node_modules'],
|
||||
});
|
||||
|
||||
const googleServiceFiles = await filterAsync(
|
||||
foundFilePaths.map(file => path.join(projectDir, file)),
|
||||
fileIsGoogleServicesAsync
|
||||
);
|
||||
|
||||
if (googleServiceFiles.length > 1) {
|
||||
const selectedPath = await displayPathChooserAsync(googleServiceFiles, projectDir);
|
||||
|
||||
if (selectedPath !== false) {
|
||||
return selectedPath;
|
||||
}
|
||||
} else if (googleServiceFiles.length === 1) {
|
||||
const [detectedPath] = googleServiceFiles;
|
||||
|
||||
if (await confirmDetectedPathAsync(detectedPath)) {
|
||||
return detectedPath;
|
||||
}
|
||||
}
|
||||
|
||||
return await getServiceAccountAsync({ sourceType: ServiceAccountSourceType.prompt });
|
||||
}
|
||||
|
||||
async function handlePromptSourceAsync(_source: ServiceAccountPromptSource): Promise<string> {
|
||||
const path = await askForServiceAccountPathAsync();
|
||||
return await getServiceAccountAsync({
|
||||
@@ -80,3 +123,53 @@ async function askForServiceAccountPathAsync(): Promise<string> {
|
||||
});
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function displayPathChooserAsync(
|
||||
paths: string[],
|
||||
projectDir: string
|
||||
): Promise<string | false> {
|
||||
const choices = paths.map<{ title: string; value: string | false }>(f => ({
|
||||
value: f,
|
||||
title: f.startsWith(projectDir) ? path.relative(projectDir, f) : f,
|
||||
}));
|
||||
|
||||
choices.push({
|
||||
title: 'None of the above',
|
||||
value: false,
|
||||
});
|
||||
|
||||
Log.log(
|
||||
'Multiple Google Service Account JSON keys have been found inside your project directory.'
|
||||
);
|
||||
const { selectedPath } = await promptAsync({
|
||||
name: 'selectedPath',
|
||||
type: 'select',
|
||||
message: 'Choose the key you want to use for this submission:',
|
||||
choices,
|
||||
});
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
return selectedPath;
|
||||
}
|
||||
|
||||
async function confirmDetectedPathAsync(path: string): Promise<boolean> {
|
||||
Log.log(`A Google Service Account JSON key has been found at\n ${chalk.underline(path)}`);
|
||||
const { confirmed } = await promptAsync({
|
||||
name: 'confirmed',
|
||||
type: 'confirm',
|
||||
message: 'Would you like to use this file?',
|
||||
initial: true,
|
||||
});
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
async function fileIsGoogleServicesAsync(path: string): Promise<boolean> {
|
||||
try {
|
||||
const jsonFile = await fs.readJson(path);
|
||||
return jsonFile.type === 'service_account';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { asMock } from '../../../__tests__/utils';
|
||||
import { findProjectRootAsync } from '../../../project/projectUtils';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import {
|
||||
ServiceAccountSource,
|
||||
@@ -10,11 +11,19 @@ import {
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('../../../project/projectUtils');
|
||||
|
||||
describe(getServiceAccountAsync, () => {
|
||||
beforeAll(() => {
|
||||
const mockDetectableServiceAccountJson = JSON.stringify({
|
||||
type: 'service_account',
|
||||
});
|
||||
|
||||
vol.fromJSON({
|
||||
'/google-service-account.json': JSON.stringify({ service: 'account' }),
|
||||
'/project_dir/subdir/service-account.json': mockDetectableServiceAccountJson,
|
||||
'/project_dir/another-service-account.json': mockDetectableServiceAccountJson,
|
||||
'/other_dir/invalid_file.txt': 'this is not even a JSON',
|
||||
});
|
||||
});
|
||||
afterAll(() => {
|
||||
@@ -90,4 +99,100 @@ describe(getServiceAccountAsync, () => {
|
||||
expect(serviceAccountPath).toBe('/google-service-account.json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when source is ServiceAccountSourceType.detect', () => {
|
||||
it('detects a single google-services file and prompts for confirmation', async () => {
|
||||
asMock(findProjectRootAsync).mockResolvedValueOnce('/project_dir/subdir'); // should find 1 file
|
||||
asMock(promptAsync).mockResolvedValueOnce({
|
||||
confirmed: true,
|
||||
});
|
||||
|
||||
const serviceAccountPath = await getServiceAccountAsync({
|
||||
sourceType: ServiceAccountSourceType.detect,
|
||||
});
|
||||
|
||||
expect(promptAsync).toHaveBeenCalledWith(expect.objectContaining({ type: 'confirm' }));
|
||||
expect(serviceAccountPath).toBe('/project_dir/subdir/service-account.json');
|
||||
});
|
||||
|
||||
it('falls back to prompt, when no valid files are found in the dir', async () => {
|
||||
asMock(findProjectRootAsync).mockResolvedValueOnce('/other_dir'); // no valid files in that dir
|
||||
asMock(promptAsync).mockResolvedValueOnce({
|
||||
filePath: '/google-service-account.json',
|
||||
});
|
||||
|
||||
const serviceAccountPath = await getServiceAccountAsync({
|
||||
sourceType: ServiceAccountSourceType.detect,
|
||||
});
|
||||
|
||||
expect(promptAsync).toHaveBeenCalledWith(expect.objectContaining({ type: 'text' }));
|
||||
expect(serviceAccountPath).toBe('/google-service-account.json');
|
||||
});
|
||||
|
||||
it('falls back to prompt, when user rejects to use detected file', async () => {
|
||||
asMock(findProjectRootAsync).mockResolvedValueOnce('/project_dir/subdir'); // should find 1 file
|
||||
asMock(promptAsync)
|
||||
.mockResolvedValueOnce({
|
||||
confirmed: false,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
filePath: '/google-service-account.json',
|
||||
});
|
||||
const source: ServiceAccountSource = {
|
||||
sourceType: ServiceAccountSourceType.detect,
|
||||
};
|
||||
|
||||
const serviceAccountPath = await getServiceAccountAsync(source);
|
||||
|
||||
expect(promptAsync).toHaveBeenCalledTimes(2);
|
||||
expect(promptAsync).toHaveBeenCalledWith(expect.objectContaining({ type: 'confirm' }));
|
||||
expect(promptAsync).toHaveBeenLastCalledWith(expect.objectContaining({ type: 'text' }));
|
||||
expect(serviceAccountPath).toBe('/google-service-account.json');
|
||||
});
|
||||
|
||||
it('displays a chooser, when multiple files are found', async () => {
|
||||
asMock(findProjectRootAsync).mockResolvedValueOnce('/project_dir'); // should find 2 files here
|
||||
asMock(promptAsync).mockResolvedValueOnce({
|
||||
selectedPath: '/project_dir/another-service-account.json',
|
||||
});
|
||||
|
||||
const serviceAccountPath = await getServiceAccountAsync({
|
||||
sourceType: ServiceAccountSourceType.detect,
|
||||
});
|
||||
|
||||
const expectedChoices = expect.arrayContaining([
|
||||
expect.objectContaining({ value: '/project_dir/another-service-account.json' }),
|
||||
expect.objectContaining({ value: '/project_dir/subdir/service-account.json' }),
|
||||
expect.objectContaining({ value: false }),
|
||||
]);
|
||||
|
||||
expect(promptAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'select',
|
||||
choices: expectedChoices,
|
||||
})
|
||||
);
|
||||
expect(serviceAccountPath).toBe('/project_dir/another-service-account.json');
|
||||
});
|
||||
|
||||
it('falls back to prompt, when user selects the "None of above"', async () => {
|
||||
asMock(findProjectRootAsync).mockResolvedValueOnce('/project_dir'); // should find 2 files here
|
||||
asMock(promptAsync)
|
||||
.mockResolvedValueOnce({
|
||||
selectedPath: false,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
filePath: '/google-service-account.json',
|
||||
});
|
||||
|
||||
const serviceAccountPath = await getServiceAccountAsync({
|
||||
sourceType: ServiceAccountSourceType.detect,
|
||||
});
|
||||
|
||||
expect(promptAsync).toHaveBeenCalledTimes(2);
|
||||
expect(promptAsync).toHaveBeenCalledWith(expect.objectContaining({ type: 'select' }));
|
||||
expect(promptAsync).toHaveBeenLastCalledWith(expect.objectContaining({ type: 'text' }));
|
||||
expect(serviceAccountPath).toBe('/google-service-account.json');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* asynchronous array filter
|
||||
* @param arr array to filter
|
||||
* @param predicate a predicate function to run asynchronously
|
||||
* @returns a promise resolving to a filtered array
|
||||
*/
|
||||
export const filterAsync = async <T>(
|
||||
arr: T[],
|
||||
predicate: (value: T, index: number, array: T[]) => Promise<boolean>
|
||||
) => Promise.all(arr.map(predicate)).then(results => arr.filter((_v, index) => results[index]));
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"description": "A library for interacting with the eas.json",
|
||||
"version": "0.22.0",
|
||||
"version": "0.22.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/eas-build-job": "0.2.44",
|
||||
"@hapi/joi": "17.1.1",
|
||||
"fs-extra": "9.0.1",
|
||||
"tslib": "1.14.1"
|
||||
"tslib": "2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
@@ -20,8 +20,7 @@
|
||||
"@types/node": "^12",
|
||||
"babel-jest": "^26.3.0",
|
||||
"memfs": "^3.2.0",
|
||||
"ts-node": "^8",
|
||||
"typescript": "^3.3"
|
||||
"typescript": "4.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
|
||||
@@ -22,6 +22,10 @@ function intersect<T>(setA: Set<T>, setB: Set<T>): Set<T> {
|
||||
}
|
||||
|
||||
export class EasJsonReader {
|
||||
public static formatEasJsonPath(projectDir: string) {
|
||||
return path.join(projectDir, 'eas.json');
|
||||
}
|
||||
|
||||
constructor(private projectDir: string, private platform: 'android' | 'ios' | 'all') {}
|
||||
|
||||
/**
|
||||
@@ -94,7 +98,7 @@ export class EasJsonReader {
|
||||
}
|
||||
|
||||
public async readRawAsync(): Promise<EasJson> {
|
||||
const rawFile = await fs.readFile(path.join(this.projectDir, 'eas.json'), 'utf8');
|
||||
const rawFile = await fs.readFile(EasJsonReader.formatEasJsonPath(this.projectDir), 'utf8');
|
||||
const json = JSON.parse(rawFile);
|
||||
|
||||
const { value, error } = EasJsonSchema.validate(json, {
|
||||
|
||||
Vendored
+1
-3
@@ -407,9 +407,7 @@ declare module 'xcode' {
|
||||
*/
|
||||
findPBXGroupKey(criteria: { name?: string; path?: string }): UUID | undefined;
|
||||
findPBXVariantGroupKey(criteria: unknown): string;
|
||||
addLocalizationVariantGroup(
|
||||
name: unknown
|
||||
): {
|
||||
addLocalizationVariantGroup(name: unknown): {
|
||||
uuid: unknown;
|
||||
fileRef: unknown;
|
||||
basename: unknown;
|
||||
|
||||
Reference in new issue
Block a user