Compare commits

...
22 Commits
Author SHA1 Message Date
Dominik Sokal 878028271a v0.22.0 2021-07-23 10:25:00 +02:00
Dominik Sokal d4e15bbcbf add iosEnterpriseProvisioning to build metadata (#522)
* generate graphql code

* add iosEnterpriseProvisioning to build metadata

* update CHANGELOG

* upgrade @expo/eas-build-job to 0.2.44
2021-07-23 10:09:19 +02:00
Evan Bacon 13bb056df1 Create and link capability identifiers automatically (#524)
* Create and link capability identifiers automatically

* changelog

* Update capabilityIdentifiers.ts

* Update capabilityIdentifiers.ts

* Added better assertion errors
2021-07-22 18:50:51 -06:00
Will Schurman 14d56904d2 [eas-cli] Upload exp config in extra when publishing or republishing (#514) 2021-07-22 10:30:23 -07:00
Dominik Sokal c2ffd05763 fall back to APK when building for internal distribution (#527)
* fall back to APK when building for internal distribution

* update CHANGELOG

* address PR feedback
2021-07-22 16:21:20 +02:00
Dominik Sokal 46e394fbef use default version code when it's not defined for managed project in app.json (#526)
* use default version code when it's not defined for managed project in app.json

* update CHANGELOG
2021-07-22 13:36:26 +02:00
Dominik Sokal 2613f0c2f9 enable lint rule for curly braces around blocks (#525) 2021-07-22 11:49:12 +02:00
Bartłomiej KlocekandWojciech Kozyra e5fd7c01f4 [eas submit] propagate build id to submission mutation (#518)
* Propagate build id

* Display build details in submission summary

* Update tests

* Add test for build id

* Unify build fragment naming

* Apply suggestion

Co-authored-by: Wojciech Kozyra <[email protected]>

Co-authored-by: Wojciech Kozyra <[email protected]>
2021-07-22 11:21:57 +02:00
Evan Bacon 73a0dd6e23 Fix capability syncing (#521)
* Update bundleIdCapabilities.ts

* Update CHANGELOG.md
2021-07-21 12:22:43 -06:00
Dominik Sokal a9dc7f9eeb don't show diff for generated files (yarn.lock, graphql files) 2021-07-21 10:43:22 +02:00
Bartłomiej KlocekandBrent Vatne c3836f23d1 [eas submit] Pretty-print iPad multitasking error message (#517)
* [eas submit] Pretty-print iPad multitasking error message

* Apply suggestion

Co-authored-by: Brent Vatne <[email protected]>

Co-authored-by: Brent Vatne <[email protected]>
2021-07-21 08:00:21 +02:00
Cedric van Putten bfd73cbb19 [eas-cli] Add force option to overwrite existing secrets (#513)
* [eas-cli] Add force option to overwrite existing secrets

* Add changelog entry
2021-07-15 17:15:29 +02:00
Wojciech Kozyra 6a1753edaa [eas-cli] handle blocked amplitude domains (#512)
* [eas-cli] handle blocked amplitude domains

* update CHANGELOG
2021-07-14 13:28:17 +02:00
Wojciech KozyraandBrent Vatne 558a73086d [eas-cli] add error message when free tier is disabled (#509)
* [eas-cli] add error message when free tier is disabled

* Update packages/eas-cli/src/build/build.ts

Co-authored-by: Brent Vatne <[email protected]>

Co-authored-by: Brent Vatne <[email protected]>
2021-07-13 12:36:20 +02:00
jkh 15966fbd26 [eas-cli] un-hide project:* (#500) 2021-07-12 13:57:36 -07:00
Dominik Sokal 1e8b55383e update CHANGELOG 2021-07-12 10:51:57 +02:00
Dominik Sokal 6920ebb0ba v0.21.0 2021-07-12 10:43:10 +02:00
Dominik Sokal 55f35949e1 build:list / build:view - print runtime version (#508)
* build:list / build:view - print runtime version

* update CHANGELOG
2021-07-12 10:38:17 +02:00
Dominik Sokal ccb998a221 [ENG-1460] add runtime version to build metadata (#493)
* add runtime version to build metadata

* update CHANGELOG

* upgrade @expo/eas-build-job to 0.2.43
2021-07-12 09:46:00 +02:00
Dominik Sokal 30bc27b4c4 [ENG-1474] build:list / build:view: print more metadata (#504)
* build:list / build:view: print more metadata

* update CHANGELOG
2021-07-12 09:36:58 +02:00
Evan Bacon da3c95ccaf Added iOS 15 capabilities (#499)
* Added iOS 15 capabilities

* Update CHANGELOG.md
2021-07-09 14:18:52 -06:00
Dominik Sokal bbcbe1dceb update CHANGELOG 2021-07-09 10:41:01 +02:00
54 changed files with 1688 additions and 223 deletions

No files matched your search

+1
View File
@@ -19,6 +19,7 @@ module.exports = {
},
],
'flowtype/no-types-missing-file-annotation': 'off',
'curly': 'warn',
},
settings: {
react: {
+1
View File
@@ -0,0 +1 @@
**/yarn.lock linguist-generated
+1
View File
@@ -11,3 +11,4 @@ tmp
node_modules
coverage
.history
.DS_Store
+27 -3
View File
@@ -6,17 +6,41 @@ 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))
### 🎉 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))
### 🐛 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))
### 🧹 Chores
## [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
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.20.0"
"version": "0.22.0"
}
+2
View File
@@ -0,0 +1,2 @@
graphql.schema.json linguist-generated
src/graphql/generated.ts linguist-generated
+50 -22
View File
@@ -47,6 +47,8 @@ eas --help COMMAND
* [`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 +71,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.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -83,7 +85,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.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -97,7 +99,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.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -108,7 +110,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.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas build`
@@ -128,7 +130,7 @@ OPTIONS
--[no-]wait Wait for build(s) to complete
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.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.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -139,7 +141,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.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -153,7 +155,7 @@ OPTIONS
-p, --platform=(android|ios|all) Platform to configure
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.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.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:list`
@@ -169,7 +171,7 @@ OPTIONS
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.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.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:view [BUILD_ID]`
@@ -180,7 +182,7 @@ 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.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas credentials`
@@ -191,7 +193,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.0/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -202,7 +204,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.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:list`
@@ -216,7 +218,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.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.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -227,7 +229,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.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas help [COMMAND]`
@@ -246,6 +248,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.0/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.0/packages/eas-cli/src/commands/project/init.ts)_
## `eas secret:create`
Create an environment secret on the current project or owner account.
@@ -255,12 +282,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.0/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -277,7 +305,7 @@ DESCRIPTION
Unsure where to find the secret's ID? Run eas secrets:list
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.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.0/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -288,7 +316,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.0/packages/eas-cli/src/commands/secret/list.ts)_
## `eas submit --platform=(android|ios)`
@@ -368,7 +396,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.0/packages/eas-cli/src/commands/submit.ts)_
## `eas webhook:create`
@@ -387,7 +415,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.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -401,7 +429,7 @@ ARGUMENTS
ID ID of the webhook to delete
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.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.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -415,7 +443,7 @@ OPTIONS
--event=(BUILD) Event type that triggers the webhook
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.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.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -435,7 +463,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.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -449,5 +477,5 @@ ARGUMENTS
ID ID of the webhook to view
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.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.0/packages/eas-cli/src/commands/webhook/view.ts)_
<!-- commandsstop -->
+572 -11
View File
@@ -312,6 +312,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "easBuild",
"description": "Top-level query object for querying EAS Build configuration.",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "EasBuildQuery",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "experimentation",
"description": "Top-level query object for querying Experimentation configuration.",
@@ -1419,7 +1435,7 @@
},
{
"name": "buildOrBuildJobs",
"description": "Coalesced Build (EAS) or BuildJob (Classic) items for this account. Use \"createdBefore\" to offset a query.",
"description": "Coalesced Build (EAS) or BuildJob (Classic) for all apps belonging to this account.",
"args": [
{
"name": "limit",
@@ -1437,7 +1453,7 @@
},
{
"name": "createdBefore",
"description": " Use a Date to offset queries ",
"description": " Offset the query ",
"type": {
"kind": "SCALAR",
"name": "DateTime",
@@ -1463,6 +1479,73 @@
}
}
},
"isDeprecated": true,
"deprecationReason": "Use activityTimelineProjectActivities with filterTypes instead"
},
{
"name": "activityTimelineProjectActivities",
"description": "Coalesced project activity for all apps belonging to this account.",
"args": [
{
"name": "limit",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "createdBefore",
"description": " Offset the query ",
"type": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
},
"defaultValue": null
},
{
"name": "filterTypes",
"description": " Types of objects to filter ",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "ActivityTimelineProjectActivityType",
"ofType": null
}
}
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INTERFACE",
"name": "ActivityTimelineProjectActivity",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
@@ -2822,7 +2905,7 @@
},
{
"name": "buildOrBuildJobs",
"description": "Coalesced Build (EAS) or BuildJob (Classic) items for this app. Use \"createdBefore\" to offset a query.",
"description": "Coalesced Build (EAS) or BuildJob (Classic) items for this app.",
"args": [
{
"name": "limit",
@@ -2840,7 +2923,7 @@
},
{
"name": "createdBefore",
"description": " Use a Date to offset queries ",
"description": " Offset the query ",
"type": {
"kind": "SCALAR",
"name": "DateTime",
@@ -2866,8 +2949,8 @@
}
}
},
"isDeprecated": false,
"deprecationReason": null
"isDeprecated": true,
"deprecationReason": "Use activityTimelineProjectActivities with filterTypes instead"
},
{
"name": "submissions",
@@ -3168,7 +3251,7 @@
},
{
"name": "activityTimelineProjectActivities",
"description": "Coalesced project activity for an app. Use \"createdBefore\" to offset a query.",
"description": "Coalesced project activity for an app",
"args": [
{
"name": "limit",
@@ -3186,13 +3269,31 @@
},
{
"name": "createdBefore",
"description": " Use a Date to offset queries ",
"description": " Offset the query ",
"type": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
},
"defaultValue": null
},
{
"name": "filterTypes",
"description": " Types of objects to filter ",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "ActivityTimelineProjectActivityType",
"ofType": null
}
}
},
"defaultValue": null
}
],
"type": {
@@ -4068,6 +4169,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "iosEnterpriseProvisioning",
"description": "",
"args": [],
"type": {
"kind": "ENUM",
"name": "BuildIosEnterpriseProvisioning",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "buildProfile",
"description": "",
@@ -4668,6 +4781,73 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "activityTimelineProjectActivities",
"description": "Coalesced project activity for all apps belonging to all accounts this user belongs to. Only resolves for the viewer.",
"args": [
{
"name": "limit",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "createdBefore",
"description": " Offset the query ",
"type": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
},
"defaultValue": null
},
{
"name": "filterTypes",
"description": " Types of objects to filter ",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "ActivityTimelineProjectActivityType",
"ofType": null
}
}
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INTERFACE",
"name": "ActivityTimelineProjectActivity",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "featureGates",
"description": "Server feature gate values for this actor, optionally filtering by desired gates.\nOnly resolves for the viewer.",
@@ -5340,6 +5520,41 @@
],
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "ActivityTimelineProjectActivityType",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "BUILD_JOB",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "BUILD",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "UPDATE",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "SUBMISSION",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "JSONObject",
@@ -5449,6 +5664,29 @@
],
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "BuildIosEnterpriseProvisioning",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "ADHOC",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "UNIVERSAL",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "BuildError",
@@ -10857,6 +11095,79 @@
},
"isDeprecated": true,
"deprecationReason": "Use App.builds instead"
},
{
"name": "all",
"description": "Get all builds.\nBy default, they are sorted from latest to oldest.\nAvailable only for admin users.",
"args": [
{
"name": "statuses",
"description": null,
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "BuildStatus",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "offset",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "limit",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "order",
"description": null,
"type": {
"kind": "ENUM",
"name": "Order",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Build",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -10864,6 +11175,29 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "Order",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "DESC",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ASC",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ClientBuildQuery",
@@ -10993,6 +11327,113 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "EasBuildQuery",
"description": "",
"fields": [
{
"name": "killSwitches",
"description": "Get EAS Build kill switches state",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "EasBuildKillSwitch",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "EasBuildKillSwitch",
"description": "",
"fields": [
{
"name": "name",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EasBuildKillSwitchName",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "value",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "EasBuildKillSwitchName",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "STOP_ACCEPTING_BUILDS",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "STOP_SCHEDULING_BUILDS",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "STOP_ACCEPTING_NORMAL_PRIORITY_BUILDS",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ExperimentationQuery",
@@ -12080,6 +12521,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "easBuildKillSwitch",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "EasBuildKillSwitchMutation",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "emailSubscription",
"description": "Mutations that modify an EmailSubscription",
@@ -16103,6 +16560,16 @@
},
"defaultValue": null
},
{
"name": "iosEnterpriseProvisioning",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildIosEnterpriseProvisioning",
"ofType": null
},
"defaultValue": null
},
{
"name": "appName",
"description": "",
@@ -18730,9 +19197,13 @@
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Update",
"ofType": null
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Update",
"ofType": null
}
}
}
},
@@ -18968,6 +19439,16 @@
}
},
"defaultValue": null
},
{
"name": "extra",
"description": "",
"type": {
"kind": "SCALAR",
"name": "JSONObject",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
@@ -20464,6 +20945,86 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "EasBuildKillSwitchMutation",
"description": "",
"fields": [
{
"name": "set",
"description": "Set an EAS Build kill switch to a given value",
"args": [
{
"name": "name",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EasBuildKillSwitchName",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "value",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "EasBuildKillSwitch",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "resetAll",
"description": "Reset all EAS Build kill switches (set them to false)",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "EasBuildKillSwitch",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "EmailSubscriptionMutation",
+18 -6
View File
@@ -1,20 +1,20 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.20.0",
"version": "0.22.0",
"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.44",
"@expo/eas-json": "^0.22.0",
"@expo/json-file": "8.2.25",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.11",
@@ -131,15 +131,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"
},
+1 -1
View File
@@ -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) });
}
}
@@ -94,7 +94,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,4 +1,4 @@
import { ArchiveSource, Job, sanitizeJob } from '@expo/eas-build-job';
import { Android, ArchiveSource, Job, sanitizeJob } from '@expo/eas-build-job';
import path from 'path';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
@@ -34,6 +34,12 @@ export async function prepareJobAsync(
}
: {};
const { gradleCommand } = ctx.buildProfile;
let buildType: Android.BuildType | undefined = ctx.buildProfile.buildType;
if (!buildType && !gradleCommand && ctx.buildProfile.distribution === 'internal') {
buildType = Android.BuildType.APK;
}
const job = {
type: ctx.workflow,
platform: Platform.ANDROID,
@@ -57,11 +63,11 @@ export async function prepareJobAsync(
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
gradleCommand: ctx.buildProfile.gradleCommand,
gradleCommand,
artifactPath: ctx.buildProfile.artifactPath,
username,
buildType: ctx.buildProfile.buildType,
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;
}
}
+7
View File
@@ -109,6 +109,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'
) {
+14
View File
@@ -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;
}
}
+13 -1
View File
@@ -1,5 +1,5 @@
import { Metadata } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import { CredentialsSource, IosEnterpriseProvisioning } from '@expo/eas-json';
import { getApplicationIdAsync } from '../project/android/applicationId';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
@@ -44,6 +44,7 @@ export async function collectMetadata<T extends Platform>(
workflow: ctx.workflow,
credentialsSource,
sdkVersion: ctx.commandCtx.exp.sdkVersion,
runtimeVersion: ctx.commandCtx.exp.runtimeVersion,
...channelOrReleaseChannel,
distribution: ctx.buildProfile.distribution ?? 'store',
appName: ctx.commandCtx.exp.name,
@@ -51,6 +52,11 @@ export async function collectMetadata<T extends Platform>(
buildProfile: ctx.commandCtx.profile,
gitCommitHash: await vcs.getCommitHashAsync(),
username: getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync()),
...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
ctx as BuildContext<Platform.IOS>
),
}),
};
}
@@ -134,3 +140,9 @@ async function getNativeChannelAsync<T extends Platform>(
return undefined;
}
function resolveIosEnterpriseProvisioning(
ctx: BuildContext<Platform.IOS>
): IosEnterpriseProvisioning | undefined {
return ctx.buildProfile.enterpriseProvisioning;
}
@@ -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,6 +1,10 @@
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';
@@ -8,7 +12,7 @@ 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,11 +41,39 @@ 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',
@@ -57,13 +89,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 +113,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 => {
@@ -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,
@@ -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'];
+43 -7
View File
@@ -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();
+1 -3
View File
@@ -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'],
@@ -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',
]);
@@ -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,6 +25,13 @@ 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');
};
@@ -63,7 +73,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 +119,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 +132,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 +155,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 +226,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 +325,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 +357,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(
@@ -136,10 +136,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);
@@ -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));
}
+111 -11
View File
@@ -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,7 +429,10 @@ 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>;
@@ -427,7 +448,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>;
@@ -538,6 +559,7 @@ export type AppUpdateBranchByNameArgs = {
export type AppActivityTimelineProjectActivitiesArgs = {
limit: Scalars['Int'];
createdBefore?: Maybe<Scalars['DateTime']>;
filterTypes?: Maybe<Array<ActivityTimelineProjectActivityType>>;
};
@@ -622,6 +644,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 +690,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 +729,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 +814,13 @@ export enum Role {
NotAdmin = 'NOT_ADMIN'
}
export enum ActivityTimelineProjectActivityType {
BuildJob = 'BUILD_JOB',
Build = 'BUILD',
Update = 'UPDATE',
Submission = 'SUBMISSION'
}
export type BuildArtifacts = {
__typename?: 'BuildArtifacts';
@@ -800,6 +840,11 @@ export enum DistributionType {
Simulator = 'SIMULATOR'
}
export enum BuildIosEnterpriseProvisioning {
Adhoc = 'ADHOC',
Universal = 'UNIVERSAL'
}
export type BuildError = {
__typename?: 'BuildError';
errorCode: Scalars['String'];
@@ -1496,6 +1541,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 +1563,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 +1597,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 +1794,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 +2549,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 +2956,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 +3008,7 @@ export type UpdateInfoGroup = {
export type PartialManifest = {
launchAsset: PartialManifestAsset;
assets: Array<Maybe<PartialManifestAsset>>;
extra?: Maybe<Scalars['JSONObject']>;
};
export type PartialManifestAsset = {
@@ -3207,6 +3292,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>;
@@ -4640,10 +4739,10 @@ export type UpdatePublishMutation = (
{ __typename?: 'RootMutation' }
& { updateBranch: (
{ __typename?: 'UpdateBranchMutation' }
& { publishUpdateGroup: Array<Maybe<(
& { publishUpdateGroup: Array<(
{ __typename?: 'Update' }
& Pick<Update, 'id' | 'group'>
)>> }
)> }
) }
);
@@ -4651,6 +4750,7 @@ export type CreateSubmissionMutationVariables = Exact<{
appId: Scalars['ID'];
platform: AppPlatform;
config: Scalars['JSONObject'];
submittedBuildId?: Maybe<Scalars['ID']>;
}>;
@@ -4796,16 +4896,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 +5063,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'>
@@ -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
}
@@ -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 [
@@ -34,8 +34,16 @@ export const BuildFragmentNode = gql`
}
}
}
channel
releaseChannel
distribution
iosEnterpriseProvisioning
buildProfile
sdkVersion
appVersion
appBuildVersion
runtimeVersion
gitCommitHash
createdAt
updatedAt
}
+6 -2
View File
@@ -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',
},
},
},
});
});
+13 -2
View File
@@ -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;
@@ -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,
submittedBuildId: buildId,
});
return submission.id;
}
@@ -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>> = {
@@ -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
);
});
});
@@ -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,
};
}
@@ -49,7 +49,12 @@ class IosSubmitter extends BaseSubmitter<IosSubmissionContext, IosSubmissionOpti
SummaryHumanReadableKeys,
SummaryHumanReadableValues
);
const result = await this.startSubmissionAsync(submissionConfig, this.ctx.commandFlags.verbose);
const result = await this.startSubmissionAsync(
submissionConfig,
resolvedSourceOptions.archive.build?.id,
this.ctx.commandFlags.verbose
);
if (result === SubmissionStatus.Finished) {
Log.addNewLineIfNone();
@@ -115,7 +120,7 @@ const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
projectId: 'Project ID',
archiveUrl: 'Archive URL',
archivePath: 'Archive Path',
buildId: 'Build ID',
formattedBuild: 'Build',
};
const SummaryHumanReadableValues: Partial<Record<keyof SummaryData, Function>> = {};
@@ -104,7 +104,8 @@ describe(IosSubmitCommand, () => {
expect(SubmissionService.startSubmissionAsync).toHaveBeenCalledWith(
AppPlatform.Ios,
projectId,
iosSubmissionConfig
iosSubmissionConfig,
undefined
);
delete process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD;
@@ -1,30 +1,35 @@
import { AppPlatform, BuildStatus } from '../../graphql/generated';
import { AppPlatform, BuildArtifacts, BuildFragment, BuildStatus } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
export async function getBuildArtifactUrlByIdAsync(
// `BuildFragment` with non-null `artifacts.buildUrl`
type BuildFragmentWithArtifact = BuildFragment & {
artifacts: BuildArtifacts & { buildUrl: string };
};
/**
* Gets build by ID and ensures that `artifacts.buildUrl` exists
*/
export async function getBuildByIdForSubmissionAsync(
platform: AppPlatform,
buildId: string
): Promise<string> {
const { platform: buildPlatform, artifacts } = await BuildQuery.byIdAsync(buildId);
): Promise<BuildFragmentWithArtifact> {
const build = await BuildQuery.byIdAsync(buildId);
if (buildPlatform !== platform) {
if (build.platform !== platform) {
throw new Error("Build platform doesn't match!");
}
if (!artifacts) {
throw new Error('Build has no artifacts.');
if (!buildFragmentHasArtifact(build)) {
throw new Error('Build has no artifacts or build URL is not defined.');
}
const buildUrl = artifacts.buildUrl;
if (!buildUrl) {
throw new Error('Build URL is not defined.');
}
return buildUrl;
return build;
}
export async function getLatestBuildArtifactUrlAsync(
export async function getLatestBuildForSubmissionAsync(
platform: AppPlatform,
appId: string
): Promise<string | null> {
): Promise<BuildFragmentWithArtifact | null> {
const builds = await BuildQuery.allForAppAsync(appId, {
platform,
status: BuildStatus.Finished,
@@ -35,10 +40,15 @@ export async function getLatestBuildArtifactUrlAsync(
return null;
}
const { artifacts } = builds[0];
if (!artifacts) {
const build = builds[0];
if (!buildFragmentHasArtifact(build)) {
return null;
}
return artifacts.buildUrl ?? null;
return build;
}
// Utility function needed to make TypeScript happy
function buildFragmentHasArtifact(build: BuildFragment): build is BuildFragmentWithArtifact {
return build.artifacts?.buildUrl != null;
}
@@ -15,6 +15,7 @@ enum SubmissionErrorCode {
IOS_MISSING_APP_ICON = 'SUBMISSION_SERVICE_IOS_MISSING_APP_ICON',
IOS_INVALID_SIGNATURE = 'SUBMISSION_SERVICE_IOS_INVALID_SIGNATURE',
IOS_INCORRECT_CREDENTIALS = 'SUBMISSION_SERVICE_IOS_INVALID_CREDENTIALS',
IOS_IPAD_INVALID_ORIENTATION = 'SUBMISSION_SERVICE_IOS_IPAD_INVALID_ORIENTATION',
}
const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
@@ -57,6 +58,10 @@ const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
`${learnMore('https://docs.expo.io/distribution/app-signing')}`,
[SubmissionErrorCode.IOS_INCORRECT_CREDENTIALS]:
'Your Apple ID or app-specific password is incorrect. Please verify that you entered them correctly and try again.',
[SubmissionErrorCode.IOS_IPAD_INVALID_ORIENTATION]:
"Your app doesn't support iPad multitasking and has to require full screen.\n" +
"If you're submitting a managed Expo project, set the `expo.ios.requireFullScreen` to true in app.json and build the project again.\n" +
`${learnMore('https://expo.fyi/ipad-requires-fullscreen')}`,
};
export function printSubmissionError(error: SubmissionError): boolean {
@@ -1,17 +1,51 @@
import chalk from 'chalk';
import { AppPlatform, BuildFragment } from '../../graphql/generated';
import Log from '../../log';
import formatFields from '../../utils/formatFields';
import { Archive, ArchiveFileSourceType } from '../archiveSource';
export interface ArchiveSourceSummaryFields {
archiveUrl?: string;
archivePath?: string;
buildId?: string;
formattedBuild?: string;
}
function formatSubmissionBuildSummary(build: BuildFragment) {
const fields = [
{
label: 'Build ID',
value: build.id,
},
{
label: 'Build Date',
value: new Date(build.createdAt).toLocaleString(),
},
{
label: 'App Version',
value: build.appVersion,
},
{
label: build.platform === AppPlatform.Android ? 'Version code' : 'Build number',
value: build.appBuildVersion,
},
];
const filteredFields = fields.filter(({ value }) => value !== undefined && value !== null) as {
label: string;
value: string;
}[];
return (
'\n' +
formatFields(filteredFields, {
labelFormat: label => ` ${chalk.dim(label)}:`,
})
);
}
export function formatArchiveSourceSummary({
realFileSource,
build,
}: Archive): ArchiveSourceSummaryFields {
const summarySlice: ArchiveSourceSummaryFields = {};
@@ -23,11 +57,8 @@ export function formatArchiveSourceSummary({
summarySlice.archiveUrl = realFileSource.url;
break;
case ArchiveFileSourceType.buildId:
summarySlice.buildId = realFileSource.id;
break;
case ArchiveFileSourceType.latest:
// TODO: Resolve real build ID here
summarySlice.buildId = '[latest]';
summarySlice.formattedBuild = formatSubmissionBuildSummary(build!);
break;
}
return summarySlice;
+3 -1
View File
@@ -14,7 +14,9 @@ export function endTimer(label = LABEL, clear: boolean = true) {
const startTime = startTimes[label];
if (startTime) {
const delta = endTime - startTime;
if (clear) delete startTimes[label];
if (clear) {
delete startTimes[label];
}
return delta;
}
throw new Error(`Timer '${label}' has not be started yet`);
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with the eas.json",
"version": "0.20.0",
"version": "0.22.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/eas-build-job": "0.2.41",
"@expo/eas-build-job": "0.2.44",
"@hapi/joi": "17.1.1",
"fs-extra": "9.0.1",
"tslib": "1.14.1"
Generated
+30 -30
View File
@@ -2,36 +2,36 @@
# yarn lockfile v1
"@amplitude/identify@1.5.0", "@amplitude/identify@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@amplitude/identify/-/identify-1.5.0.tgz#6d31aaae615393867328abc6fcad037e9d1639e8"
integrity sha512-GCDxvwZvfFInZQ6m5LYnb9sVmJP+n+Re6vkBdtclp9uTNVPpdsXtdw2ann5Ts5pV10fivDpgdZ3OHehSxe742A==
"@amplitude/identify@1.7.0", "@amplitude/identify@^1.7.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@amplitude/identify/-/identify-1.7.0.tgz#ea8e435d55e55027ced0abfe1a5e250038e75f6c"
integrity sha512-AhaYj76cExvABu2C2Q6oO4aRNQhQkP7foQb7Lk7qW5ZPhdiWpAOGThdMre0pu5CSYF21G+FUnM6/1WQ5Cfkw3A==
dependencies:
"@amplitude/types" "^1.5.0"
"@amplitude/utils" "^1.5.0"
"@amplitude/types" "^1.7.0"
"@amplitude/utils" "^1.7.0"
tslib "^1.9.3"
"@amplitude/node@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@amplitude/node/-/node-1.5.0.tgz#23e1650fed6302696b669dd8ceea0164b73f220e"
integrity sha512-/jfmYHanhln/OAuL8QG76EjThnaRDnfxLxO4oYstMFZKRktnUahE5i0sH02+tJ7I2nzq+4KTYY2hc8Rvi+Vt4Q==
"@amplitude/node@1.7.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@amplitude/node/-/node-1.7.0.tgz#534745e79a621ef74b1153c19c5683559ff930ac"
integrity sha512-+2z0V+3fVbSihbJipADvFPb2bDCLvpgDnzLkNUka606DTyLWPuoDKzawUgl2yanuFXowFYEmQ2EltuQHoXd3YQ==
dependencies:
"@amplitude/identify" "^1.5.0"
"@amplitude/types" "^1.5.0"
"@amplitude/utils" "^1.5.0"
"@amplitude/identify" "^1.7.0"
"@amplitude/types" "^1.7.0"
"@amplitude/utils" "^1.7.0"
tslib "^1.9.3"
"@amplitude/types@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@amplitude/types/-/types-1.5.0.tgz#cf34d0f304558d8b0d847a6217d506e6226e653c"
integrity sha512-XspuOsUzUcxwAptHeGiIn4giuLWs285xTJa7h8kAEEynxtEI3/krWCoDYZSB9PekaPXB6phxiO/tMd9t5V9LgQ==
"@amplitude/types@^1.7.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@amplitude/types/-/types-1.7.0.tgz#c9f6636245f2de0ab58ee667b442cef58c42c453"
integrity sha512-zPENZDWlh64WHj/7Jjghu2HGWywMPundf25ycxn0tYXtxTvPmK2m/MMV6GvUN5uW0kg65eeKETCfge1UJ9MXlQ==
"@amplitude/utils@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@amplitude/utils/-/utils-1.5.0.tgz#b48c3233e12f2c04880dd98f3c2628fd0976d3cb"
integrity sha512-1DrDJkb4dVX+FiBXhGpO2Dn2cRKdP+gtrVR8vZcE8wz/V2XxUI3DDx7uQbIS6WbQf6swv6Uo2eMHYtrwebostw==
"@amplitude/utils@^1.7.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@amplitude/utils/-/utils-1.7.0.tgz#91ef07f203223ad4f5309f0c8950ebb5de6bd8eb"
integrity sha512-+WsKdFbA+rR/xSTlho90C+GlzDL7Sn51J4bmmGtOSTZk5oEPohvHzHO8pVBGDB8UvNAs2LjQQ7q7b2qRWdUtvA==
dependencies:
"@amplitude/types" "^1.5.0"
"@amplitude/types" "^1.7.0"
tslib "^1.9.3"
"@ardatan/[email protected]":
@@ -1818,10 +1818,10 @@
unique-filename "^1.1.1"
which "^1.3.1"
"@expo/[email protected]0":
version "0.0.0-alpha.20"
resolved "https://registry.yarnpkg.com/@expo/apple-utils/-/apple-utils-0.0.0-alpha.20.tgz#cbc92e4c7e8754b051b7293af60a55a550fb6583"
integrity sha512-L/M9NPNlT1e38whA3M4QdnIDCClj6Y2GPXFOxBxuwzlmh847RHwZ/ojJpVP8sLVC+is54DS1hU9vtDkiPHGPRw==
"@expo/[email protected]4":
version "0.0.0-alpha.24"
resolved "https://registry.yarnpkg.com/@expo/apple-utils/-/apple-utils-0.0.0-alpha.24.tgz#4208204d3763a4770ce9e63f36c14476adc635bd"
integrity sha512-96MGsGcRIjzJ3Stqw9fiXAgOfMn4UlmlBp2EZkmpad7945ke9Gguceafb6rpbKJHEkQd2qcBcfcumU73QisP2w==
"@expo/babel-preset-cli@^0.2.17":
version "0.2.18"
@@ -1918,10 +1918,10 @@
xcode "^3.0.0"
xml-js "^1.6.11"
"@expo/[email protected]1":
version "0.2.41"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.41.tgz#d44ce7f5e7a1f08caa415913fed43f875fce4d52"
integrity sha512-tp3i7EZRMGvlJ3NyhpBpLdIxg6+CI6z6P3JgZVh57s2skN5zjzHcfjXPcw0gVH9fCouUnvhSSye8O+g2Zo1oaA==
"@expo/[email protected]4":
version "0.2.44"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.44.tgz#11f442cf9c1ac8402afbb06742bc99d4c1c6b6d9"
integrity sha512-4/wtOyAFLa0aILfco0a/slL96CBUppSeXfPdLzcHkBIc+AUB/pUSGf/M+Z3Pn7QoA3BHCnSIW8DosqqBBOppRg==
dependencies:
"@hapi/joi" "^17.1.1"