mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 13:08:11 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20b091fbfa | ||
|
|
168b6afdfa | ||
|
|
3c6320b929 | ||
|
|
4f1dc582bd | ||
|
|
daf2ef1e30 | ||
|
|
f703bd8b5a | ||
|
|
997cd7058a | ||
|
|
e896c2f6c5 | ||
|
|
3d723ca426 | ||
|
|
c17e1b3fa2 | ||
|
|
d70fc36485 | ||
|
|
47784ca6d3 | ||
|
|
3e5a3aca27 | ||
|
|
89b0227594 | ||
|
|
b98a30e7c6 | ||
|
|
5c7862a685 | ||
|
|
82833a9eb4 | ||
|
|
66171c3c4b | ||
|
|
179def91c7 | ||
|
|
9f63ad4527 | ||
|
|
878028271a | ||
|
|
d4e15bbcbf | ||
|
|
13bb056df1 | ||
|
|
14d56904d2 | ||
|
|
c2ffd05763 | ||
|
|
46e394fbef | ||
|
|
2613f0c2f9 | ||
|
|
e5fd7c01f4 | ||
|
|
73a0dd6e23 | ||
|
|
a9dc7f9eeb | ||
|
|
c3836f23d1 | ||
|
|
bfd73cbb19 | ||
|
|
6a1753edaa | ||
|
|
558a73086d | ||
|
|
15966fbd26 | ||
|
|
1e8b55383e | ||
|
|
6920ebb0ba | ||
|
|
55f35949e1 | ||
|
|
ccb998a221 | ||
|
|
30bc27b4c4 | ||
|
|
da3c95ccaf | ||
|
|
bbcbe1dceb |
No files matched your search
@@ -19,6 +19,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'flowtype/no-types-missing-file-annotation': 'off',
|
||||
'curly': 'warn',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
**/yarn.lock linguist-generated
|
||||
@@ -11,3 +11,4 @@ tmp
|
||||
node_modules
|
||||
coverage
|
||||
.history
|
||||
.DS_Store
|
||||
+58
-3
@@ -6,17 +6,72 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Unifify generic and managed workflow, deprecate `workflow` field. ([#497](https://github.com/expo/eas-cli/pull/497) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Introduce new format of eas.json (old eas.json will be migrated automatically). ([#537](https://github.com/expo/eas-cli/pull/537) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Validate metro config for managed workflow projects. ([#534](https://github.com/expo/eas-cli/pull/534) by [@dsokal](https://github.com/dsokal))
|
||||
- Add more filter params to `build:list`. ([#540](https://github.com/expo/eas-cli/pull/540) by [@dsokal](https://github.com/dsokal))
|
||||
- Evaluate interpolated iOS version strings for metadata. ([#541](https://github.com/expo/eas-cli/pull/541) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Validate metadata client side to print better errors. ([#542](https://github.com/expo/eas-cli/pull/542) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix `new` build status when building both paltforms. ([#543](https://github.com/expo/eas-cli/pull/543) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [0.22.1](https://github.com/expo/eas-cli/releases/tag/v0.22.1) - 2021-07-28
|
||||
|
||||
### 🎉 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
|
||||
|
||||
- Fix iOS capability syncing on build. ([#521](https://github.com/expo/eas-cli/pull/521) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- 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))
|
||||
|
||||
## [0.21.0](https://github.com/expo/eas-cli/releases/tag/v0.21.0) - 2021-07-12
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `project:*` commands. ([#500](https://github.com/expo/eas-cli/pull/500) by [@jkhales](https://github.com/jkhales))
|
||||
- Added support for iOS 15 capabilities: Communication Notifications, Time Sensitive Notifications, Group Activities, and Family Controls. ([#499](https://github.com/expo/eas-cli/pull/499) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Show more build metadata in `build:view` and `build:list`. ([#504](https://github.com/expo/eas-cli/pull/504) and [#508](https://github.com/expo/eas-cli/pull/508) by [@dsokal](https://github.com/dsokal))
|
||||
- Add runtime version to build metadata. ([#493](https://github.com/expo/eas-cli/pull/493) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.20.0](https://github.com/expo/eas-cli/releases/tag/v0.20.0) - 2021-07-09
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Unifify generic and managed workflow, deprecate `workflow` field. ([#497](https://github.com/expo/eas-cli/pull/497) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix runtime version checks. ([#495](https://github.com/expo/eas-cli/pull/495) by [@dsokal](https://github.com/dsokal))
|
||||
- Resolve `--android-package` correctly in `eas submit` command. ([#494](https://github.com/expo/eas-cli/pull/494) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [0.19.1](https://github.com/expo/eas-cli/releases/tag/v0.19.1) - 2021-07-02
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.20.0"
|
||||
"version": "0.22.2"
|
||||
}
|
||||
+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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
graphql.schema.json linguist-generated
|
||||
src/graphql/generated.ts linguist-generated
|
||||
+75
-22
@@ -42,11 +42,14 @@ eas --help COMMAND
|
||||
* [`eas build:configure`](#eas-buildconfigure)
|
||||
* [`eas build:list`](#eas-buildlist)
|
||||
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
|
||||
* [`eas config`](#eas-config)
|
||||
* [`eas credentials`](#eas-credentials)
|
||||
* [`eas device:create`](#eas-devicecreate)
|
||||
* [`eas device:list`](#eas-devicelist)
|
||||
* [`eas device:view [UDID]`](#eas-deviceview-udid)
|
||||
* [`eas help [COMMAND]`](#eas-help-command)
|
||||
* [`eas project:info`](#eas-projectinfo)
|
||||
* [`eas project:init`](#eas-projectinit)
|
||||
* [`eas secret:create`](#eas-secretcreate)
|
||||
* [`eas secret:delete`](#eas-secretdelete)
|
||||
* [`eas secret:list`](#eas-secretlist)
|
||||
@@ -69,7 +72,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.20.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.2/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -83,7 +86,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.20.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.2/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -97,7 +100,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.20.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.2/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -108,7 +111,7 @@ USAGE
|
||||
$ eas analytics [STATUS]
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -128,7 +131,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.20.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.2/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -139,7 +142,7 @@ USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.20.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.2/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -153,7 +156,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.20.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.2/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -164,12 +167,21 @@ USAGE
|
||||
$ eas build:list
|
||||
|
||||
OPTIONS
|
||||
--appBuildVersion=appBuildVersion
|
||||
--appIdentifier=appIdentifier
|
||||
--appVersion=appVersion
|
||||
--buildProfile=buildProfile
|
||||
--channel=channel
|
||||
--distribution=(store|internal|simulator)
|
||||
--gitCommitHash=gitCommitHash
|
||||
--limit=limit
|
||||
--platform=(all|android|ios)
|
||||
--runtimeVersion=runtimeVersion
|
||||
--sdkVersion=sdkVersion
|
||||
--status=(new|in-queue|in-progress|errored|finished|canceled)
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.20.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.2/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -180,7 +192,22 @@ USAGE
|
||||
$ eas build:view [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.20.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.2/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
Show the eas.json config
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas config
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios)
|
||||
--profile=profile
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -191,7 +218,7 @@ USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/credentials.ts)_
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -202,7 +229,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.20.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.2/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -216,7 +243,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.20.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.2/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -227,7 +254,7 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.20.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.2/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -246,6 +273,31 @@ OPTIONS
|
||||
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.0/src/commands/help.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
information about the current project
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas project:info
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
create or link an EAS project
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas project:init
|
||||
|
||||
ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
Create an environment secret on the current project or owner account.
|
||||
@@ -255,12 +307,13 @@ USAGE
|
||||
$ eas secret:create
|
||||
|
||||
OPTIONS
|
||||
--force Delete and recreate existing secrets
|
||||
--name=name Name of the secret
|
||||
--scope=(account|project) [default: project] Scope for the secret
|
||||
--value=value Value of the secret
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.20.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.2/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -277,7 +330,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.20.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.2/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -288,7 +341,7 @@ USAGE
|
||||
$ eas secret:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.20.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.2/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
|
||||
## `eas submit --platform=(android|ios)`
|
||||
|
||||
@@ -368,7 +421,7 @@ EXAMPLES
|
||||
and provide its App ID
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -387,7 +440,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.20.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.2/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -401,7 +454,7 @@ ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.20.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.2/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -415,7 +468,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.20.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.2/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -435,7 +488,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.20.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.2/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -449,5 +502,5 @@ ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.20.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.2/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
Generated
+1086
-220
File diff suppressed because it is too large.
Load diff
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.20.0",
|
||||
"version": "0.22.2",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@amplitude/identify": "1.5.0",
|
||||
"@amplitude/node": "1.5.0",
|
||||
"@expo/apple-utils": "0.0.0-alpha.20",
|
||||
"@amplitude/identify": "1.7.0",
|
||||
"@amplitude/node": "1.7.0",
|
||||
"@expo/apple-utils": "0.0.0-alpha.24",
|
||||
"@expo/config": "3.3.42",
|
||||
"@expo/config-plugins": "3.0.3",
|
||||
"@expo/eas-build-job": "0.2.41",
|
||||
"@expo/eas-json": "^0.20.0",
|
||||
"@expo/eas-build-job": "0.2.45",
|
||||
"@expo/eas-json": "^0.22.2",
|
||||
"@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"
|
||||
@@ -131,15 +127,27 @@
|
||||
],
|
||||
"helpClass": "./build/help",
|
||||
"topics": {
|
||||
"account": {
|
||||
"description": "manage your account"
|
||||
},
|
||||
"build": {
|
||||
"description": "build app binaries"
|
||||
},
|
||||
"channel": {
|
||||
"description": "manage update channels"
|
||||
},
|
||||
"device": {
|
||||
"description": "manage your Apple devices for internal distribution"
|
||||
},
|
||||
"project": {
|
||||
"description": "manage your project"
|
||||
},
|
||||
"release": {
|
||||
"description": "manage update releases"
|
||||
},
|
||||
"secret": {
|
||||
"description": "manage project and account secrets"
|
||||
},
|
||||
"update": {
|
||||
"description": "manage individual updates"
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function initAsync(): Promise<void> {
|
||||
process.env.EXPO_STAGING || process.env.EXPO_LOCAL
|
||||
? 'cdebbc678931403439486c4750781544'
|
||||
: '4ac443afd5073c0df6169291db1d3495';
|
||||
client = Amplitude.init(apiKey);
|
||||
client = Amplitude.init(apiKey, { retryClass: new Amplitude.OfflineRetryHandler(apiKey) });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -94,7 +95,7 @@ export async function readChannelSafelyAsync(projectDir: string): Promise<string
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
const stringifiedRequestHeaders = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
|
||||
androidManifest,
|
||||
'expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY' //TODO-JJ AndroidConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY once https://github.com/expo/expo-cli/pull/3571 is published
|
||||
AndroidConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
|
||||
);
|
||||
if (!stringifiedRequestHeaders) {
|
||||
return null;
|
||||
|
||||
@@ -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,18 +48,19 @@ 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);
|
||||
}
|
||||
},
|
||||
getMetadataContext: () => ({}),
|
||||
prepareJobAsync,
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
@@ -90,21 +88,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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ArchiveSource, Job, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { Android, ArchiveSource, Job, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile } from '@expo/eas-json';
|
||||
import path from 'path';
|
||||
|
||||
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
|
||||
@@ -13,13 +14,19 @@ interface JobData {
|
||||
credentials?: AndroidCredentials;
|
||||
}
|
||||
|
||||
const cacheDefaults = {
|
||||
disabled: false,
|
||||
customPaths: [],
|
||||
cacheDefaultPaths: true,
|
||||
};
|
||||
|
||||
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 buildProfile: AndroidBuildProfile = ctx.buildProfile;
|
||||
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
|
||||
const { credentials } = jobData;
|
||||
const buildCredentials = credentials
|
||||
? {
|
||||
@@ -34,22 +41,28 @@ export async function prepareJobAsync(
|
||||
}
|
||||
: {};
|
||||
|
||||
const job = {
|
||||
let buildType = ctx.buildProfile.buildType;
|
||||
if (!buildType && !buildProfile.gradleCommand && ctx.buildProfile.distribution === 'internal') {
|
||||
buildType = Android.BuildType.APK;
|
||||
}
|
||||
|
||||
const job: Android.Job = {
|
||||
type: ctx.workflow,
|
||||
platform: Platform.ANDROID,
|
||||
projectRootDirectory,
|
||||
projectArchive: jobData.projectArchive,
|
||||
builderEnvironment: {
|
||||
image: ctx.buildProfile.image,
|
||||
node: ctx.buildProfile.node,
|
||||
yarn: ctx.buildProfile.yarn,
|
||||
ndk: ctx.buildProfile.ndk,
|
||||
expoCli: ctx.buildProfile.expoCli,
|
||||
env: ctx.buildProfile.env,
|
||||
image: buildProfile.image ?? 'default',
|
||||
node: buildProfile.node,
|
||||
yarn: buildProfile.yarn,
|
||||
ndk: buildProfile.ndk,
|
||||
expoCli: buildProfile.expoCli,
|
||||
env: buildProfile.env,
|
||||
},
|
||||
cache: {
|
||||
...ctx.buildProfile.cache,
|
||||
clear: ctx.commandCtx.clearCache,
|
||||
...cacheDefaults,
|
||||
...buildProfile.cache,
|
||||
clear: ctx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
...buildCredentials,
|
||||
@@ -57,11 +70,11 @@ export async function prepareJobAsync(
|
||||
releaseChannel: ctx.buildProfile.releaseChannel,
|
||||
updates: { channel: ctx.buildProfile.channel },
|
||||
|
||||
gradleCommand: ctx.buildProfile.gradleCommand,
|
||||
artifactPath: ctx.buildProfile.artifactPath,
|
||||
gradleCommand: buildProfile.gradleCommand,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
|
||||
username,
|
||||
buildType: ctx.buildProfile.buildType,
|
||||
buildType: buildProfile.developmentClient ? Android.BuildType.DEVELOPMENT_CLIENT : buildType,
|
||||
};
|
||||
|
||||
return sanitizeJob(job);
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function readVersionCodeAsync(
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
return AndroidConfig.Version.getVersionCode(exp) ?? undefined;
|
||||
return AndroidConfig.Version.getVersionCode(exp) ?? 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
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';
|
||||
import { runLocalBuildAsync } from './local';
|
||||
import { collectMetadata } from './metadata';
|
||||
import { MetadataContext, collectMetadata } from './metadata';
|
||||
import { Platform, TrackingContext } from './types';
|
||||
import Analytics, { Event } from './utils/analytics';
|
||||
import { printDeprecationWarnings } from './utils/printBuildInfo';
|
||||
@@ -36,63 +41,62 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
|
||||
ctx: BuildContext<TPlatform>
|
||||
): Promise<CredentialsResult<Credentials> | undefined>;
|
||||
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
|
||||
getMetadataContext: () => MetadataContext<TPlatform>;
|
||||
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<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 { ctx } = builder;
|
||||
const credentialsResult = await withAnalyticsAsync(
|
||||
async () => await builder.ensureCredentialsAsync(builder.ctx),
|
||||
async () => await builder.ensureCredentialsAsync(ctx),
|
||||
{
|
||||
successEvent: Event.GATHER_CREDENTIALS_SUCCESS,
|
||||
failureEvent: Event.GATHER_CREDENTIALS_FAIL,
|
||||
trackingCtx: builder.ctx.trackingCtx,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
if (!builder.ctx.commandCtx.skipProjectConfiguration) {
|
||||
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(builder.ctx), {
|
||||
if (!ctx.skipProjectConfiguration) {
|
||||
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(ctx), {
|
||||
successEvent: Event.CONFIGURE_PROJECT_SUCCESS,
|
||||
failureEvent: Event.CONFIGURE_PROJECT_FAIL,
|
||||
trackingCtx: builder.ctx.trackingCtx,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
});
|
||||
}
|
||||
|
||||
if (await vcs.hasUncommittedChangesAsync()) {
|
||||
Log.addNewLineIfNone();
|
||||
await reviewAndCommitChangesAsync(
|
||||
`[EAS Build] Run EAS Build for ${
|
||||
requestedPlatformDisplayNames[builder.ctx.platform as Platform]
|
||||
}`,
|
||||
{
|
||||
nonInteractive: builder.ctx.commandCtx.nonInteractive,
|
||||
}
|
||||
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
|
||||
{ nonInteractive: ctx.nonInteractive }
|
||||
);
|
||||
}
|
||||
|
||||
const projectArchive = builder.ctx.commandCtx.local
|
||||
const projectArchive = ctx.local
|
||||
? ({
|
||||
type: ArchiveSourceType.PATH,
|
||||
path: (await makeProjectTarballAsync()).path,
|
||||
} as const)
|
||||
: ({
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: await uploadProjectAsync(builder.ctx),
|
||||
bucketKey: await uploadProjectAsync(ctx),
|
||||
} as const);
|
||||
|
||||
const metadata = await collectMetadata(builder.ctx, {
|
||||
credentialsSource: credentialsResult?.source,
|
||||
});
|
||||
const job = await builder.prepareJobAsync(builder.ctx, {
|
||||
const metadataContext = builder.getMetadataContext();
|
||||
const metadata = await collectMetadata(ctx, metadataContext);
|
||||
const job = await builder.prepareJobAsync(ctx, {
|
||||
projectArchive,
|
||||
credentials: credentialsResult?.credentials,
|
||||
});
|
||||
|
||||
return async () => {
|
||||
if (builder.ctx.commandCtx.local) {
|
||||
if (ctx.local) {
|
||||
await runLocalBuildAsync(job);
|
||||
return undefined;
|
||||
} else {
|
||||
@@ -109,6 +113,13 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
'EAS Build is down for maintenance, please try again later. Check https://status.expo.dev/ for updates.'
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED'
|
||||
) {
|
||||
Log.error(
|
||||
'EAS Build free tier is temporarily disabled, please try again later. Check https://status.expo.dev/ for updates.'
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
|
||||
) {
|
||||
@@ -170,7 +181,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 () => {
|
||||
@@ -179,13 +190,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,
|
||||
@@ -267,3 +278,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 - newBuilds - 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,37 @@ const configureCommitMessage = {
|
||||
[RequestedPlatform.All]: 'Configure EAS Build',
|
||||
};
|
||||
|
||||
export async function ensureProjectConfiguredAsync(
|
||||
projectDir: string,
|
||||
requestedPlatform: RequestedPlatform
|
||||
): Promise<void> {
|
||||
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = 'This project is not configured to build with EAS. Set it up now?';
|
||||
const confirm = await confirmAsync({ message });
|
||||
if (confirm) {
|
||||
await configureAsync({
|
||||
projectDir,
|
||||
platform: requestedPlatform,
|
||||
});
|
||||
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;
|
||||
@@ -63,95 +95,46 @@ export async function configureAsync(options: {
|
||||
}
|
||||
}
|
||||
|
||||
const ANDROID_MANAGED_DEFAULTS = {
|
||||
release: {
|
||||
buildType: 'app-bundle',
|
||||
},
|
||||
const MANAGED_DEFAULTS = {
|
||||
release: {},
|
||||
development: {
|
||||
buildType: 'development-client',
|
||||
distribution: 'internal',
|
||||
},
|
||||
};
|
||||
const ANDROID_GENERIC_DEFAULTS = {
|
||||
release: {
|
||||
gradleCommand: ':app:bundleRelease',
|
||||
},
|
||||
development: {
|
||||
gradleCommand: ':app:assembleDebug',
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
},
|
||||
};
|
||||
|
||||
const IOS_MANAGED_DEFAULTS = {
|
||||
release: {
|
||||
buildType: 'release',
|
||||
},
|
||||
const GENERIC_DEFAULTS = {
|
||||
release: {},
|
||||
development: {
|
||||
buildType: 'development-client',
|
||||
distribution: 'internal',
|
||||
},
|
||||
};
|
||||
|
||||
const IOS_GENERIC_DEFAULTS = {
|
||||
release: {
|
||||
schemeBuildConfiguration: 'Release',
|
||||
},
|
||||
development: {
|
||||
schemeBuildConfiguration: 'Debug',
|
||||
distribution: 'internal',
|
||||
android: {
|
||||
gradleCommand: ':app:assembleDebug',
|
||||
},
|
||||
ios: {
|
||||
buildConfiguration: 'Debug',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
|
||||
const easJsonPath = path.join(ctx.projectDir, 'eas.json');
|
||||
let existingEasJson;
|
||||
const easJsonPath = EasJsonReader.formatEasJsonPath(ctx.projectDir);
|
||||
|
||||
if (await fs.pathExists(easJsonPath)) {
|
||||
const reader = new EasJsonReader(ctx.projectDir, ctx.requestedPlatform);
|
||||
await reader.validateAsync();
|
||||
const reader = new EasJsonReader(ctx.projectDir);
|
||||
await reader.readAndValidateAsync();
|
||||
|
||||
existingEasJson = await reader.readRawAsync();
|
||||
Log.withTick('Validated eas.json');
|
||||
|
||||
// If we have already populated eas.json with the default fields for the
|
||||
// platform then proceed
|
||||
if (ctx.requestedPlatform !== 'all' && existingEasJson.builds[ctx.requestedPlatform]) {
|
||||
return;
|
||||
} else if (
|
||||
ctx.requestedPlatform === 'all' &&
|
||||
existingEasJson.builds.ios &&
|
||||
existingEasJson.builds.android
|
||||
) {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldInitIOS =
|
||||
['all', 'ios'].includes(ctx.requestedPlatform) && !existingEasJson?.builds.ios;
|
||||
const shouldInitAndroid =
|
||||
['all', 'android'].includes(ctx.requestedPlatform) && !existingEasJson?.builds.android;
|
||||
|
||||
const easJson = {
|
||||
builds: {
|
||||
...existingEasJson?.builds,
|
||||
...(shouldInitAndroid
|
||||
? {
|
||||
android: ctx.hasAndroidNativeProject
|
||||
? ANDROID_GENERIC_DEFAULTS
|
||||
: ANDROID_MANAGED_DEFAULTS,
|
||||
}
|
||||
: null),
|
||||
...(shouldInitIOS
|
||||
? {
|
||||
ios: ctx.hasIosNativeProject ? IOS_GENERIC_DEFAULTS : IOS_MANAGED_DEFAULTS,
|
||||
}
|
||||
: null),
|
||||
},
|
||||
};
|
||||
const easJson =
|
||||
ctx.hasAndroidNativeProject && ctx.hasIosNativeProject
|
||||
? { build: GENERIC_DEFAULTS }
|
||||
: { build: MANAGED_DEFAULTS };
|
||||
|
||||
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
|
||||
await vcs.trackFileAsync(easJsonPath);
|
||||
Log.withTick(`${existingEasJson ? 'Updated' : 'Generated'} eas.json`);
|
||||
Log.withTick('Generated eas.json');
|
||||
}
|
||||
|
||||
enum ShouldCommitChanges {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile, EasConfig, IosBuildProfile } from '@expo/eas-json';
|
||||
import { AndroidBuildProfile, BuildProfile, IosBuildProfile } from '@expo/eas-json';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { getProjectAccountName } from '../project/projectUtils';
|
||||
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';
|
||||
@@ -13,66 +14,6 @@ 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;
|
||||
@@ -84,56 +25,83 @@ export interface ConfigureContext {
|
||||
hasIosNativeProject: boolean;
|
||||
}
|
||||
|
||||
type PlatformBuildProfile<T extends Platform> = T extends Platform.ANDROID
|
||||
? AndroidBuildProfile
|
||||
: IosBuildProfile;
|
||||
|
||||
export interface BuildContext<T extends Platform> {
|
||||
commandCtx: CommandContext;
|
||||
trackingCtx: TrackingContext;
|
||||
accountName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
buildProfileName: string;
|
||||
clearCache: boolean;
|
||||
exp: ExpoConfig;
|
||||
local: boolean;
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
buildProfile: PlatformBuildProfile<T>;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
skipProjectConfiguration: boolean;
|
||||
trackingCtx: TrackingContext;
|
||||
user: Actor;
|
||||
workflow: Workflow;
|
||||
}
|
||||
|
||||
export async function createBuildContextAsync<T extends Platform>({
|
||||
buildProfileName,
|
||||
buildProfile,
|
||||
clearCache = false,
|
||||
local,
|
||||
nonInteractive = false,
|
||||
platform,
|
||||
easConfig,
|
||||
commandCtx,
|
||||
projectDir,
|
||||
skipProjectConfiguration = false,
|
||||
}: {
|
||||
buildProfileName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
clearCache: boolean;
|
||||
local: boolean;
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
easConfig: EasConfig;
|
||||
commandCtx: CommandContext;
|
||||
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 exp = getExpoConfig(projectDir, { env: buildProfile.env });
|
||||
|
||||
const workflow = await resolveWorkflowAsync(commandCtx.projectDir, platform);
|
||||
const accountId = findAccountByName(commandCtx.user.accounts, commandCtx.accountName)?.id;
|
||||
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,21 +109,19 @@ function getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
workflow,
|
||||
}: {
|
||||
platform: Platform;
|
||||
projectDir: string;
|
||||
buildProfile: AndroidBuildProfile | IosBuildProfile;
|
||||
workflow: Workflow;
|
||||
}): Partial<TrackingContext> {
|
||||
let includesDevClient;
|
||||
const version = tryGetDevClientVersion(projectDir);
|
||||
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
|
||||
} else if (platform === Platform.IOS && 'schemeBuildConfiguration' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.schemeBuildConfiguration === 'Debug');
|
||||
} else if ('buildType' in buildProfile) {
|
||||
includesDevClient = buildProfile.buildType === 'development-client';
|
||||
} else if (platform === Platform.IOS && 'buildConfiguration' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.buildConfiguration === 'Debug');
|
||||
} else if (buildProfile.developmentClient) {
|
||||
includesDevClient = true;
|
||||
} else {
|
||||
includesDevClient = false;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { ArchiveSource, ArchiveSourceType, Metadata, Workflow } from '@expo/eas-
|
||||
|
||||
import {
|
||||
BuildCredentialsSource,
|
||||
BuildIosEnterpriseProvisioning,
|
||||
BuildMetadataInput,
|
||||
BuildWorkflow,
|
||||
DistributionType,
|
||||
@@ -32,6 +33,9 @@ export function transformMetadata(metadata: Metadata): BuildMetadataInput {
|
||||
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
|
||||
distribution: metadata.distribution && transformDistribution(metadata.distribution),
|
||||
workflow: metadata.workflow && transformWorkflow(metadata.workflow),
|
||||
iosEnterpriseProvisioning:
|
||||
metadata.iosEnterpriseProvisioning &&
|
||||
transformIosEnterpriseProvisioning(metadata.iosEnterpriseProvisioning),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,3 +66,13 @@ export function transformWorkflow(workflow: Workflow): BuildWorkflow {
|
||||
return BuildWorkflow.Managed;
|
||||
}
|
||||
}
|
||||
|
||||
export function transformIosEnterpriseProvisioning(
|
||||
enterpriseProvisioning: Metadata['iosEnterpriseProvisioning']
|
||||
): BuildIosEnterpriseProvisioning {
|
||||
if (enterpriseProvisioning === 'adhoc') {
|
||||
return BuildIosEnterpriseProvisioning.Adhoc;
|
||||
} else {
|
||||
return BuildIosEnterpriseProvisioning.Universal;
|
||||
}
|
||||
}
|
||||
@@ -149,7 +149,7 @@ describe(readBuildNumberAsync, () => {
|
||||
describe('generic project', () => {
|
||||
it('reads the build number from native code', async () => {
|
||||
const exp = initGenericProject();
|
||||
const buildNumber = await readBuildNumberAsync('/repo', exp);
|
||||
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
|
||||
expect(buildNumber).toBe('1');
|
||||
});
|
||||
});
|
||||
@@ -157,7 +157,7 @@ describe(readBuildNumberAsync, () => {
|
||||
describe('managed project', () => {
|
||||
it('reads the build number from expo config', async () => {
|
||||
const exp = initManagedProject();
|
||||
const buildNumber = await readBuildNumberAsync('/repo', exp);
|
||||
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
|
||||
expect(buildNumber).toBe('1');
|
||||
});
|
||||
});
|
||||
@@ -167,21 +167,33 @@ describe(readShortVersionAsync, () => {
|
||||
describe('generic project', () => {
|
||||
it('reads the short version from native code', async () => {
|
||||
const exp = initGenericProject();
|
||||
const shortVersion = await readShortVersionAsync('/repo', exp);
|
||||
const shortVersion = await readShortVersionAsync('/repo', exp, {});
|
||||
expect(shortVersion).toBe('1.0.0');
|
||||
});
|
||||
it('evaluates interpolated build number', async () => {
|
||||
const exp = initGenericProject({
|
||||
shortVersion: '$(CURRENT_PROJECT_VERSION)',
|
||||
});
|
||||
const buildNumber = await readShortVersionAsync('/repo', exp, {
|
||||
CURRENT_PROJECT_VERSION: '1.0.0',
|
||||
});
|
||||
expect(buildNumber).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('managed project', () => {
|
||||
it('reads the version from app config', async () => {
|
||||
const exp = initGenericProject();
|
||||
const shortVersion = await readShortVersionAsync('/repo', exp);
|
||||
const shortVersion = await readShortVersionAsync('/repo', exp, {});
|
||||
expect(shortVersion).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function initGenericProject(): ExpoConfig {
|
||||
function initGenericProject({
|
||||
shortVersion = '1.0.0',
|
||||
version = '1',
|
||||
}: { shortVersion?: string; version?: string } = {}): ExpoConfig {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
@@ -197,9 +209,9 @@ function initGenericProject(): ExpoConfig {
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<string>${shortVersion}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>${version}</string>
|
||||
</dict>
|
||||
</plist>`,
|
||||
},
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
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 { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { IosMetadataContext } from '../metadata';
|
||||
import { Platform } from '../types';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { ensureIosCredentialsAsync } from './credentials';
|
||||
@@ -16,51 +17,55 @@ 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,
|
||||
});
|
||||
},
|
||||
getMetadataContext: (): IosMetadataContext => {
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
return { buildSettings: {} };
|
||||
}
|
||||
const applicationTarget = findApplicationTarget(targets);
|
||||
const project = IOSConfig.XcodeUtils.getPbxproj(ctx.projectDir);
|
||||
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
|
||||
targetName: applicationTarget.targetName,
|
||||
buildConfiguration: applicationTarget.buildConfiguration,
|
||||
});
|
||||
const buildSettings = xcBuildConfiguration?.buildSettings ?? {};
|
||||
return { buildSettings };
|
||||
},
|
||||
prepareJobAsync: async (
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData<IosCredentials>
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function validateAndSyncProjectConfigurationAsync({
|
||||
}): Promise<void> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
const { autoIncrement } = buildProfile;
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement);
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
|
||||
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
|
||||
@@ -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,
|
||||
@@ -45,5 +41,5 @@ export async function ensureIosCredentialsAsync(
|
||||
}
|
||||
|
||||
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
|
||||
return buildCtx.buildProfile.distribution !== 'simulator';
|
||||
return !buildCtx.buildProfile.simulator;
|
||||
}
|
||||
@@ -15,13 +15,18 @@ interface JobData {
|
||||
buildScheme: string;
|
||||
}
|
||||
|
||||
const cacheDefaults = {
|
||||
disabled: false,
|
||||
customPaths: [],
|
||||
cacheDefaultPaths: true,
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -30,12 +35,12 @@ export async function prepareJobAsync(
|
||||
}
|
||||
}
|
||||
|
||||
const job = {
|
||||
const job: Ios.Job = {
|
||||
type: ctx.workflow,
|
||||
platform: Platform.IOS,
|
||||
projectArchive: jobData.projectArchive,
|
||||
projectRootDirectory,
|
||||
distribution: ctx.buildProfile.distribution,
|
||||
distribution: ctx.buildProfile.scheme ? 'simulator' : ctx.buildProfile.distribution,
|
||||
builderEnvironment: {
|
||||
image: resolveImage(ctx),
|
||||
node: ctx.buildProfile.node,
|
||||
@@ -47,8 +52,9 @@ export async function prepareJobAsync(
|
||||
env: ctx.buildProfile.env,
|
||||
},
|
||||
cache: {
|
||||
...cacheDefaults,
|
||||
...ctx.buildProfile.cache,
|
||||
clear: ctx.commandCtx.clearCache,
|
||||
clear: ctx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
buildCredentials,
|
||||
@@ -57,20 +63,21 @@ export async function prepareJobAsync(
|
||||
updates: { channel: ctx.buildProfile.channel },
|
||||
|
||||
scheme: jobData.buildScheme,
|
||||
buildConfiguration: ctx.buildProfile.schemeBuildConfiguration,
|
||||
buildConfiguration: ctx.buildProfile.buildConfiguration,
|
||||
artifactPath: ctx.buildProfile.artifactPath,
|
||||
|
||||
buildType: ctx.buildProfile.buildType,
|
||||
buildType: ctx.buildProfile.developmentClient
|
||||
? Ios.BuildType.DEVELOPMENT_CLIENT
|
||||
: Ios.BuildType.RELEASE,
|
||||
username,
|
||||
};
|
||||
|
||||
return sanitizeJob(job);
|
||||
}
|
||||
|
||||
function resolveImage(ctx: BuildContext<Platform.IOS>): Ios.BuilderEnvironment['image'] {
|
||||
// see https://linear.app/expo/issue/ENG-1396/make-default-image-dependent-on-sdk-version
|
||||
if (!ctx.buildProfile.image && ctx.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';
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
import semver from 'semver';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import Log from '../../log';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
||||
import { evaluateString } from '../utils/template';
|
||||
import { readPlistAsync, writePlistAsync } from './plist';
|
||||
|
||||
export enum BumpStrategy {
|
||||
@@ -104,12 +106,16 @@ export async function bumpVersionInAppJsonAsync({
|
||||
|
||||
export async function readShortVersionAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
exp: ExpoConfig,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): Promise<string | undefined> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const infoPlist = await readInfoPlistAsync(projectDir);
|
||||
return infoPlist.CFBundleShortVersionString;
|
||||
return (
|
||||
infoPlist.CFBundleShortVersionString &&
|
||||
evaluateString(infoPlist.CFBundleShortVersionString, buildSettings)
|
||||
);
|
||||
} else {
|
||||
return exp.version;
|
||||
}
|
||||
@@ -117,12 +123,13 @@ export async function readShortVersionAsync(
|
||||
|
||||
export async function readBuildNumberAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
exp: ExpoConfig,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): Promise<string | undefined> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const infoPlist = await readInfoPlistAsync(projectDir);
|
||||
return infoPlist.CFBundleVersion;
|
||||
return infoPlist.CFBundleVersion && evaluateString(infoPlist.CFBundleVersion, buildSettings);
|
||||
} else {
|
||||
return IOSConfig.Version.getBuildNumber(exp);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Metadata } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import { Metadata, sanitizeMetadata } from '@expo/eas-build-job';
|
||||
import { IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import { getApplicationIdAsync } from '../project/android/applicationId';
|
||||
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
|
||||
@@ -27,50 +28,65 @@ import { isExpoUpdatesInstalled } from './utils/updates';
|
||||
*/
|
||||
const packageJSON = require('../../package.json');
|
||||
|
||||
export type MetadataContext<T extends Platform> = T extends Platform.ANDROID
|
||||
? object
|
||||
: IosMetadataContext;
|
||||
|
||||
export interface IosMetadataContext {
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}
|
||||
|
||||
export async function collectMetadata<T extends Platform>(
|
||||
ctx: BuildContext<T>,
|
||||
{
|
||||
credentialsSource,
|
||||
}: {
|
||||
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
|
||||
}
|
||||
platformContext?: MetadataContext<T>
|
||||
): Promise<Metadata> {
|
||||
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
|
||||
return {
|
||||
const metadata = {
|
||||
trackingContext: ctx.trackingCtx,
|
||||
appVersion: await resolveAppVersionAsync(ctx),
|
||||
appBuildVersion: await resolveAppBuildVersionAsync(ctx),
|
||||
appBuildVersion: await resolveAppBuildVersionAsync(ctx, platformContext),
|
||||
cliVersion: packageJSON.version,
|
||||
workflow: ctx.workflow,
|
||||
credentialsSource,
|
||||
sdkVersion: ctx.commandCtx.exp.sdkVersion,
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
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>
|
||||
),
|
||||
}),
|
||||
};
|
||||
return sanitizeMetadata(metadata);
|
||||
}
|
||||
|
||||
async function resolveAppVersionAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
ctx: BuildContext<T>,
|
||||
platformContext?: MetadataContext<T>
|
||||
): Promise<string | undefined> {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
return await readShortVersionAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
const iosContext = platformContext as IosMetadataContext | undefined;
|
||||
return await readShortVersionAsync(ctx.projectDir, ctx.exp, iosContext?.buildSettings ?? {});
|
||||
} else {
|
||||
return await readVersionNameAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
return await readVersionNameAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveAppBuildVersionAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
ctx: BuildContext<T>,
|
||||
platformContext?: MetadataContext<T>
|
||||
): Promise<string | undefined> {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
return await readBuildNumberAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
const iosContext = platformContext as IosMetadataContext | undefined;
|
||||
return await readBuildNumberAsync(ctx.projectDir, ctx.exp, iosContext?.buildSettings ?? {});
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
@@ -79,16 +95,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) {
|
||||
@@ -110,10 +126,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';
|
||||
@@ -125,12 +141,18 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveIosEnterpriseProvisioning(
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
): IosEnterpriseProvisioning | undefined {
|
||||
return ctx.buildProfile.enterpriseProvisioning;
|
||||
}
|
||||
@@ -15,4 +15,10 @@ export enum BuildStatus {
|
||||
CANCELED = 'canceled',
|
||||
}
|
||||
|
||||
export enum BuildDistributionType {
|
||||
STORE = 'store',
|
||||
INTERNAL = 'internal',
|
||||
SIMULATOR = 'simulator',
|
||||
}
|
||||
|
||||
export type TrackingContext = Record<string, string | number | boolean>;
|
||||
@@ -7,9 +7,10 @@ import { requestedPlatformDisplayNames } from '../constants';
|
||||
|
||||
export function logCredentialsSource(credentialsSource: CredentialsSource, platform: Platform) {
|
||||
let message = `Using ${credentialsSource} ${requestedPlatformDisplayNames[platform]} credentials`;
|
||||
if (credentialsSource === CredentialsSource.LOCAL)
|
||||
if (credentialsSource === CredentialsSource.LOCAL) {
|
||||
message += ` ${chalk.dim('(credentials.json)')}`;
|
||||
else if (credentialsSource === CredentialsSource.REMOTE)
|
||||
} else if (credentialsSource === CredentialsSource.REMOTE) {
|
||||
message += ` ${chalk.dim('(Expo server)')}`;
|
||||
}
|
||||
Log.succeed(message);
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { BuildFragment, BuildStatus as GraphQLBuildStatus } from '../../graphql/generated';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildStatus as GraphQLBuildStatus,
|
||||
} from '../../graphql/generated';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { appPlatformDisplayNames } from '../constants';
|
||||
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 }[] = [
|
||||
const fields: { label: string; value?: string | null }[] = [
|
||||
{ label: 'ID', value: build.id },
|
||||
{
|
||||
label: 'Platform',
|
||||
@@ -37,15 +40,43 @@ export function formatGraphQLBuild(build: BuildFragment) {
|
||||
},
|
||||
{
|
||||
label: 'Distribution',
|
||||
value: build.distribution?.toLowerCase() ?? chalk.gray('unknown'),
|
||||
value: build.distribution?.toLowerCase(),
|
||||
},
|
||||
{
|
||||
label: 'Enterprise Provisioning',
|
||||
value: build.iosEnterpriseProvisioning?.toLowerCase(),
|
||||
},
|
||||
{
|
||||
label: 'Release Channel',
|
||||
value: build.releaseChannel ?? chalk.gray('unknown'),
|
||||
value: build.releaseChannel,
|
||||
},
|
||||
{
|
||||
label: 'Channel',
|
||||
value: build.channel,
|
||||
},
|
||||
{
|
||||
label: 'SDK Version',
|
||||
value: build.sdkVersion,
|
||||
},
|
||||
{
|
||||
label: 'Runtime Version',
|
||||
value: build.runtimeVersion,
|
||||
},
|
||||
{
|
||||
label: 'Version',
|
||||
value: build.appVersion,
|
||||
},
|
||||
{
|
||||
label: build.platform === AppPlatform.Android ? 'Version code' : 'Build number',
|
||||
value: build.appBuildVersion,
|
||||
},
|
||||
{
|
||||
label: 'Commit',
|
||||
value: build.gitCommitHash,
|
||||
},
|
||||
{
|
||||
label: 'Logs',
|
||||
value: getBuildLogsUrl({ buildId: build.id, account }),
|
||||
value: getBuildLogsUrl(build),
|
||||
},
|
||||
{
|
||||
label: 'Artifact',
|
||||
@@ -57,13 +88,13 @@ export function formatGraphQLBuild(build: BuildFragment) {
|
||||
return '<in progress>';
|
||||
case GraphQLBuildStatus.Canceled:
|
||||
case GraphQLBuildStatus.Errored:
|
||||
return '---------';
|
||||
return null;
|
||||
case GraphQLBuildStatus.Finished: {
|
||||
const url = build.artifacts?.buildUrl;
|
||||
return url ? url : chalk.red('not found');
|
||||
}
|
||||
default:
|
||||
return 'unknown';
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -81,7 +112,11 @@ export function formatGraphQLBuild(build: BuildFragment) {
|
||||
{ label: 'Started by', value: actor ?? 'unknown' },
|
||||
];
|
||||
|
||||
return formatFields(fields);
|
||||
const filteredFields = fields.filter(({ value }) => value !== undefined && value !== null) as {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
return formatFields(filteredFields);
|
||||
}
|
||||
|
||||
const getActorName = (build: BuildFragment): string => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import template from 'lodash/template';
|
||||
|
||||
export function evaluateString(s: string, vars: Record<string, any>): string {
|
||||
return template(s, { interpolate: /\$\(([\s\S]+?)\)/g })(vars);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -130,7 +130,10 @@ export default class BranchPublish extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const { exp } = getConfig(projectDir, {
|
||||
skipSDKVersionRequirement: true,
|
||||
isPublicConfig: true,
|
||||
});
|
||||
let { runtimeVersion, sdkVersion } = exp;
|
||||
|
||||
// When a SDK version is supplied instead of a runtime version and we're in the managed workflow
|
||||
@@ -260,7 +263,7 @@ export default class BranchPublish extends Command {
|
||||
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
|
||||
const assets = collectAssets({ inputDir: inputDir!, platforms });
|
||||
await uploadAssetsAsync(assets);
|
||||
updateInfoGroup = await buildUpdateInfoGroupAsync(assets);
|
||||
updateInfoGroup = await buildUpdateInfoGroupAsync(assets, exp);
|
||||
assetSpinner.succeed('Uploaded assets!');
|
||||
} catch (e) {
|
||||
assetSpinner.fail('Failed to upload assets');
|
||||
|
||||
@@ -23,8 +23,6 @@ import formatFields from '../../utils/formatFields';
|
||||
|
||||
const PAGE_LIMIT = 10_000;
|
||||
|
||||
type TruncatedUpdate = Pick<Update, 'group' | 'message' | 'createdAt' | 'actor'>;
|
||||
|
||||
export async function viewUpdateBranchAsync({
|
||||
appId,
|
||||
name,
|
||||
|
||||
@@ -67,9 +67,9 @@ async function selectBuildToCancelAsync(
|
||||
let builds;
|
||||
try {
|
||||
const [newBuilds, inQueueBuilds, inProgressBuilds] = await Promise.all([
|
||||
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.New }),
|
||||
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.InQueue }),
|
||||
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.InProgress }),
|
||||
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.New } }),
|
||||
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.InQueue } }),
|
||||
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.InProgress } }),
|
||||
]);
|
||||
spinner.stop();
|
||||
builds = [...newBuilds, ...inQueueBuilds, ...inProgressBuilds];
|
||||
|
||||
@@ -1,24 +1,54 @@
|
||||
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 { configureAsync } from '../../build/configure';
|
||||
import { createCommandContextAsync } from '../../build/context';
|
||||
import { buildAsync } from '../../build/create';
|
||||
import { RequestedPlatform } from '../../build/types';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import {
|
||||
EasJsonReader,
|
||||
hasMismatchedExtendsAsync,
|
||||
isUsingDeprecatedFormatAsync,
|
||||
migrateAsync,
|
||||
} from '@expo/eas-json';
|
||||
import { flags } from '@oclif/command';
|
||||
import { error, exit } from '@oclif/errors';
|
||||
|
||||
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 { 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 { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { confirmAsync, 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 +86,177 @@ 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;
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
|
||||
|
||||
await vcs.ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(flags.nonInteractive);
|
||||
|
||||
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
|
||||
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const platformsToBuild = this.getPlatformsToBuild(requestedPlatform);
|
||||
|
||||
const startedBuilds: BuildFragment[] = [];
|
||||
let metroConfigValidated = false;
|
||||
for (const platform of platformsToBuild) {
|
||||
const ctx = await createBuildContextAsync({
|
||||
buildProfileName: flags.profile,
|
||||
clearCache: flags.clearCache,
|
||||
buildProfile: await easJsonReader.readBuildProfileAsync(flags.profile, platform),
|
||||
local: flags.local,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
platform,
|
||||
projectDir,
|
||||
skipProjectConfiguration: flags.skipProjectConfiguration,
|
||||
});
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED && !metroConfigValidated) {
|
||||
await validateMetroConfigForManagedWorkflowAsync(ctx);
|
||||
metroConfigValidated = true;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleDeprecatedEasJsonAsync(
|
||||
projectDir: string,
|
||||
nonInteractive: boolean
|
||||
): Promise<void> {
|
||||
if (await isUsingDeprecatedFormatAsync(projectDir)) {
|
||||
const hasMismatchedExtendsKeys = await hasMismatchedExtendsAsync(projectDir);
|
||||
if (nonInteractive) {
|
||||
Log.error('We detected that your eas.json is using a deprecated format.');
|
||||
Log.error(
|
||||
'We will convert it automatically if run this command without --non-interactive flag. Alternatively, you can update eas.json manually according to https://docs.expo.io/build/eas-json'
|
||||
);
|
||||
error('Unsupported eas.json format', { exit: 1 });
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message:
|
||||
'We detected that your eas.json is using a deprecated format, do you want to migrate to the new format?',
|
||||
});
|
||||
if (confirm) {
|
||||
await migrateAsync(projectDir);
|
||||
if (hasMismatchedExtendsKeys) {
|
||||
Log.warn(
|
||||
'"extends" keyword can only be migrated automatically to the new format if both Android and iOS profiles extend base profiles with the same names for both platforms'
|
||||
);
|
||||
Log.warn(
|
||||
'Migration was successful, but you need to manually adjust the extend rules for your profiles'
|
||||
);
|
||||
error('Fix eas.json manually', { exit: 1 });
|
||||
}
|
||||
} else {
|
||||
Log.error(
|
||||
"Aborting, update your eas.json according to https://docs.expo.io/build/eas-json and run 'eas build' again"
|
||||
);
|
||||
error('Unsupported eas.json format', { exit: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,13 @@ import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
|
||||
import { BuildStatus, RequestedPlatform } from '../../build/types';
|
||||
import { BuildDistributionType, BuildStatus, RequestedPlatform } from '../../build/types';
|
||||
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
|
||||
import { AppPlatform, BuildStatus as GraphQLBuildStatus } from '../../graphql/generated';
|
||||
import {
|
||||
AppPlatform,
|
||||
DistributionType,
|
||||
BuildStatus as GraphQLBuildStatus,
|
||||
} from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
@@ -31,16 +35,36 @@ export default class BuildList extends Command {
|
||||
BuildStatus.CANCELED,
|
||||
],
|
||||
}),
|
||||
distribution: flags.enum({
|
||||
options: [
|
||||
BuildDistributionType.STORE,
|
||||
BuildDistributionType.INTERNAL,
|
||||
BuildDistributionType.SIMULATOR,
|
||||
],
|
||||
}),
|
||||
channel: flags.string(),
|
||||
appVersion: flags.string(),
|
||||
appBuildVersion: flags.string(),
|
||||
sdkVersion: flags.string(),
|
||||
runtimeVersion: flags.string(),
|
||||
appIdentifier: flags.string(),
|
||||
buildProfile: flags.string(),
|
||||
gitCommitHash: flags.string(),
|
||||
limit: flags.integer(),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const { platform: requestedPlatform, status: buildStatus, limit = 10 } = this.parse(
|
||||
BuildList
|
||||
).flags;
|
||||
const { flags } = this.parse(BuildList);
|
||||
const {
|
||||
platform: requestedPlatform,
|
||||
status: buildStatus,
|
||||
distribution: buildDistribution,
|
||||
limit = 10,
|
||||
} = flags;
|
||||
|
||||
const platform = toAppPlatform(requestedPlatform);
|
||||
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
|
||||
const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution);
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
@@ -52,8 +76,19 @@ export default class BuildList extends Command {
|
||||
try {
|
||||
const builds = await BuildQuery.allForAppAsync(projectId, {
|
||||
limit,
|
||||
platform,
|
||||
status: graphqlBuildStatus,
|
||||
filter: {
|
||||
platform,
|
||||
status: graphqlBuildStatus,
|
||||
distribution: graphqlBuildDistribution,
|
||||
channel: flags.channel,
|
||||
appVersion: flags.appVersion,
|
||||
appBuildVersion: flags.appBuildVersion,
|
||||
sdkVersion: flags.sdkVersion,
|
||||
runtimeVersion: flags.runtimeVersion,
|
||||
appIdentifier: flags.appIdentifier,
|
||||
buildProfile: flags.buildProfile,
|
||||
gitCommitHash: flags.gitCommitHash,
|
||||
},
|
||||
});
|
||||
|
||||
if (builds.length) {
|
||||
@@ -90,7 +125,7 @@ const toAppPlatform = (requestedPlatform?: RequestedPlatform): AppPlatform | und
|
||||
}
|
||||
};
|
||||
|
||||
const toGraphQLBuildStatus = (buildStatus: BuildStatus): GraphQLBuildStatus | undefined => {
|
||||
const toGraphQLBuildStatus = (buildStatus?: BuildStatus): GraphQLBuildStatus | undefined => {
|
||||
if (!buildStatus) {
|
||||
return undefined;
|
||||
} else if (buildStatus === BuildStatus.NEW) {
|
||||
@@ -107,3 +142,17 @@ const toGraphQLBuildStatus = (buildStatus: BuildStatus): GraphQLBuildStatus | un
|
||||
return GraphQLBuildStatus.Canceled;
|
||||
}
|
||||
};
|
||||
|
||||
const toGraphQLBuildDistribution = (
|
||||
buildDistribution?: BuildDistributionType
|
||||
): DistributionType | undefined => {
|
||||
if (buildDistribution === BuildDistributionType.STORE) {
|
||||
return DistributionType.Store;
|
||||
} else if (buildDistribution === BuildDistributionType.INTERNAL) {
|
||||
return DistributionType.Internal;
|
||||
} else if (buildDistribution === BuildDistributionType.SIMULATOR) {
|
||||
return DistributionType.Simulator;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { getProjectConfigDescription } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
|
||||
import Log from '../log';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { selectAsync } from '../prompts';
|
||||
import { handleDeprecatedEasJsonAsync } from './build';
|
||||
export { Platform } from '@expo/eas-build-job';
|
||||
|
||||
export default class Config extends Command {
|
||||
static description = 'Show the eas.json config';
|
||||
|
||||
static flags = {
|
||||
platform: flags.enum({ char: 'p', options: ['android', 'ios'] }),
|
||||
profile: flags.string(),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const { flags } = this.parse(Config);
|
||||
const { platform: maybePlatform, profile: maybeProfile } = flags as {
|
||||
platform?: Platform;
|
||||
profile?: string;
|
||||
};
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
await handleDeprecatedEasJsonAsync(projectDir, false);
|
||||
|
||||
const reader = new EasJsonReader(projectDir);
|
||||
const profileName =
|
||||
maybeProfile ??
|
||||
(await selectAsync(
|
||||
'Select build profile',
|
||||
(
|
||||
await reader.getBuildProfileNamesAsync()
|
||||
).map(profileName => ({
|
||||
title: profileName,
|
||||
value: profileName,
|
||||
}))
|
||||
));
|
||||
const platform =
|
||||
maybePlatform ??
|
||||
(await selectAsync('Select platform', [
|
||||
{
|
||||
title: 'Android',
|
||||
value: Platform.ANDROID,
|
||||
},
|
||||
{
|
||||
title: 'iOS',
|
||||
value: Platform.IOS,
|
||||
},
|
||||
]));
|
||||
|
||||
const profile = await reader.readBuildProfileAsync(profileName, platform);
|
||||
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
|
||||
|
||||
Log.log(getProjectConfigDescription(projectDir));
|
||||
Log.newLine();
|
||||
Log.log(JSON.stringify(config, null, 2));
|
||||
Log.newLine();
|
||||
Log.newLine();
|
||||
Log.log(`Build profile "${profileName}" from eas.json for platform ${platform}`);
|
||||
Log.newLine();
|
||||
Log.log(JSON.stringify(profile, null, 2));
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ async function projectInfoByIdAsync(appId: string): Promise<AppInfoQuery> {
|
||||
}
|
||||
|
||||
export default class ProjectInfo extends Command {
|
||||
static hidden = true;
|
||||
static description = 'information about the current project';
|
||||
|
||||
async run() {
|
||||
@@ -49,8 +48,8 @@ export default class ProjectInfo extends Command {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'ID', value: projectId },
|
||||
{ label: 'fullName', value: app.byId.fullName },
|
||||
{ label: 'ID', value: projectId },
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import Log from '../../log';
|
||||
import { findProjectRootAsync, setProjectIdAsync } from '../../project/projectUtils';
|
||||
|
||||
export default class ProjectInit extends Command {
|
||||
static hidden = true;
|
||||
static description = 'create or link an EAS project';
|
||||
static aliases = ['init'];
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@ import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
import {
|
||||
EnvironmentSecretScope,
|
||||
EnvironmentSecretsQuery,
|
||||
} from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
@@ -18,10 +22,6 @@ import { findAccountByName } from '../../user/Account';
|
||||
import { getActorDisplayName } from '../../user/User';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
export enum EnvironmentSecretScope {
|
||||
ACCOUNT = 'account',
|
||||
PROJECT = 'project',
|
||||
}
|
||||
export default class EnvironmentSecretCreate extends Command {
|
||||
static description = 'Create an environment secret on the current project or owner account.';
|
||||
|
||||
@@ -37,12 +37,16 @@ export default class EnvironmentSecretCreate extends Command {
|
||||
value: flags.string({
|
||||
description: 'Value of the secret',
|
||||
}),
|
||||
force: flags.boolean({
|
||||
description: 'Delete and recreate existing secrets',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const actor = await ensureLoggedInAsync();
|
||||
let {
|
||||
flags: { name, value: secretValue, scope },
|
||||
flags: { name, value: secretValue, scope, force },
|
||||
} = this.parse(EnvironmentSecretCreate);
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
@@ -93,7 +97,9 @@ export default class EnvironmentSecretCreate extends Command {
|
||||
},
|
||||
}));
|
||||
|
||||
if (!name) throw new Error('Secret name may not be empty.');
|
||||
if (!name) {
|
||||
throw new Error('Secret name may not be empty.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!secretValue) {
|
||||
@@ -106,10 +112,26 @@ export default class EnvironmentSecretCreate extends Command {
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
|
||||
if (!secretValue) throw new Error(validationMessage);
|
||||
if (!secretValue) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (scope === EnvironmentSecretScope.PROJECT) {
|
||||
if (force) {
|
||||
const existingSecrets = await EnvironmentSecretsQuery.byAppIdAsync(projectId);
|
||||
const existingSecret = existingSecrets.find(secret => secret.name === name);
|
||||
|
||||
if (existingSecret) {
|
||||
await EnvironmentSecretMutation.delete(existingSecret.id);
|
||||
Log.withTick(
|
||||
`Deleting existing secret ${chalk.bold(name)} on project ${chalk.bold(
|
||||
`@${accountName}/${slug}`
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const secret = await EnvironmentSecretMutation.createForApp(
|
||||
{ name, value: secretValue },
|
||||
projectId
|
||||
@@ -136,6 +158,20 @@ export default class EnvironmentSecretCreate extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
if (force) {
|
||||
const existingSecrets = await EnvironmentSecretsQuery.byAcccountNameAsync(projectId);
|
||||
const existingSecret = existingSecrets.find(secret => secret.name === name);
|
||||
|
||||
if (existingSecret) {
|
||||
await EnvironmentSecretMutation.delete(existingSecret.id);
|
||||
Log.withTick(
|
||||
`Deleting existing secret ${chalk.bold(name)} on account ${chalk.bold(
|
||||
ownerAccount.name
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const secret = await EnvironmentSecretMutation.createForAccount(
|
||||
{ name, value: secretValue },
|
||||
ownerAccount.id
|
||||
|
||||
@@ -4,6 +4,7 @@ import chalk from 'chalk';
|
||||
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
import {
|
||||
EnvironmentSecretScope,
|
||||
EnvironmentSecretWithScope,
|
||||
EnvironmentSecretsQuery,
|
||||
} from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
@@ -15,12 +16,10 @@ import {
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { EnvironmentSecretScope } from './create';
|
||||
|
||||
export default class EnvironmentSecretDelete extends Command {
|
||||
static description = `Delete an environment secret by ID.
|
||||
@@ -38,7 +37,6 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectFullName = await getProjectFullNameAsync(exp);
|
||||
const projectAccountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
@@ -55,7 +53,7 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
if (!id) {
|
||||
const validationMessage = 'You must select which secret to delete.';
|
||||
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectFullName);
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectId);
|
||||
|
||||
({ secret } = await promptAsync({
|
||||
type: 'autocomplete',
|
||||
@@ -69,7 +67,9 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
|
||||
id = secret?.id;
|
||||
|
||||
if (!id) throw new Error(validationMessage);
|
||||
if (!id) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
@@ -27,7 +26,6 @@ export default class EnvironmentSecretList extends Command {
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectFullName = await getProjectFullNameAsync(exp);
|
||||
const projectAccountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
@@ -40,7 +38,7 @@ export default class EnvironmentSecretList extends Command {
|
||||
throw new Error("Please run this command inside your project's directory");
|
||||
}
|
||||
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectFullName);
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectId);
|
||||
|
||||
const table = new Table({
|
||||
head: ['Name', 'Scope', 'ID', 'Updated at'],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource, IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import { CredentialsSource, DistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
|
||||
import { CommonIosAppCredentialsFragment } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
@@ -18,7 +18,7 @@ import { isAdHocProfile } from './utils/provisioningProfile';
|
||||
interface Options {
|
||||
app: App;
|
||||
targets: Target[];
|
||||
distribution: IosDistributionType;
|
||||
distribution: DistributionType;
|
||||
enterpriseProvisioning?: IosEnterpriseProvisioning;
|
||||
iosCapabilitiesOptions?: IosCapabilitiesOptions;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import { DistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
@@ -12,7 +12,7 @@ import { SetupTargetBuildCredentials } from './SetupTargetBuildCredentials';
|
||||
interface Options {
|
||||
app: App;
|
||||
targets: Target[];
|
||||
distribution: IosDistributionType;
|
||||
distribution: DistributionType;
|
||||
enterpriseProvisioning?: IosEnterpriseProvisioning;
|
||||
iosCapabilitiesOptions?: IosCapabilitiesOptions;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import { DistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
|
||||
import {
|
||||
IosDistributionType as GraphQLIosDistributionType,
|
||||
@@ -14,7 +14,7 @@ import { SetupProvisioningProfile } from './SetupProvisioningProfile';
|
||||
|
||||
interface Options {
|
||||
app: GraphQLAppLookupParams;
|
||||
distribution: IosDistributionType;
|
||||
distribution: DistributionType;
|
||||
enterpriseProvisioning?: IosEnterpriseProvisioning;
|
||||
iosCapabilitiesOptions?: IosCapabilitiesOptions;
|
||||
}
|
||||
|
||||
+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
|
||||
|
||||
+40
-1
@@ -1,6 +1,21 @@
|
||||
import { BundleIdCapability, CapabilityType } from '@expo/apple-utils';
|
||||
|
||||
import { syncCapabilitiesForEntitlementsAsync } from '../bundleIdCapabilities';
|
||||
import {
|
||||
CapabilityMapping,
|
||||
assertValidOptions,
|
||||
syncCapabilitiesForEntitlementsAsync,
|
||||
} from '../bundleIdCapabilities';
|
||||
|
||||
describe(assertValidOptions, () => {
|
||||
it(`adds a reason for asserting capability identifiers`, () => {
|
||||
const classifier = CapabilityMapping.find(
|
||||
({ capabilityIdPrefix }) => capabilityIdPrefix === 'merchant.'
|
||||
)!;
|
||||
expect(() => assertValidOptions(classifier, ['foobar'])).toThrowError(
|
||||
/Expected an array of strings, where each string is prefixed with "merchant."/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
it('enables all capabilities', async () => {
|
||||
@@ -33,6 +48,10 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
'com.apple.developer.applesignin': ['Default'],
|
||||
'com.apple.developer.siri': true,
|
||||
'com.apple.developer.networking.wifi-info': true,
|
||||
'com.apple.developer.usernotifications.communication': true,
|
||||
'com.apple.developer.usernotifications.time-sensitive': true,
|
||||
'com.apple.developer.group-session': true,
|
||||
'com.apple.developer.family-controls': true,
|
||||
'com.apple.developer.authentication-services.autofill-credential-provider': true,
|
||||
// 'com.apple.developer.game-center': true,
|
||||
'com.apple.security.application-groups': [
|
||||
@@ -61,6 +80,22 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
{ capabilityType: 'APPLE_ID_AUTH', option: 'ON' },
|
||||
{ capabilityType: 'SIRIKIT', option: 'ON' },
|
||||
{ capabilityType: 'ACCESS_WIFI_INFORMATION', option: 'ON' },
|
||||
{
|
||||
capabilityType: 'USERNOTIFICATIONS_COMMUNICATION',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'USERNOTIFICATIONS_TIMESENSITIVE',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'GROUP_ACTIVITIES',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'FAMILY_CONTROLS',
|
||||
option: 'ON',
|
||||
},
|
||||
{ capabilityType: 'AUTOFILL_CREDENTIAL_PROVIDER', option: 'ON' },
|
||||
{ capabilityType: 'APP_GROUPS', option: 'ON' },
|
||||
]);
|
||||
@@ -86,6 +121,10 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
'Sign In with Apple',
|
||||
'SiriKit',
|
||||
'Access WiFi Information',
|
||||
'Communication Notifications',
|
||||
'Time Sensitive Notifications',
|
||||
'Group Activities',
|
||||
'Family Controls',
|
||||
'AutoFill Credential Provider',
|
||||
'App Groups',
|
||||
]);
|
||||
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
import { syncCapabilityIdentifiersForEntitlementsAsync } from '../capabilityIdentifiers';
|
||||
|
||||
function mockCapabilities(Apple: any) {
|
||||
Apple.MerchantId.getAsync = jest.fn(() => [
|
||||
{
|
||||
id: 'XXX-merch-1',
|
||||
attributes: {
|
||||
identifier: 'merchant.expo',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
Apple.MerchantId.createAsync = jest.fn((ctx, { identifier }) => ({
|
||||
id: 'XXX-merch-2',
|
||||
attributes: {
|
||||
identifier,
|
||||
},
|
||||
}));
|
||||
|
||||
Apple.AppGroup.getAsync = jest.fn(() => [
|
||||
{
|
||||
id: 'XXX-group-1',
|
||||
attributes: {
|
||||
identifier: 'group.expo',
|
||||
},
|
||||
},
|
||||
]);
|
||||
Apple.AppGroup.createAsync = jest.fn((ctx, { identifier }) => ({
|
||||
id: 'XXX-group-2',
|
||||
attributes: {
|
||||
identifier,
|
||||
},
|
||||
}));
|
||||
|
||||
Apple.CloudContainer.getAsync = jest.fn(() => [
|
||||
{
|
||||
id: 'XXX-icloud-1',
|
||||
attributes: {
|
||||
identifier: 'iCloud.expo',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
Apple.CloudContainer.createAsync = jest.fn((ctx, { identifier }) => ({
|
||||
id: 'XXX-icloud-2',
|
||||
attributes: {
|
||||
identifier,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
describe(syncCapabilityIdentifiersForEntitlementsAsync, () => {
|
||||
it(`creates missing capability identifiers`, async () => {
|
||||
const Apple = require('@expo/apple-utils');
|
||||
mockCapabilities(Apple);
|
||||
|
||||
const bundleId = {
|
||||
context: {},
|
||||
updateBundleIdCapabilityAsync: jest.fn(),
|
||||
id: 'XXX',
|
||||
} as any;
|
||||
|
||||
await syncCapabilityIdentifiersForEntitlementsAsync(bundleId, {
|
||||
'com.apple.developer.in-app-payments': ['merchant.bacon'],
|
||||
'com.apple.security.application-groups': ['group.bacon'],
|
||||
'com.apple.developer.icloud-container-identifiers': ['iCloud.bacon'],
|
||||
});
|
||||
|
||||
// Ensure we create missing ids
|
||||
expect(Apple.MerchantId.createAsync).toHaveBeenLastCalledWith(
|
||||
// auth context
|
||||
{},
|
||||
// props
|
||||
{ identifier: 'merchant.bacon' }
|
||||
);
|
||||
// Ensure we create missing ids
|
||||
expect(Apple.AppGroup.createAsync).toHaveBeenLastCalledWith(
|
||||
// auth context
|
||||
{},
|
||||
// props
|
||||
{ identifier: 'group.bacon' }
|
||||
);
|
||||
// Ensure we create missing ids
|
||||
expect(Apple.CloudContainer.createAsync).toHaveBeenLastCalledWith(
|
||||
// auth context
|
||||
{},
|
||||
// props
|
||||
{ identifier: 'iCloud.bacon' }
|
||||
);
|
||||
|
||||
expect(bundleId.updateBundleIdCapabilityAsync).toBeCalledWith([
|
||||
{
|
||||
capabilityType: 'APP_GROUPS',
|
||||
option: 'ON',
|
||||
relationships: {
|
||||
appGroups: ['XXX-group-2'],
|
||||
},
|
||||
},
|
||||
{
|
||||
capabilityType: 'APPLE_PAY',
|
||||
option: 'ON',
|
||||
relationships: {
|
||||
merchantIds: ['XXX-merch-2'],
|
||||
},
|
||||
},
|
||||
{
|
||||
capabilityType: 'ICLOUD',
|
||||
option: 'ON',
|
||||
relationships: {
|
||||
cloudContainers: ['XXX-icloud-2'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`removes local duplicates`, async () => {
|
||||
const Apple = require('@expo/apple-utils');
|
||||
mockCapabilities(Apple);
|
||||
const bundleId = {
|
||||
context: {},
|
||||
updateBundleIdCapabilityAsync: jest.fn(),
|
||||
id: 'XXX',
|
||||
} as any;
|
||||
|
||||
await syncCapabilityIdentifiersForEntitlementsAsync(bundleId, {
|
||||
'com.apple.developer.in-app-payments': ['merchant.expo', 'merchant.expo'],
|
||||
});
|
||||
|
||||
// Only called once because we remove local duplicates to minimize network requests.
|
||||
expect(Apple.MerchantId.getAsync).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it(`creates missing capability identifiers`, async () => {
|
||||
const Apple = require('@expo/apple-utils');
|
||||
mockCapabilities(Apple);
|
||||
const bundleId = {
|
||||
context: {},
|
||||
updateBundleIdCapabilityAsync: jest.fn(),
|
||||
id: 'XXX',
|
||||
} as any;
|
||||
|
||||
await syncCapabilityIdentifiersForEntitlementsAsync(bundleId, {
|
||||
'com.apple.developer.in-app-payments': ['merchant.expo'],
|
||||
});
|
||||
|
||||
expect(Apple.MerchantId.createAsync).not.toBeCalled();
|
||||
});
|
||||
|
||||
it(`throws when creating a missing capability that is reserved`, async () => {
|
||||
const Apple = require('@expo/apple-utils');
|
||||
mockCapabilities(Apple);
|
||||
Apple.MerchantId.createAsync = jest.fn((ctx: any, { identifier }: { identifier: string }) => {
|
||||
// e2e test with: merchant.expodemo
|
||||
throw new Error(
|
||||
`There is a problem with the request entity - A Merchant ID with Identifier '${identifier}' is not available. Please enter a different string.`
|
||||
);
|
||||
});
|
||||
const bundleId = {
|
||||
context: {},
|
||||
updateBundleIdCapabilityAsync: jest.fn(),
|
||||
id: 'XXX',
|
||||
} as any;
|
||||
|
||||
await expect(
|
||||
syncCapabilityIdentifiersForEntitlementsAsync(bundleId, {
|
||||
'com.apple.developer.in-app-payments': ['merchant.bacon'],
|
||||
})
|
||||
).rejects.toThrow(/is not available. Please enter a different string/);
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,20 @@
|
||||
import {
|
||||
AppGroup,
|
||||
BundleId,
|
||||
BundleIdCapability,
|
||||
CapabilityOptionMap,
|
||||
CapabilityType,
|
||||
CapabilityTypeDataProtectionOption,
|
||||
CapabilityTypeOption,
|
||||
CloudContainer,
|
||||
MerchantId,
|
||||
} from '@expo/apple-utils';
|
||||
import { JSONObject, JSONValue } from '@expo/json-file';
|
||||
import getenv from 'getenv';
|
||||
|
||||
import Log from '../../../log';
|
||||
|
||||
const EXPO_NO_CAPABILITY_SYNC = getenv.boolish('EXPO_NO_CAPABILITY_SYNC', false);
|
||||
export const EXPO_NO_CAPABILITY_SYNC = getenv.boolish('EXPO_NO_CAPABILITY_SYNC', false);
|
||||
|
||||
type GetOptionsMethod<T extends CapabilityType = any> = (
|
||||
entitlement: JSONValue,
|
||||
@@ -22,17 +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 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']);
|
||||
|
||||
@@ -63,7 +79,9 @@ export async function syncCapabilitiesForEntitlementsAsync(
|
||||
bundleId: BundleId,
|
||||
entitlements: JSONObject = {}
|
||||
): Promise<{ enabled: string[]; disabled: string[] }> {
|
||||
if (EXPO_NO_CAPABILITY_SYNC) return { enabled: [], disabled: [] };
|
||||
if (EXPO_NO_CAPABILITY_SYNC) {
|
||||
return { enabled: [], disabled: [] };
|
||||
}
|
||||
const currentCapabilities = await bundleId.getBundleIdCapabilitiesAsync();
|
||||
if (Log.isDebug) {
|
||||
Log.log(`Current remote capabilities:\n${JSON.stringify(currentCapabilities, null, 2)}`);
|
||||
@@ -107,7 +125,7 @@ function getCapabilitiesToEnable(
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingIndex = currentCapabilities.findIndex(existing =>
|
||||
const existingIndex = remainingCapabilities.findIndex(existing =>
|
||||
existing.isType(staticCapabilityInfo.capability)
|
||||
);
|
||||
const existing = existingIndex > -1 ? remainingCapabilities[existingIndex] : null;
|
||||
@@ -120,16 +138,19 @@ function getCapabilitiesToEnable(
|
||||
remainingCapabilities.splice(existingIndex, 1);
|
||||
if (Log.isDebug) {
|
||||
Log.log(`Skipping existing capability: ${key} (${staticCapabilityInfo.name})`);
|
||||
Log.log(
|
||||
`Remaining to remove: `,
|
||||
remainingCapabilities.map(({ id }) => id)
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!staticCapabilityInfo.validateOptions(value)) {
|
||||
throw new Error(`iOS entitlement "${key}" has invalid value "${value}".`);
|
||||
}
|
||||
assertValidOptions(staticCapabilityInfo, value);
|
||||
|
||||
enabledCapabilityNames.push(staticCapabilityInfo.name);
|
||||
|
||||
const option = staticCapabilityInfo.getOptions(value!, entitlements);
|
||||
const option = staticCapabilityInfo.getOptions(value, entitlements);
|
||||
|
||||
request.push({
|
||||
capabilityType: staticCapabilityInfo.capability,
|
||||
@@ -140,11 +161,30 @@ function getCapabilitiesToEnable(
|
||||
return { enabledCapabilityNames, request, remainingCapabilities };
|
||||
}
|
||||
|
||||
export function assertValidOptions(classifier: CapabilityClassifier, value: any): asserts value {
|
||||
if (!classifier.validateOptions(value)) {
|
||||
let reason = '';
|
||||
if (classifier.capabilityIdPrefix) {
|
||||
// Assert string array matching prefix. ASC will throw if the IDs are invalid, this just saves some time.
|
||||
reason = ` Expected an array of strings, where each string is prefixed with "${classifier.capabilityIdPrefix}", ex: ["${classifier.capabilityIdPrefix}myapp"]`;
|
||||
}
|
||||
throw new Error(
|
||||
`iOS entitlement "${classifier.entitlement}" has invalid value "${value}".${reason}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getCapabilitiesToDisable(
|
||||
bundleId: BundleId,
|
||||
currentCapabilities: BundleIdCapability[],
|
||||
request: { capabilityType: CapabilityType; option: any }[]
|
||||
) {
|
||||
if (Log.isDebug) {
|
||||
Log.log(
|
||||
`Existing to disable: `,
|
||||
currentCapabilities.map(({ id }) => id)
|
||||
);
|
||||
}
|
||||
const disabledCapabilityNames: string[] = [];
|
||||
|
||||
// Disable any extras that aren't present, this functionality is kinda unreliable because managed apps
|
||||
@@ -192,17 +232,21 @@ function getCapabilitiesToDisable(
|
||||
return { disabledCapabilityNames, request };
|
||||
}
|
||||
|
||||
// NOTE(Bacon): From manually toggling values in Xcode and checking the git diff and network requests.
|
||||
// Last Updated: May 5th, 2021
|
||||
// https://developer.apple.com/documentation/bundleresources/entitlements
|
||||
export const CapabilityMapping: {
|
||||
type CapabilityClassifier = {
|
||||
name: string;
|
||||
entitlement: string;
|
||||
capability: CapabilityType;
|
||||
validateOptions: (options: any) => boolean;
|
||||
getOptions: GetOptionsMethod;
|
||||
capabilityIdModel?: typeof MerchantId;
|
||||
capabilityIdPrefix?: string;
|
||||
options?: undefined;
|
||||
}[] = [
|
||||
};
|
||||
|
||||
// NOTE(Bacon): From manually toggling values in Xcode and checking the git diff and network requests.
|
||||
// Last Updated: July 22nd, 2021
|
||||
// https://developer.apple.com/documentation/bundleresources/entitlements
|
||||
export const CapabilityMapping: CapabilityClassifier[] = [
|
||||
{
|
||||
name: 'HomeKit',
|
||||
entitlement: 'com.apple.developer.homekit',
|
||||
@@ -287,24 +331,30 @@ export const CapabilityMapping: {
|
||||
entitlement: 'com.apple.security.application-groups',
|
||||
capability: CapabilityType.APP_GROUP,
|
||||
// Ex: ['group.CY-A5149AC2-49FC-11E7-B3F3-0335A16FFB8D.com.cydia.Extender']
|
||||
validateOptions: validateStringArrayOptions,
|
||||
validateOptions: validatePrefixedStringArrayOptions('group.'),
|
||||
getOptions: getDefinedOptions,
|
||||
capabilityIdModel: AppGroup,
|
||||
capabilityIdPrefix: 'group.',
|
||||
},
|
||||
{
|
||||
name: 'Apple Pay Payment Processing',
|
||||
entitlement: 'com.apple.developer.in-app-payments',
|
||||
capability: CapabilityType.APPLE_PAY,
|
||||
// Ex: ['merchant.com.example.development']
|
||||
validateOptions: validateStringArrayOptions,
|
||||
validateOptions: validatePrefixedStringArrayOptions('merchant.'),
|
||||
getOptions: getDefinedOptions,
|
||||
capabilityIdModel: MerchantId,
|
||||
capabilityIdPrefix: 'merchant.',
|
||||
},
|
||||
{
|
||||
name: 'iCloud',
|
||||
entitlement: 'com.apple.developer.icloud-container-identifiers',
|
||||
capability: CapabilityType.ICLOUD,
|
||||
validateOptions: validateStringArrayOptions,
|
||||
validateOptions: validatePrefixedStringArrayOptions('iCloud.'),
|
||||
// Only supports Xcode +6, 5 could be added if needed.
|
||||
getOptions: getDefinedOptions,
|
||||
capabilityIdModel: CloudContainer,
|
||||
capabilityIdPrefix: 'iCloud.',
|
||||
},
|
||||
{
|
||||
name: 'ClassKit',
|
||||
@@ -313,6 +363,34 @@ export const CapabilityMapping: {
|
||||
validateOptions: validateDevProdString,
|
||||
getOptions: getDefinedOptions,
|
||||
},
|
||||
{
|
||||
name: 'Communication Notifications',
|
||||
entitlement: 'com.apple.developer.usernotifications.communication',
|
||||
capability: CapabilityType.USER_NOTIFICATIONS_COMMUNICATION,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
name: 'Time Sensitive Notifications',
|
||||
entitlement: 'com.apple.developer.usernotifications.time-sensitive',
|
||||
capability: CapabilityType.USER_NOTIFICATIONS_TIME_SENSITIVE,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
name: 'Group Activities',
|
||||
entitlement: 'com.apple.developer.group-session',
|
||||
capability: CapabilityType.GROUP_ACTIVITIES,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
name: 'Family Controls',
|
||||
entitlement: 'com.apple.developer.family-controls',
|
||||
capability: CapabilityType.FAMILY_CONTROLS,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
// https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_default-data-protection
|
||||
name: 'Data Protection',
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { BundleId, CapabilityTypeOption } from '@expo/apple-utils';
|
||||
import { JSONObject } from '@expo/json-file';
|
||||
|
||||
import Log from '../../../log';
|
||||
import {
|
||||
CapabilityMapping,
|
||||
EXPO_NO_CAPABILITY_SYNC,
|
||||
assertValidOptions,
|
||||
} from './bundleIdCapabilities';
|
||||
|
||||
type UpdateCapabilityRequest = Parameters<BundleId['updateBundleIdCapabilityAsync']>[0];
|
||||
|
||||
/**
|
||||
* Sync the capability identifiers with the bundle identifier capabilities.
|
||||
* If a capability identifier is missing, then attempt to create it.
|
||||
* Link all of the capability identifiers at the same time after parsing the entitlements file.
|
||||
*
|
||||
* @param bundleId Bundle identifier object.
|
||||
* @param entitlements JSON representation of the iOS entitlements plist
|
||||
*
|
||||
* @returns an object specifying the capabilities that were linked, and created.
|
||||
*/
|
||||
export async function syncCapabilityIdentifiersForEntitlementsAsync(
|
||||
bundleId: BundleId,
|
||||
entitlements: JSONObject = {}
|
||||
): Promise<{ created: string[]; linked: string[] }> {
|
||||
if (EXPO_NO_CAPABILITY_SYNC) {
|
||||
return { created: [], linked: [] };
|
||||
}
|
||||
|
||||
const createdIds: string[] = [];
|
||||
const linkedIds: string[] = [];
|
||||
const CapabilityIdMapping = CapabilityMapping.filter(capability => capability.capabilityIdModel);
|
||||
|
||||
const updateRequest: UpdateCapabilityRequest = [];
|
||||
|
||||
// Iterate through the supported capabilities to build the request.
|
||||
for (const classifier of CapabilityIdMapping) {
|
||||
const CapabilityModel = classifier.capabilityIdModel;
|
||||
// Skip capabilities that don't support capability IDs.
|
||||
if (!CapabilityModel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const validate = (value: any): value is string[] => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
assertValidOptions(classifier, value);
|
||||
return true;
|
||||
};
|
||||
|
||||
// Skip capabilities that aren't defined in the entitlements file.
|
||||
const entitlementValue = entitlements[classifier.entitlement];
|
||||
|
||||
if (!validate(entitlementValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove any duplicates to cut down on network requests
|
||||
const capabilityIds: string[] = [...new Set(entitlementValue)];
|
||||
|
||||
// Get a list of all of the capability IDs that are already created on the server.
|
||||
const existingIds = await CapabilityModel.getAsync(bundleId.context);
|
||||
|
||||
// A list of server IDs for linking.
|
||||
const capabilityIdOpaqueIds = [];
|
||||
|
||||
// Iterate through all the local IDs and see if they exist on the server.
|
||||
for (const localId of capabilityIds) {
|
||||
let remoteIdModel = existingIds.find(model => model.attributes.identifier === localId);
|
||||
|
||||
// If a remote ID exists, then create it.
|
||||
if (!remoteIdModel) {
|
||||
if (Log.isDebug) {
|
||||
Log.log(`Creating capability ID: ${localId} (${CapabilityModel.type})`);
|
||||
}
|
||||
try {
|
||||
remoteIdModel = await CapabilityModel.createAsync(bundleId.context, {
|
||||
identifier: localId,
|
||||
});
|
||||
} catch (error) {
|
||||
// Add a more helpful error message.
|
||||
error.message += `\n\nRemove the value '${localId}' from the array '${classifier.entitlement}' in the iOS project entitlements.\nIf you know that the ID is registered to one of your apps, try again with a different Apple account.`;
|
||||
throw error;
|
||||
}
|
||||
// Create a list of newly created IDs for displaying in the CLI.
|
||||
createdIds.push(localId);
|
||||
if (Log.isDebug) {
|
||||
Log.log(`Created capability ID: ${remoteIdModel.id}`);
|
||||
}
|
||||
}
|
||||
if (Log.isDebug) {
|
||||
Log.log(`Linking ID to ${CapabilityModel.type}: ${localId} (${remoteIdModel.id})`);
|
||||
}
|
||||
// Create a list of linked IDs for displaying in the CLI.
|
||||
linkedIds.push(remoteIdModel.attributes.identifier);
|
||||
capabilityIdOpaqueIds.push(remoteIdModel.id);
|
||||
}
|
||||
|
||||
updateRequest.push({
|
||||
capabilityType: classifier.capability,
|
||||
option: CapabilityTypeOption.ON,
|
||||
relationships: {
|
||||
// One of: `merchantIds`, `appGroups`, `cloudContainers`.
|
||||
[CapabilityModel.type]: capabilityIdOpaqueIds,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (updateRequest.length) {
|
||||
if (Log.isDebug) {
|
||||
Log.log(`Updating bundle identifier with capability identifiers:`, updateRequest);
|
||||
}
|
||||
await bundleId.updateBundleIdCapabilityAsync(updateRequest);
|
||||
} else if (Log.isDebug) {
|
||||
Log.log(`No capability identifiers need to be updated`);
|
||||
}
|
||||
return { created: createdIds, linked: linkedIds };
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import ora from 'ora';
|
||||
import Log from '../../../log';
|
||||
import { AuthCtx, getRequestContext } from './authenticate';
|
||||
import { syncCapabilitiesForEntitlementsAsync } from './bundleIdCapabilities';
|
||||
import { syncCapabilityIdentifiersForEntitlementsAsync } from './capabilityIdentifiers';
|
||||
import { assertContractMessagesAsync } from './contractMessages';
|
||||
|
||||
export interface IosCapabilitiesOptions {
|
||||
@@ -95,8 +96,6 @@ export async function syncCapabilities(
|
||||
bundleId,
|
||||
entitlements
|
||||
);
|
||||
const buildMessage = (title: string, items: string[]) =>
|
||||
items.length ? `${title}: ${items.join(', ')}` : '';
|
||||
const results =
|
||||
[buildMessage('Enabled', enabled), buildMessage('Disabled', disabled)]
|
||||
.filter(Boolean)
|
||||
@@ -107,6 +106,43 @@ export async function syncCapabilities(
|
||||
spinner.fail(`Failed to sync capabilities ${chalk.dim(bundleId.attributes.identifier)}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Always run this after syncing the capabilities...
|
||||
await syncCapabilityIdentifiersAsync(bundleId, { entitlements });
|
||||
}
|
||||
|
||||
const buildMessage = (title: string, items: string[]) =>
|
||||
items.length ? `${title}: ${items.join(', ')}` : '';
|
||||
|
||||
export async function syncCapabilityIdentifiersAsync(
|
||||
bundleId: BundleId,
|
||||
{ entitlements }: IosCapabilitiesOptions
|
||||
): Promise<void> {
|
||||
const spinner = ora(`Syncing capabilities identifiers`).start();
|
||||
|
||||
// Stop spinning in debug mode so we can print other information
|
||||
if (Log.isDebug) {
|
||||
spinner.stop();
|
||||
}
|
||||
|
||||
try {
|
||||
const { created, linked } = await syncCapabilityIdentifiersForEntitlementsAsync(
|
||||
bundleId,
|
||||
entitlements
|
||||
);
|
||||
|
||||
const results =
|
||||
[buildMessage('Created', created), buildMessage('Linked', linked)]
|
||||
.filter(Boolean)
|
||||
.join(' | ') || 'No updates';
|
||||
|
||||
spinner.succeed(`Synced capability identifiers: ` + chalk.dim(results));
|
||||
} catch (err) {
|
||||
spinner.fail(
|
||||
`Failed to sync capability identifiers ${chalk.dim(bundleId.attributes.identifier)}`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureAppExistsAsync(
|
||||
|
||||
@@ -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) {
|
||||
@@ -136,10 +135,11 @@ async function manageAdHocProfilesAsync(
|
||||
if (profileId) {
|
||||
existingProfile = await findProfileByIdAsync(context, profileId, bundleId);
|
||||
// Fail if we cannot find the profile that was specifically requested
|
||||
if (!existingProfile)
|
||||
if (!existingProfile) {
|
||||
throw new Error(
|
||||
`Could not find profile with profile id "${profileId}" for bundle id "${bundleId}"`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If no profile id is passed, try to find a suitable provisioning profile for the App ID.
|
||||
const results = await findProfileByBundleIdAsync(context, bundleId, certSerialNumber);
|
||||
@@ -227,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}`);
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface App {
|
||||
|
||||
export interface Target {
|
||||
targetName: string;
|
||||
buildConfiguration?: string;
|
||||
bundleIdentifier: string;
|
||||
parentBundleIdentifier?: string;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -41,7 +41,9 @@ function getService(rootUrl: string) {
|
||||
},
|
||||
replacement(content: string, node: any) {
|
||||
let href = node.getAttribute('href');
|
||||
if (href.startsWith('/')) href = `${rootUrl}${href}`;
|
||||
if (href.startsWith('/')) {
|
||||
href = `${rootUrl}${href}`;
|
||||
}
|
||||
if (terminalLink.isSupported) {
|
||||
return chalk.cyan(terminalLink(content, href));
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EasJsonReader, IosBuildProfile } from '@expo/eas-json';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { IosBuildProfile } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
@@ -201,9 +202,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);
|
||||
}
|
||||
@@ -285,9 +285,10 @@ export class ManageIos {
|
||||
}
|
||||
|
||||
const app = { account, projectName: ctx.exp.slug };
|
||||
const easJsonReader = new EasJsonReader(ctx.projectDir, 'ios');
|
||||
const easConfig = await new SelectBuildProfileFromEasJson(easJsonReader).runAsync(ctx);
|
||||
const buildProfile = nullthrows(easConfig.builds.ios, 'iOS build profile must be defined');
|
||||
const buildProfile = await new SelectBuildProfileFromEasJson(
|
||||
ctx.projectDir,
|
||||
Platform.IOS
|
||||
).runAsync(ctx);
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||
{
|
||||
projectDir: ctx.projectDir,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { EasConfig, EasJsonReader } from '@expo/eas-json';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile, EasJsonReader } from '@expo/eas-json';
|
||||
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { Context } from '../context';
|
||||
|
||||
export class SelectBuildProfileFromEasJson {
|
||||
constructor(private easJsonReader: EasJsonReader) {}
|
||||
async runAsync(ctx: Context): Promise<EasConfig> {
|
||||
export class SelectBuildProfileFromEasJson<T extends Platform> {
|
||||
private easJsonReader: EasJsonReader;
|
||||
|
||||
constructor(private projectDir: string, private platform: T) {
|
||||
this.easJsonReader = new EasJsonReader(projectDir);
|
||||
}
|
||||
|
||||
async runAsync(ctx: Context): Promise<BuildProfile<T>> {
|
||||
const profileName = await this.getProfileNameFromEasConfigAsync(ctx);
|
||||
const easConfig = await this.easJsonReader.readAsync(profileName);
|
||||
const easConfig = await this.easJsonReader.readBuildProfileAsync<T>(profileName, this.platform);
|
||||
Log.succeed(`Using build profile: ${profileName}`);
|
||||
return easConfig;
|
||||
}
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ export class SelectIosDistributionTypeGraphqlFromBuildProfile {
|
||||
constructor(private buildProfile: IosBuildProfile) {}
|
||||
|
||||
async runAsync(ctx: Context): Promise<IosDistributionTypeGraphql> {
|
||||
const { distribution } = this.buildProfile;
|
||||
if (distribution === 'simulator') {
|
||||
const { distribution, simulator } = this.buildProfile;
|
||||
if (simulator) {
|
||||
throw new Error('A simulator distribution does not require credentials to be configured.');
|
||||
} else if (distribution === 'store') {
|
||||
return IosDistributionTypeGraphql.AppStore;
|
||||
|
||||
+15
-22
@@ -7,15 +7,14 @@ import { SelectIosDistributionTypeGraphqlFromBuildProfile } from '../SelectIosDi
|
||||
describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
|
||||
it('errors with a simulator distribution', async () => {
|
||||
const buildProfile = {
|
||||
distribution: 'simulator',
|
||||
simulator: true,
|
||||
} as IosBuildProfile;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
+182
-44
@@ -58,6 +58,8 @@ export type RootQuery = {
|
||||
buildJobs: BuildJobQuery;
|
||||
builds: BuildQuery;
|
||||
clientBuilds: ClientBuildQuery;
|
||||
/** Top-level query object for querying EAS Build configuration. */
|
||||
easBuild: EasBuildQuery;
|
||||
/** Top-level query object for querying Experimentation configuration. */
|
||||
experimentation: ExperimentationQuery;
|
||||
project: ProjectQuery;
|
||||
@@ -188,8 +190,13 @@ export type Account = {
|
||||
buildJobs: Array<BuildJob>;
|
||||
/** (EAS Build) Builds associated with this account */
|
||||
builds: Array<Build>;
|
||||
/** Coalesced Build (EAS) or BuildJob (Classic) items for this account. Use "createdBefore" to offset a query. */
|
||||
/**
|
||||
* Coalesced Build (EAS) or BuildJob (Classic) for all apps belonging to this account.
|
||||
* @deprecated Use activityTimelineProjectActivities with filterTypes instead
|
||||
*/
|
||||
buildOrBuildJobs: Array<BuildOrBuildJob>;
|
||||
/** Coalesced project activity for all apps belonging to this account. */
|
||||
activityTimelineProjectActivities: Array<ActivityTimelineProjectActivity>;
|
||||
/** Owning User of this account if personal account */
|
||||
owner?: Maybe<User>;
|
||||
/** Actors associated with this account and permissions they hold */
|
||||
@@ -279,6 +286,17 @@ export type AccountBuildOrBuildJobsArgs = {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* An account is a container owning projects, credentials, billing and other organization
|
||||
* data and settings. Actors may own and be members of accounts.
|
||||
*/
|
||||
export type AccountActivityTimelineProjectActivitiesArgs = {
|
||||
limit: Scalars['Int'];
|
||||
createdBefore?: Maybe<Scalars['DateTime']>;
|
||||
filterTypes?: Maybe<Array<ActivityTimelineProjectActivityType>>;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* An account is a container owning projects, credentials, billing and other organization
|
||||
* data and settings. Actors may own and be members of accounts.
|
||||
@@ -411,10 +429,15 @@ export type App = Project & {
|
||||
/** (EAS Build) Builds associated with this app */
|
||||
builds: Array<Build>;
|
||||
buildJobs: Array<BuildJob>;
|
||||
/** Coalesced Build (EAS) or BuildJob (Classic) items for this app. Use "createdBefore" to offset a query. */
|
||||
/**
|
||||
* Coalesced Build (EAS) or BuildJob (Classic) items for this app.
|
||||
* @deprecated Use activityTimelineProjectActivities with filterTypes instead
|
||||
*/
|
||||
buildOrBuildJobs: Array<BuildOrBuildJob>;
|
||||
/** EAS Submissions associated with this app */
|
||||
submissions: Array<Submission>;
|
||||
/** Deployments associated with this app */
|
||||
deployments: Array<Deployment>;
|
||||
/** iOS app credentials for the project */
|
||||
iosAppCredentials: Array<IosAppCredentials>;
|
||||
/** Android app credentials for the project */
|
||||
@@ -427,7 +450,7 @@ export type App = Project & {
|
||||
updateBranches: Array<UpdateBranch>;
|
||||
/** get an EAS branch owned by the app by name */
|
||||
updateBranchByName?: Maybe<UpdateBranch>;
|
||||
/** Coalesced project activity for an app. Use "createdBefore" to offset a query. */
|
||||
/** Coalesced project activity for an app */
|
||||
activityTimelineProjectActivities: Array<ActivityTimelineProjectActivity>;
|
||||
/** Environment secrets for an app */
|
||||
environmentSecrets: Array<EnvironmentSecret>;
|
||||
@@ -466,6 +489,7 @@ export type App = Project & {
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppBuildsArgs = {
|
||||
filter?: Maybe<BuildFilter>;
|
||||
offset: Scalars['Int'];
|
||||
limit: Scalars['Int'];
|
||||
status?: Maybe<BuildStatus>;
|
||||
@@ -496,6 +520,14 @@ export type AppSubmissionsArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppDeploymentsArgs = {
|
||||
limit: Scalars['Int'];
|
||||
buildListMaxSize: Scalars['Int'];
|
||||
mostRecentlyUpdatedAt?: Maybe<Scalars['DateTime']>;
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppIosAppCredentialsArgs = {
|
||||
filter?: Maybe<IosAppCredentialsFilter>;
|
||||
@@ -538,6 +570,7 @@ export type AppUpdateBranchByNameArgs = {
|
||||
export type AppActivityTimelineProjectActivitiesArgs = {
|
||||
limit: Scalars['Int'];
|
||||
createdBefore?: Maybe<Scalars['DateTime']>;
|
||||
filterTypes?: Maybe<Array<ActivityTimelineProjectActivityType>>;
|
||||
};
|
||||
|
||||
|
||||
@@ -583,6 +616,25 @@ export enum AppPrivacy {
|
||||
Hidden = 'HIDDEN'
|
||||
}
|
||||
|
||||
export type BuildFilter = {
|
||||
platform?: Maybe<AppPlatform>;
|
||||
status?: Maybe<BuildStatus>;
|
||||
distribution?: Maybe<DistributionType>;
|
||||
channel?: Maybe<Scalars['String']>;
|
||||
appVersion?: Maybe<Scalars['String']>;
|
||||
appBuildVersion?: Maybe<Scalars['String']>;
|
||||
sdkVersion?: Maybe<Scalars['String']>;
|
||||
runtimeVersion?: Maybe<Scalars['String']>;
|
||||
appIdentifier?: Maybe<Scalars['String']>;
|
||||
buildProfile?: Maybe<Scalars['String']>;
|
||||
gitCommitHash?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export enum AppPlatform {
|
||||
Ios = 'IOS',
|
||||
Android = 'ANDROID'
|
||||
}
|
||||
|
||||
export enum BuildStatus {
|
||||
New = 'NEW',
|
||||
InQueue = 'IN_QUEUE',
|
||||
@@ -592,9 +644,10 @@ export enum BuildStatus {
|
||||
Canceled = 'CANCELED'
|
||||
}
|
||||
|
||||
export enum AppPlatform {
|
||||
Ios = 'IOS',
|
||||
Android = 'ANDROID'
|
||||
export enum DistributionType {
|
||||
Store = 'STORE',
|
||||
Internal = 'INTERNAL',
|
||||
Simulator = 'SIMULATOR'
|
||||
}
|
||||
|
||||
/** Represents an EAS Build */
|
||||
@@ -607,6 +660,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']>;
|
||||
@@ -622,6 +676,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
|
||||
channel?: Maybe<Scalars['String']>;
|
||||
metrics?: Maybe<BuildMetrics>;
|
||||
distribution?: Maybe<DistributionType>;
|
||||
iosEnterpriseProvisioning?: Maybe<BuildIosEnterpriseProvisioning>;
|
||||
buildProfile?: Maybe<Scalars['String']>;
|
||||
gitCommitHash?: Maybe<Scalars['String']>;
|
||||
error?: Maybe<BuildError>;
|
||||
@@ -667,6 +722,8 @@ export type User = Actor & {
|
||||
hasPendingUserInvitations: Scalars['Boolean'];
|
||||
/** Pending UserInvitations for this user. Only resolves for the viewer. */
|
||||
pendingUserInvitations: Array<UserInvitation>;
|
||||
/** Coalesced project activity for all apps belonging to all accounts this user belongs to. Only resolves for the viewer. */
|
||||
activityTimelineProjectActivities: Array<ActivityTimelineProjectActivity>;
|
||||
/**
|
||||
* Server feature gate values for this actor, optionally filtering by desired gates.
|
||||
* Only resolves for the viewer.
|
||||
@@ -704,6 +761,14 @@ export type UserAppsArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** Represents a human (not robot) actor. */
|
||||
export type UserActivityTimelineProjectActivitiesArgs = {
|
||||
limit: Scalars['Int'];
|
||||
createdBefore?: Maybe<Scalars['DateTime']>;
|
||||
filterTypes?: Maybe<Array<ActivityTimelineProjectActivityType>>;
|
||||
};
|
||||
|
||||
|
||||
/** Represents a human (not robot) actor. */
|
||||
export type UserFeatureGatesArgs = {
|
||||
filter?: Maybe<Array<Scalars['String']>>;
|
||||
@@ -781,6 +846,13 @@ export enum Role {
|
||||
NotAdmin = 'NOT_ADMIN'
|
||||
}
|
||||
|
||||
export enum ActivityTimelineProjectActivityType {
|
||||
BuildJob = 'BUILD_JOB',
|
||||
Build = 'BUILD',
|
||||
Update = 'UPDATE',
|
||||
Submission = 'SUBMISSION'
|
||||
}
|
||||
|
||||
|
||||
export type BuildArtifacts = {
|
||||
__typename?: 'BuildArtifacts';
|
||||
@@ -794,10 +866,9 @@ export type BuildMetrics = {
|
||||
buildDuration?: Maybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
export enum DistributionType {
|
||||
Store = 'STORE',
|
||||
Internal = 'INTERNAL',
|
||||
Simulator = 'SIMULATOR'
|
||||
export enum BuildIosEnterpriseProvisioning {
|
||||
Adhoc = 'ADHOC',
|
||||
Universal = 'UNIVERSAL'
|
||||
}
|
||||
|
||||
export type BuildError = {
|
||||
@@ -939,6 +1010,39 @@ export type SubmissionFilter = {
|
||||
status?: Maybe<SubmissionStatus>;
|
||||
};
|
||||
|
||||
/** 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 type UpdateBranch = {
|
||||
__typename?: 'UpdateBranch';
|
||||
id: Scalars['ID'];
|
||||
appId: Scalars['ID'];
|
||||
name: Scalars['String'];
|
||||
createdAt: Scalars['DateTime'];
|
||||
updatedAt: Scalars['DateTime'];
|
||||
updates: Array<Update>;
|
||||
mostRecentUpdateForEachPlatform: Array<Update>;
|
||||
};
|
||||
|
||||
|
||||
export type UpdateBranchUpdatesArgs = {
|
||||
offset: Scalars['Int'];
|
||||
limit: Scalars['Int'];
|
||||
filter?: Maybe<UpdatesFilter>;
|
||||
};
|
||||
|
||||
export type UpdatesFilter = {
|
||||
platform?: Maybe<AppPlatform>;
|
||||
runtimeVersions?: Maybe<Array<Scalars['String']>>;
|
||||
};
|
||||
|
||||
export type IosAppCredentialsFilter = {
|
||||
appleAppIdentifierId?: Maybe<Scalars['String']>;
|
||||
};
|
||||
@@ -1194,28 +1298,6 @@ export type UpdateChannelUpdateBranchesArgs = {
|
||||
limit: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type UpdateBranch = {
|
||||
__typename?: 'UpdateBranch';
|
||||
id: Scalars['ID'];
|
||||
appId: Scalars['ID'];
|
||||
name: Scalars['String'];
|
||||
createdAt: Scalars['DateTime'];
|
||||
updatedAt: Scalars['DateTime'];
|
||||
updates: Array<Update>;
|
||||
};
|
||||
|
||||
|
||||
export type UpdateBranchUpdatesArgs = {
|
||||
offset: Scalars['Int'];
|
||||
limit: Scalars['Int'];
|
||||
filter?: Maybe<UpdatesFilter>;
|
||||
};
|
||||
|
||||
export type UpdatesFilter = {
|
||||
platform?: Maybe<AppPlatform>;
|
||||
runtimeVersions?: Maybe<Array<Scalars['String']>>;
|
||||
};
|
||||
|
||||
export type EnvironmentSecret = {
|
||||
__typename?: 'EnvironmentSecret';
|
||||
id: Scalars['ID'];
|
||||
@@ -1496,6 +1578,12 @@ export type BuildQuery = {
|
||||
* @deprecated Use App.builds instead
|
||||
*/
|
||||
allForApp: Array<Maybe<Build>>;
|
||||
/**
|
||||
* Get all builds.
|
||||
* By default, they are sorted from latest to oldest.
|
||||
* Available only for admin users.
|
||||
*/
|
||||
all: Array<Build>;
|
||||
};
|
||||
|
||||
|
||||
@@ -1512,6 +1600,19 @@ export type BuildQueryAllForAppArgs = {
|
||||
limit?: Maybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
|
||||
export type BuildQueryAllArgs = {
|
||||
statuses?: Maybe<Array<BuildStatus>>;
|
||||
offset?: Maybe<Scalars['Int']>;
|
||||
limit?: Maybe<Scalars['Int']>;
|
||||
order?: Maybe<Order>;
|
||||
};
|
||||
|
||||
export enum Order {
|
||||
Desc = 'DESC',
|
||||
Asc = 'ASC'
|
||||
}
|
||||
|
||||
export type ClientBuildQuery = {
|
||||
__typename?: 'ClientBuildQuery';
|
||||
byId: ClientBuild;
|
||||
@@ -1533,6 +1634,24 @@ export type ClientBuild = {
|
||||
userId?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type EasBuildQuery = {
|
||||
__typename?: 'EasBuildQuery';
|
||||
/** Get EAS Build kill switches state */
|
||||
killSwitches: Array<EasBuildKillSwitch>;
|
||||
};
|
||||
|
||||
export type EasBuildKillSwitch = {
|
||||
__typename?: 'EasBuildKillSwitch';
|
||||
name: EasBuildKillSwitchName;
|
||||
value: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export enum EasBuildKillSwitchName {
|
||||
StopAcceptingBuilds = 'STOP_ACCEPTING_BUILDS',
|
||||
StopSchedulingBuilds = 'STOP_SCHEDULING_BUILDS',
|
||||
StopAcceptingNormalPriorityBuilds = 'STOP_ACCEPTING_NORMAL_PRIORITY_BUILDS'
|
||||
}
|
||||
|
||||
export type ExperimentationQuery = {
|
||||
__typename?: 'ExperimentationQuery';
|
||||
/** Get user experimentation config */
|
||||
@@ -1712,6 +1831,7 @@ export type RootMutation = {
|
||||
userInvitation: UserInvitationMutation;
|
||||
/** Mutations that modify the currently authenticated User */
|
||||
me?: Maybe<MeMutation>;
|
||||
easBuildKillSwitch: EasBuildKillSwitchMutation;
|
||||
/** Mutations that modify an EmailSubscription */
|
||||
emailSubscription: EmailSubscriptionMutation;
|
||||
/** Mutations that create and delete EnvironmentSecrets */
|
||||
@@ -2466,6 +2586,7 @@ export type BuildMetadataInput = {
|
||||
releaseChannel?: Maybe<Scalars['String']>;
|
||||
channel?: Maybe<Scalars['String']>;
|
||||
distribution?: Maybe<DistributionType>;
|
||||
iosEnterpriseProvisioning?: Maybe<BuildIosEnterpriseProvisioning>;
|
||||
appName?: Maybe<Scalars['String']>;
|
||||
appIdentifier?: Maybe<Scalars['String']>;
|
||||
buildProfile?: Maybe<Scalars['String']>;
|
||||
@@ -2872,7 +2993,7 @@ export type UpdateBranchMutation = {
|
||||
/** Delete an EAS branch and all of its updates as long as the branch is not being used by any channels */
|
||||
deleteUpdateBranch: DeleteUpdateBranchResult;
|
||||
/** Publish an update group to a branch */
|
||||
publishUpdateGroup: Array<Maybe<Update>>;
|
||||
publishUpdateGroup: Array<Update>;
|
||||
};
|
||||
|
||||
|
||||
@@ -2924,6 +3045,7 @@ export type UpdateInfoGroup = {
|
||||
export type PartialManifest = {
|
||||
launchAsset: PartialManifestAsset;
|
||||
assets: Array<Maybe<PartialManifestAsset>>;
|
||||
extra?: Maybe<Scalars['JSONObject']>;
|
||||
};
|
||||
|
||||
export type PartialManifestAsset = {
|
||||
@@ -3207,6 +3329,20 @@ export type SecondFactorRegenerateBackupCodesResult = {
|
||||
plaintextBackupCodes: Array<Maybe<Scalars['String']>>;
|
||||
};
|
||||
|
||||
export type EasBuildKillSwitchMutation = {
|
||||
__typename?: 'EasBuildKillSwitchMutation';
|
||||
/** Set an EAS Build kill switch to a given value */
|
||||
set: EasBuildKillSwitch;
|
||||
/** Reset all EAS Build kill switches (set them to false) */
|
||||
resetAll: Array<EasBuildKillSwitch>;
|
||||
};
|
||||
|
||||
|
||||
export type EasBuildKillSwitchMutationSetArgs = {
|
||||
name: EasBuildKillSwitchName;
|
||||
value: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type EmailSubscriptionMutation = {
|
||||
__typename?: 'EmailSubscriptionMutation';
|
||||
addUser?: Maybe<AddUserPayload>;
|
||||
@@ -4531,6 +4667,7 @@ export type CreateAndroidBuildMutation = (
|
||||
& { build: (
|
||||
{ __typename?: 'Build' }
|
||||
& Pick<Build, 'id'>
|
||||
& BuildFragment
|
||||
), deprecationInfo?: Maybe<(
|
||||
{ __typename?: 'EASBuildDeprecationInfo' }
|
||||
& Pick<EasBuildDeprecationInfo, 'type' | 'message'>
|
||||
@@ -4555,6 +4692,7 @@ export type CreateIosBuildMutation = (
|
||||
& { build: (
|
||||
{ __typename?: 'Build' }
|
||||
& Pick<Build, 'id'>
|
||||
& BuildFragment
|
||||
), deprecationInfo?: Maybe<(
|
||||
{ __typename?: 'EASBuildDeprecationInfo' }
|
||||
& Pick<EasBuildDeprecationInfo, 'type' | 'message'>
|
||||
@@ -4640,10 +4778,10 @@ export type UpdatePublishMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { updateBranch: (
|
||||
{ __typename?: 'UpdateBranchMutation' }
|
||||
& { publishUpdateGroup: Array<Maybe<(
|
||||
& { publishUpdateGroup: Array<(
|
||||
{ __typename?: 'Update' }
|
||||
& Pick<Update, 'id' | 'group'>
|
||||
)>> }
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
@@ -4651,6 +4789,7 @@ export type CreateSubmissionMutationVariables = Exact<{
|
||||
appId: Scalars['ID'];
|
||||
platform: AppPlatform;
|
||||
config: Scalars['JSONObject'];
|
||||
submittedBuildId?: Maybe<Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -4754,8 +4893,7 @@ export type GetAllBuildsForAppQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
offset: Scalars['Int'];
|
||||
limit: Scalars['Int'];
|
||||
status?: Maybe<BuildStatus>;
|
||||
platform?: Maybe<AppPlatform>;
|
||||
filter?: Maybe<BuildFilter>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -4796,16 +4934,16 @@ export type EnvironmentSecretsByAccountNameQuery = (
|
||||
) }
|
||||
);
|
||||
|
||||
export type EnvironmentSecretsByAppFullNameQueryVariables = Exact<{
|
||||
fullName: Scalars['String'];
|
||||
export type EnvironmentSecretsByAppIdQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type EnvironmentSecretsByAppFullNameQuery = (
|
||||
export type EnvironmentSecretsByAppIdQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byFullName: (
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
& Pick<App, 'id'>
|
||||
& { environmentSecrets: Array<(
|
||||
@@ -4963,7 +5101,7 @@ export type AppFragment = (
|
||||
|
||||
export type BuildFragment = (
|
||||
{ __typename?: 'Build' }
|
||||
& Pick<Build, 'id' | 'status' | 'platform' | 'releaseChannel' | 'distribution' | 'createdAt' | 'updatedAt'>
|
||||
& Pick<Build, 'id' | 'status' | 'platform' | 'channel' | 'releaseChannel' | 'distribution' | 'iosEnterpriseProvisioning' | 'buildProfile' | 'sdkVersion' | 'appVersion' | 'appBuildVersion' | 'runtimeVersion' | 'gitCommitHash' | 'createdAt' | 'updatedAt'>
|
||||
& { error?: Maybe<(
|
||||
{ __typename?: 'BuildError' }
|
||||
& Pick<BuildError, 'errorCode' | 'message' | 'docsUrl'>
|
||||
@@ -4978,10 +5116,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
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ interface CreateSubmissionResult {
|
||||
export const SubmissionMutation = {
|
||||
async createSubmissionAsync(input: {
|
||||
appId: string;
|
||||
submittedBuildId?: string;
|
||||
platform: AppPlatform;
|
||||
config: JSONObject;
|
||||
}): Promise<CreateSubmissionResult> {
|
||||
@@ -28,9 +29,17 @@ export const SubmissionMutation = {
|
||||
$appId: ID!
|
||||
$platform: AppPlatform!
|
||||
$config: JSONObject!
|
||||
$submittedBuildId: ID
|
||||
) {
|
||||
submission {
|
||||
createSubmission(input: { appId: $appId, platform: $platform, config: $config }) {
|
||||
createSubmission(
|
||||
input: {
|
||||
appId: $appId
|
||||
platform: $platform
|
||||
config: $config
|
||||
submittedBuildId: $submittedBuildId
|
||||
}
|
||||
) {
|
||||
submission {
|
||||
id
|
||||
}
|
||||
|
||||
@@ -8,16 +8,28 @@ import {
|
||||
BuildStatus,
|
||||
BuildsByIdQuery,
|
||||
BuildsByIdQueryVariables,
|
||||
DistributionType,
|
||||
GetAllBuildsForAppQuery,
|
||||
GetAllBuildsForAppQueryVariables,
|
||||
} from '../generated';
|
||||
import { BuildFragmentNode } from '../types/Build';
|
||||
|
||||
type Filters = {
|
||||
platform?: AppPlatform;
|
||||
status?: BuildStatus;
|
||||
type BuildsQuery = {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
filter?: {
|
||||
platform?: AppPlatform;
|
||||
status?: BuildStatus;
|
||||
distribution?: DistributionType;
|
||||
channel?: string;
|
||||
appVersion?: string;
|
||||
appBuildVersion?: string;
|
||||
sdkVersion?: string;
|
||||
runtimeVersion?: string;
|
||||
appIdentifier?: string;
|
||||
buildProfile?: string;
|
||||
gitCommitHash?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const BuildQuery = {
|
||||
@@ -52,7 +64,7 @@ export const BuildQuery = {
|
||||
|
||||
async allForAppAsync(
|
||||
appId: string,
|
||||
{ limit = 10, offset = 0, status, platform }: Filters
|
||||
{ limit = 10, offset = 0, filter }: BuildsQuery
|
||||
): Promise<BuildFragment[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
@@ -63,13 +75,12 @@ export const BuildQuery = {
|
||||
$appId: String!
|
||||
$offset: Int!
|
||||
$limit: Int!
|
||||
$status: BuildStatus
|
||||
$platform: AppPlatform
|
||||
$filter: BuildFilter
|
||||
) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
builds(offset: $offset, limit: $limit, status: $status, platform: $platform) {
|
||||
builds(offset: $offset, limit: $limit, filter: $filter) {
|
||||
id
|
||||
...BuildFragment
|
||||
}
|
||||
@@ -78,7 +89,7 @@ export const BuildQuery = {
|
||||
}
|
||||
${print(BuildFragmentNode)}
|
||||
`,
|
||||
{ appId, offset, limit, status, platform }
|
||||
{ appId, offset, limit, filter }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { EnvironmentSecretScope } from '../../commands/secret/create';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
EnvironmentSecretFragment,
|
||||
EnvironmentSecretsByAccountNameQuery,
|
||||
EnvironmentSecretsByAppFullNameQuery,
|
||||
EnvironmentSecretsByAppIdQuery,
|
||||
} from '../generated';
|
||||
import { EnvironmentSecretFragmentNode } from '../types/EnvironmentSecret';
|
||||
|
||||
export enum EnvironmentSecretScope {
|
||||
ACCOUNT = 'account',
|
||||
PROJECT = 'project',
|
||||
}
|
||||
|
||||
export type EnvironmentSecretWithScope = EnvironmentSecretFragment & {
|
||||
scope: EnvironmentSecretScope;
|
||||
};
|
||||
@@ -40,14 +44,14 @@ export const EnvironmentSecretsQuery = {
|
||||
|
||||
return data.account.byName.environmentSecrets;
|
||||
},
|
||||
async byAppFullNameAsync(fullName: string): Promise<EnvironmentSecretFragment[]> {
|
||||
async byAppIdAsync(appId: string): Promise<EnvironmentSecretFragment[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<EnvironmentSecretsByAppFullNameQuery>(
|
||||
.query<EnvironmentSecretsByAppIdQuery>(
|
||||
gql`
|
||||
query EnvironmentSecretsByAppFullName($fullName: String!) {
|
||||
query EnvironmentSecretsByAppId($appId: String!) {
|
||||
app {
|
||||
byFullName(fullName: $fullName) {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
environmentSecrets {
|
||||
id
|
||||
@@ -58,12 +62,12 @@ export const EnvironmentSecretsQuery = {
|
||||
}
|
||||
${print(EnvironmentSecretFragmentNode)}
|
||||
`,
|
||||
{ fullName }
|
||||
{ appId }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.app?.byFullName.environmentSecrets ?? [];
|
||||
return data.app?.byId.environmentSecrets ?? [];
|
||||
},
|
||||
async allAsync(
|
||||
projectAccountName: string,
|
||||
@@ -71,7 +75,7 @@ export const EnvironmentSecretsQuery = {
|
||||
): Promise<EnvironmentSecretWithScope[]> {
|
||||
const [accountSecrets, appSecrets] = await Promise.all([
|
||||
this.byAcccountNameAsync(projectAccountName),
|
||||
this.byAppFullNameAsync(projectFullName),
|
||||
this.byAppIdAsync(projectFullName),
|
||||
]);
|
||||
|
||||
return [
|
||||
|
||||
@@ -27,6 +27,7 @@ export const BuildFragmentNode = gql`
|
||||
project {
|
||||
__typename
|
||||
id
|
||||
name
|
||||
... on App {
|
||||
ownerAccount {
|
||||
id
|
||||
@@ -34,8 +35,16 @@ export const BuildFragmentNode = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
channel
|
||||
releaseChannel
|
||||
distribution
|
||||
iosEnterpriseProvisioning
|
||||
buildProfile
|
||||
sdkVersion
|
||||
appVersion
|
||||
appBuildVersion
|
||||
runtimeVersion
|
||||
gitCommitHash
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
|
||||
@@ -34,12 +34,16 @@ class CustomCommandHelp extends CommandHelp {
|
||||
this.aliases(cmd.aliases),
|
||||
this.examples(cmd.examples || (cmd as any).example),
|
||||
]).join('\n\n');
|
||||
if (this.opts.stripAnsi) output = stripAnsi(output);
|
||||
if (this.opts.stripAnsi) {
|
||||
output = stripAnsi(output);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
protected flags(flags: Config.Command.Flag[]): string | undefined {
|
||||
if (flags.length === 0) return;
|
||||
if (flags.length === 0) {
|
||||
return;
|
||||
}
|
||||
const groupableFlags = flags.map(originalFlag => {
|
||||
const { helpLabel, ...flag } = originalFlag;
|
||||
// We re-purpose `helpLabel` to mean a "header to group the flag under".
|
||||
|
||||
@@ -155,22 +155,28 @@ describe(buildUpdateInfoGroupAsync, () => {
|
||||
|
||||
it('returns the correct value', async () => {
|
||||
await expect(
|
||||
buildUpdateInfoGroupAsync({
|
||||
android: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
contentType: 'bundle/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
assets: [
|
||||
{
|
||||
type: 'jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: assetPath,
|
||||
buildUpdateInfoGroupAsync(
|
||||
{
|
||||
android: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
contentType: 'bundle/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
],
|
||||
assets: [
|
||||
{
|
||||
type: 'jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: assetPath,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
{
|
||||
slug: 'hello',
|
||||
name: 'hello',
|
||||
}
|
||||
)
|
||||
).resolves.toEqual({
|
||||
android: {
|
||||
assets: [
|
||||
@@ -182,7 +188,6 @@ describe(buildUpdateInfoGroupAsync, () => {
|
||||
storageKey: 'fo8Y08LktVk6qLtGbn8GRWpOUyD13ABMUnbtRCN1L7Y',
|
||||
},
|
||||
],
|
||||
|
||||
launchAsset: {
|
||||
bundleKey: 'ec0dd14670aae108f99a810df9c1482c',
|
||||
contentType: 'bundle/javascript',
|
||||
@@ -190,6 +195,12 @@ describe(buildUpdateInfoGroupAsync, () => {
|
||||
storageBucket: 'update-assets-production',
|
||||
storageKey: 'aC9N6RZlcHoIYjIsoJd2KUcigBKy98RHvZacDyPNjCQ',
|
||||
},
|
||||
extra: {
|
||||
expoClient: {
|
||||
slug: 'hello',
|
||||
name: 'hello',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -298,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 () => {
|
||||
@@ -312,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,24 @@
|
||||
import { ExpoConfig, getConfig } from '@expo/config';
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
|
||||
interface Options {
|
||||
env?: Env;
|
||||
isPublicConfig?: boolean;
|
||||
}
|
||||
|
||||
export function getExpoConfig(projectDir: string, opts: Options = {}): ExpoConfig {
|
||||
const originalProcessEnv: NodeJS.ProcessEnv = process.env;
|
||||
try {
|
||||
process.env = {
|
||||
...process.env,
|
||||
...opts.env,
|
||||
};
|
||||
const { exp } = getConfig(projectDir, {
|
||||
skipSDKVersionRequirement: true,
|
||||
...(opts.isPublicConfig ? { isPublicConfig: true } : {}),
|
||||
});
|
||||
return exp;
|
||||
} finally {
|
||||
process.env = originalProcessEnv;
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export async function resolveXcodeBuildContextAsync(
|
||||
return {
|
||||
buildScheme,
|
||||
buildConfiguration:
|
||||
buildProfile.schemeBuildConfiguration ??
|
||||
buildProfile.buildConfiguration ??
|
||||
(await IOSConfig.BuildScheme.getArchiveBuildConfigurationForSchemeAsync(
|
||||
projectDir,
|
||||
buildScheme
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -21,12 +21,14 @@ export async function resolveTargetsAsync(
|
||||
result.push({
|
||||
targetName: applicationTarget.name,
|
||||
bundleIdentifier,
|
||||
buildConfiguration,
|
||||
});
|
||||
|
||||
if (applicationTarget.dependencies && applicationTarget.dependencies.length > 0) {
|
||||
for (const dependency of applicationTarget.dependencies) {
|
||||
result.push({
|
||||
targetName: dependency.name,
|
||||
buildConfiguration,
|
||||
bundleIdentifier: await getBundleIdentifierAsync(projectDir, exp, {
|
||||
targetName: dependency.name,
|
||||
buildConfiguration,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { exit } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
import type MetroConfig from 'metro-config';
|
||||
import resolveFrom from 'resolve-from';
|
||||
|
||||
import { BuildContext } from '../build/context';
|
||||
import { Platform } from '../build/types';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { confirmAsync } from '../prompts';
|
||||
|
||||
export async function validateMetroConfigForManagedWorkflowAsync(ctx: BuildContext<Platform>) {
|
||||
if (!(await configExistsAsync(ctx.projectDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metroConfig = await loadConfigAsync(ctx.projectDir);
|
||||
const hasHashAssetFilesPlugin = metroConfig.transformer?.assetPlugins?.find((plugin: string) =>
|
||||
plugin.match(/expo-asset\/tools\/hashAssetFiles/)
|
||||
);
|
||||
if (!hasHashAssetFilesPlugin) {
|
||||
Log.warn(
|
||||
`It looks like that you are using a custom ${chalk.bold(
|
||||
'metro.config.js'
|
||||
)} that does not extend ${chalk.bold('@expo/metro-config')}.`
|
||||
);
|
||||
Log.warn(
|
||||
'This can result in unexpected and hard to debug issues, like missing assets in the production bundle.'
|
||||
);
|
||||
Log.warn(`We recommend you to abort, fix the ${chalk.bold('metro.config.js')}, and try again.`);
|
||||
Log.warn(
|
||||
learnMore('https://docs.expo.io/guides/customizing-metro/', {
|
||||
learnMoreMessage: 'Learn more on customizing Metro',
|
||||
})
|
||||
);
|
||||
|
||||
const shouldAbort = await confirmAsync({
|
||||
message: 'Would you like to abort?',
|
||||
});
|
||||
if (shouldAbort) {
|
||||
Log.log('Aborting...');
|
||||
exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function importMetroConfigFromProject(projectDir: string): typeof MetroConfig {
|
||||
const resolvedPath = resolveFrom.silent(projectDir, 'metro-config');
|
||||
if (!resolvedPath) {
|
||||
throw new MetroConfigPackageMissingError(
|
||||
'Missing package "metro-config" in the project. ' +
|
||||
'This usually means `react-native` is not installed. ' +
|
||||
'Please verify that dependencies in package.json include "react-native" ' +
|
||||
'and run `yarn` or `npm install`.'
|
||||
);
|
||||
}
|
||||
return require(resolvedPath);
|
||||
}
|
||||
|
||||
async function configExistsAsync(projectRoot: string): Promise<boolean> {
|
||||
try {
|
||||
const MetroConfig = importMetroConfigFromProject(projectRoot);
|
||||
const result = await MetroConfig.resolveConfig(undefined, projectRoot);
|
||||
return !result.isEmpty;
|
||||
} catch (err) {
|
||||
if (err instanceof MetroConfigPackageMissingError) {
|
||||
return false;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadConfigAsync(projectDir: string): Promise<MetroConfig.ConfigT> {
|
||||
const MetroConfig = importMetroConfigFromProject(projectDir);
|
||||
return await MetroConfig.loadConfig({ cwd: projectDir }, {});
|
||||
}
|
||||
|
||||
class MetroConfigPackageMissingError extends Error {}
|
||||
@@ -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);
|
||||
|
||||
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) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Platform } from '@expo/config';
|
||||
import { ExpoConfig, Platform } from '@expo/config';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import Joi from '@hapi/joi';
|
||||
@@ -46,9 +46,14 @@ type CollectedAssets = {
|
||||
};
|
||||
};
|
||||
|
||||
type ManifestExtra = {
|
||||
expoClient?: { [key: string]: any };
|
||||
[key: string]: any;
|
||||
};
|
||||
type ManifestFragment = {
|
||||
launchAsset: PartialManifestAsset;
|
||||
assets: PartialManifestAsset[];
|
||||
extra?: ManifestExtra;
|
||||
};
|
||||
type UpdateInfoGroup = {
|
||||
[key in PublishPlatform]: ManifestFragment;
|
||||
@@ -125,7 +130,10 @@ export async function convertAssetToUpdateInfoGroupFormatAsync(
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildUpdateInfoGroupAsync(assets: CollectedAssets): Promise<UpdateInfoGroup> {
|
||||
export async function buildUpdateInfoGroupAsync(
|
||||
assets: CollectedAssets,
|
||||
exp: ExpoConfig
|
||||
): Promise<UpdateInfoGroup> {
|
||||
let platform: PublishPlatform;
|
||||
const updateInfoGroup: Partial<UpdateInfoGroup> = {};
|
||||
for (platform in assets) {
|
||||
@@ -134,6 +142,9 @@ export async function buildUpdateInfoGroupAsync(assets: CollectedAssets): Promis
|
||||
assets: await Promise.all(
|
||||
(assets[platform]?.assets ?? []).map(convertAssetToUpdateInfoGroupFormatAsync)
|
||||
),
|
||||
extra: {
|
||||
expoClient: exp,
|
||||
},
|
||||
};
|
||||
}
|
||||
return updateInfoGroup as UpdateInfoGroup;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ abstract class BaseSubmitter<SubmissionContext, SubmissionOptions> {
|
||||
|
||||
protected async startSubmissionAsync(
|
||||
submissionConfig: SubmissionConfig,
|
||||
buildId?: string,
|
||||
verbose: boolean = false
|
||||
): Promise<SubmissionStatus> {
|
||||
Log.addNewLineIfNone();
|
||||
@@ -31,7 +32,8 @@ abstract class BaseSubmitter<SubmissionContext, SubmissionOptions> {
|
||||
submissionId = await SubmissionService.startSubmissionAsync(
|
||||
this.platform,
|
||||
submissionConfig.projectId,
|
||||
submissionConfig
|
||||
submissionConfig,
|
||||
buildId
|
||||
);
|
||||
scheduleSpinner.succeed();
|
||||
} catch (err) {
|
||||
|
||||
@@ -20,12 +20,14 @@ export const DEFAULT_CHECK_INTERVAL_MS = 5 * 1000; // 5 secs
|
||||
async function startSubmissionAsync(
|
||||
platform: AppPlatform,
|
||||
projectId: string,
|
||||
config: SubmissionConfig
|
||||
config: SubmissionConfig,
|
||||
buildId?: string
|
||||
): Promise<StartSubmissionResult> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,11 @@ class AndroidSubmitter extends BaseSubmitter<AndroidSubmissionContext, AndroidSu
|
||||
SummaryHumanReadableValues
|
||||
);
|
||||
|
||||
await this.startSubmissionAsync(submissionConfig, this.ctx.commandFlags.verbose);
|
||||
await this.startSubmissionAsync(
|
||||
submissionConfig,
|
||||
resolvedSourceOptions.archive.build?.id,
|
||||
this.ctx.commandFlags.verbose
|
||||
);
|
||||
}
|
||||
|
||||
private async resolveSourceOptions(): Promise<ResolvedSourceOptions> {
|
||||
@@ -88,8 +92,8 @@ class AndroidSubmitter extends BaseSubmitter<AndroidSubmissionContext, AndroidSu
|
||||
track,
|
||||
releaseStatus,
|
||||
archiveType: archive.type as AndroidArchiveType,
|
||||
...formatArchiveSourceSummary(archive),
|
||||
serviceAccountPath,
|
||||
...formatArchiveSourceSummary(archive),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -108,11 +112,11 @@ const SummaryHumanReadableKeys: Record<keyof Summary, string> = {
|
||||
archivePath: 'Archive path',
|
||||
archiveUrl: 'Download URL',
|
||||
archiveType: 'Archive type',
|
||||
buildId: 'Build ID',
|
||||
serviceAccountPath: 'Google Service Key',
|
||||
track: 'Release track',
|
||||
releaseStatus: 'Release status',
|
||||
projectId: 'Project ID',
|
||||
formattedBuild: 'Build',
|
||||
};
|
||||
|
||||
const SummaryHumanReadableValues: Partial<Record<keyof Summary, Function>> = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,17 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { asMock } from '../../../__tests__/utils';
|
||||
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { AppPlatform, SubmissionFragment, SubmissionStatus } from '../../../graphql/generated';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
SubmissionFragment,
|
||||
SubmissionStatus,
|
||||
} from '../../../graphql/generated';
|
||||
import { createTestProject } from '../../../project/__tests__/project-utils';
|
||||
import { getProjectIdAsync } from '../../../project/projectUtils';
|
||||
import SubmissionService from '../../SubmissionService';
|
||||
import { AndroidArchiveType, AndroidSubmitCommandFlags } from '../../types';
|
||||
import { getLatestBuildForSubmissionAsync } from '../../utils/builds';
|
||||
import { AndroidSubmissionConfig, ReleaseStatus, ReleaseTrack } from '../AndroidSubmissionConfig';
|
||||
import AndroidSubmitCommand from '../AndroidSubmitCommand';
|
||||
|
||||
@@ -15,6 +21,7 @@ jest.mock('fs');
|
||||
jest.mock('ora');
|
||||
jest.mock('../../SubmissionService');
|
||||
jest.mock('../../../project/ensureProjectExists');
|
||||
jest.mock('../../utils/builds');
|
||||
jest.mock('../../../user/User', () => ({
|
||||
getUserAsync: jest.fn(() => mockJester),
|
||||
}));
|
||||
@@ -35,6 +42,13 @@ describe(AndroidSubmitCommand, () => {
|
||||
'/google-service-account.json': JSON.stringify({ service: 'account' }),
|
||||
};
|
||||
|
||||
const fakeBuildFragment: Partial<BuildFragment> = {
|
||||
id: uuidv4(),
|
||||
artifacts: { buildUrl: 'http://expo.io/fake.apk' },
|
||||
appVersion: '1.2.3',
|
||||
platform: AppPlatform.Android,
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
vol.fromJSON({
|
||||
...testProject.projectTree,
|
||||
@@ -111,7 +125,52 @@ describe(AndroidSubmitCommand, () => {
|
||||
expect(SubmissionService.startSubmissionAsync).toHaveBeenCalledWith(
|
||||
AppPlatform.Android,
|
||||
projectId,
|
||||
androidSubmissionConfig
|
||||
androidSubmissionConfig,
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it('assigns the build ID to submission', async () => {
|
||||
const projectId = uuidv4();
|
||||
asMock(SubmissionService.getSubmissionAsync).mockImplementationOnce(
|
||||
async (submissionId: string): Promise<SubmissionFragment> => {
|
||||
const actualSubmission = await originalGetSubmissionAsync(submissionId);
|
||||
return {
|
||||
...actualSubmission,
|
||||
status: SubmissionStatus.Finished,
|
||||
};
|
||||
}
|
||||
);
|
||||
asMock(getProjectIdAsync).mockImplementationOnce(() => projectId);
|
||||
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(fakeBuildFragment);
|
||||
|
||||
const options: AndroidSubmitCommandFlags = {
|
||||
latest: true,
|
||||
type: 'apk',
|
||||
key: '/google-service-account.json',
|
||||
track: 'internal',
|
||||
releaseStatus: 'draft',
|
||||
verbose: false,
|
||||
};
|
||||
const ctx = AndroidSubmitCommand.createContext(testProject.projectRoot, projectId, options);
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
|
||||
const androidSubmissionConfig: AndroidSubmissionConfig = {
|
||||
archiveUrl: 'http://expo.io/fake.apk',
|
||||
archiveType: AndroidArchiveType.apk,
|
||||
androidPackage: testProject.appJSON.expo.android?.package,
|
||||
serviceAccount: fakeFiles['/google-service-account.json'],
|
||||
releaseStatus: ReleaseStatus.draft,
|
||||
track: ReleaseTrack.internal,
|
||||
projectId,
|
||||
};
|
||||
|
||||
expect(SubmissionService.startSubmissionAsync).toHaveBeenCalledWith(
|
||||
AppPlatform.Android,
|
||||
projectId,
|
||||
androidSubmissionConfig,
|
||||
fakeBuildFragment.id
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,10 +2,10 @@ import chalk from 'chalk';
|
||||
import { URL, parse as parseUrl } from 'url';
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import { AppPlatform, BuildFragment } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { getBuildArtifactUrlByIdAsync, getLatestBuildArtifactUrlAsync } from '../utils/builds';
|
||||
import { getBuildByIdForSubmissionAsync, getLatestBuildForSubmissionAsync } from '../utils/builds';
|
||||
import { isExistingFile, uploadAppArchiveAsync } from '../utils/files';
|
||||
|
||||
export enum ArchiveFileSourceType {
|
||||
@@ -49,6 +49,7 @@ interface ArchiveFilePromptSource extends ArchiveFileSourceBase {
|
||||
export interface ResolvedArchive {
|
||||
location: string;
|
||||
realSource: ArchiveFileSource;
|
||||
build?: BuildFragment;
|
||||
}
|
||||
|
||||
export type ArchiveFileSource =
|
||||
@@ -96,9 +97,9 @@ async function handleUrlSourceAsync(source: ArchiveFileUrlSource): Promise<Resol
|
||||
|
||||
async function handleLatestSourceAsync(source: ArchiveFileLatestSource): Promise<ResolvedArchive> {
|
||||
try {
|
||||
const artifactUrl = await getLatestBuildArtifactUrlAsync(source.platform, source.projectId);
|
||||
const latestBuild = await getLatestBuildForSubmissionAsync(source.platform, source.projectId);
|
||||
|
||||
if (!artifactUrl) {
|
||||
if (!latestBuild) {
|
||||
Log.error(
|
||||
chalk.bold(
|
||||
"Couldn't find any builds for this project on EAS servers. It looks like you haven't run 'eas build' yet."
|
||||
@@ -111,8 +112,9 @@ async function handleLatestSourceAsync(source: ArchiveFileLatestSource): Promise
|
||||
}
|
||||
|
||||
return {
|
||||
location: artifactUrl,
|
||||
location: latestBuild.artifacts.buildUrl,
|
||||
realSource: source,
|
||||
build: latestBuild,
|
||||
};
|
||||
} catch (err) {
|
||||
Log.error(err);
|
||||
@@ -141,9 +143,11 @@ async function handleBuildIdSourceAsync(
|
||||
source: ArchiveFileBuildIdSource
|
||||
): Promise<ResolvedArchive> {
|
||||
try {
|
||||
const build = await getBuildByIdForSubmissionAsync(source.platform, source.id);
|
||||
return {
|
||||
location: await getBuildArtifactUrlByIdAsync(source.platform, source.id),
|
||||
location: build.artifacts.buildUrl,
|
||||
realSource: source,
|
||||
build,
|
||||
};
|
||||
} catch (err) {
|
||||
Log.error(chalk.bold(`Couldn't find build for id ${source.id}`));
|
||||
|
||||
@@ -3,10 +3,13 @@ import { vol } from 'memfs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { asMock } from '../../../__tests__/utils';
|
||||
import { AppPlatform, UploadSessionType } from '../../../graphql/generated';
|
||||
import { AppPlatform, BuildFragment, UploadSessionType } from '../../../graphql/generated';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { uploadAsync } from '../../../uploads';
|
||||
import { getBuildArtifactUrlByIdAsync, getLatestBuildArtifactUrlAsync } from '../../utils/builds';
|
||||
import {
|
||||
getBuildByIdForSubmissionAsync,
|
||||
getLatestBuildForSubmissionAsync,
|
||||
} from '../../utils/builds';
|
||||
import {
|
||||
ArchiveFileSourceType,
|
||||
ResolvedArchive,
|
||||
@@ -21,6 +24,15 @@ jest.mock('../../../uploads');
|
||||
|
||||
const ARCHIVE_URL = 'https://url.to/archive.tar.gz';
|
||||
|
||||
const MOCK_BUILD_FRAGMENT: Partial<BuildFragment> = {
|
||||
id: uuidv4(),
|
||||
artifacts: {
|
||||
buildUrl: ARCHIVE_URL,
|
||||
},
|
||||
appVersion: '1.2.3',
|
||||
platform: AppPlatform.Android,
|
||||
};
|
||||
|
||||
const SOURCE_STUB_INPUT = {
|
||||
projectId: uuidv4(),
|
||||
platform: AppPlatform.Android,
|
||||
@@ -77,7 +89,7 @@ describe(getArchiveFileLocationAsync, () => {
|
||||
|
||||
it('handles Build ID source', async () => {
|
||||
const buildId = uuidv4();
|
||||
asMock(getBuildArtifactUrlByIdAsync).mockResolvedValueOnce(ARCHIVE_URL);
|
||||
asMock(getBuildByIdForSubmissionAsync).mockResolvedValueOnce(MOCK_BUILD_FRAGMENT);
|
||||
|
||||
const resolvedArchive = await getArchiveFileLocationAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
@@ -85,12 +97,12 @@ describe(getArchiveFileLocationAsync, () => {
|
||||
id: buildId,
|
||||
});
|
||||
|
||||
expect(getBuildArtifactUrlByIdAsync).toBeCalledWith(SOURCE_STUB_INPUT.platform, buildId);
|
||||
expect(getBuildByIdForSubmissionAsync).toBeCalledWith(SOURCE_STUB_INPUT.platform, buildId);
|
||||
assertArchiveResult(resolvedArchive, ArchiveFileSourceType.buildId);
|
||||
});
|
||||
|
||||
it('prompts again if build with provided ID doesnt exist', async () => {
|
||||
asMock(getBuildArtifactUrlByIdAsync).mockRejectedValue(new Error('Build doesnt exist'));
|
||||
asMock(getBuildByIdForSubmissionAsync).mockRejectedValue(new Error('Build doesnt exist'));
|
||||
asMock(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveFileSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
@@ -106,7 +118,7 @@ describe(getArchiveFileLocationAsync, () => {
|
||||
|
||||
it('handles latest build source', async () => {
|
||||
const projectId = uuidv4();
|
||||
asMock(getLatestBuildArtifactUrlAsync).mockResolvedValueOnce(ARCHIVE_URL);
|
||||
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(MOCK_BUILD_FRAGMENT);
|
||||
|
||||
const resolvedArchive = await getArchiveFileLocationAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
@@ -114,12 +126,12 @@ describe(getArchiveFileLocationAsync, () => {
|
||||
sourceType: ArchiveFileSourceType.latest,
|
||||
});
|
||||
|
||||
expect(getLatestBuildArtifactUrlAsync).toBeCalledWith(SOURCE_STUB_INPUT.platform, projectId);
|
||||
expect(getLatestBuildForSubmissionAsync).toBeCalledWith(SOURCE_STUB_INPUT.platform, projectId);
|
||||
assertArchiveResult(resolvedArchive, ArchiveFileSourceType.latest);
|
||||
});
|
||||
|
||||
it('prompts again if no builds exists when selected latest', async () => {
|
||||
asMock(getLatestBuildArtifactUrlAsync).mockResolvedValueOnce(null);
|
||||
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(null);
|
||||
asMock(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveFileSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import { AppPlatform, BuildFragment } from '../../graphql/generated';
|
||||
import { ArchiveType } from '../types';
|
||||
import {
|
||||
ArchiveFileSource,
|
||||
@@ -16,18 +16,20 @@ export interface Archive {
|
||||
location: string;
|
||||
type: ArchiveType;
|
||||
realFileSource: ArchiveFileSource;
|
||||
build?: BuildFragment;
|
||||
}
|
||||
|
||||
export async function getArchiveAsync(
|
||||
platform: AppPlatform,
|
||||
source: ArchiveSource
|
||||
): Promise<Archive> {
|
||||
const { location, realSource } = await getArchiveFileLocationAsync(source.archiveFile);
|
||||
const { location, realSource, build } = await getArchiveFileLocationAsync(source.archiveFile);
|
||||
const type = await getArchiveTypeAsync(platform, source.archiveType, location);
|
||||
return {
|
||||
location,
|
||||
type,
|
||||
realFileSource: realSource,
|
||||
build,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Loaded 100 of 124 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user