Compare commits

...
41 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
Dominik Sokal f15beea454 v0.20.0 2021-07-09 10:31:16 +02:00
Wojciech Kozyra 16e384be76 [eas-cli] Error message for eas build downtime (#502) 2021-07-09 10:04:17 +02:00
jkh e74e6d8fc5 [eas-cli] import UI in branch:* channel:* commands (#503) 2021-07-08 18:05:51 -07:00
jkh b6f4603107 [eas-cli] remove shameful console.log (#501) 2021-07-07 18:37:41 -07:00
Wojciech Kozyra 0e7d7f765a [eas-cli] unify generic and managed workflows (#497) 2021-07-07 12:30:06 +02:00
jkh 33b4d8d719 [eas-cli] add project:info command (#498) 2021-07-06 19:25:21 -07:00
Wojciech KozyraandBartłomiej Klocek 0d765748e3 [eas-cli] override value from app.json with param (#494)
* [eas-cli] override value from app.json with param

* update changelog

* Apply suggestions from code review

Co-authored-by: Bartłomiej Klocek <[email protected]>

Co-authored-by: Bartłomiej Klocek <[email protected]>
2021-07-05 15:42:13 +02:00
Dominik Sokal 93953f1424 bump @expo/config-plugins to 3.0.3 (#495)
* bump @expo/config-plugins to 3.0.3

* update CHANGELOG

* fix type mismatch
2021-07-05 14:15:17 +02:00
Quinlan Jung e9b67db05c update chagelog 2021-07-02 11:18:30 -07:00
Quinlan Jung 09bf912b9e v0.19.1 2021-07-02 11:02:09 -07:00
Quinlan Jung e701bacdaa [eas-cli] better debugging experience (#492)
* [eas-cli] better debugging

* fyi

* changelog
2021-07-02 10:59:16 -07:00
Quinlan JungandDominik Sokal cc22542031 [eas-cli] prompt to create project if not exists, more credentials guardrails (#491)
* [eas-cli] prompt to create project if not exists

* [eas-cli] more credentials manager guardrails

* Update packages/eas-cli/src/credentials/manager/ManageAndroid.ts

Co-authored-by: Dominik Sokal <[email protected]>

* Update packages/eas-cli/src/credentials/manager/ManageIos.ts

Co-authored-by: Dominik Sokal <[email protected]>

* Update packages/eas-cli/src/project/projectUtils.ts

Co-authored-by: Dominik Sokal <[email protected]>

* pr feedback

Co-authored-by: Dominik Sokal <[email protected]>
2021-07-02 10:53:26 -07:00
Dominik Sokal 6c53262b5b [ENG-1444] improve workflow detection (#490)
* improve workflow detection

* update CHANGELOG

* address PR feedback
2021-07-02 10:24:05 +02:00
Wojciech Kozyra 7d7d4995d8 update CHANGELOG 2021-07-01 12:23:13 +02:00
Wojciech Kozyra 977546c8c9 v0.19.0 2021-07-01 12:05:57 +02:00
Brent Vatne 53dcd4cb43 [eas-cli] Support configuring one platform at a time when running eas build (#483) 2021-06-30 17:52:37 -07:00
Wojciech Kozyra 9e18e738eb [eas-cli] use new build mutation (#485) 2021-06-29 11:54:11 +02:00
Dominik Sokal 252c7187d5 [ENG-1377] auto-suggest application id and bundle identifier (#487)
* Auto-suggest application id and bundle identifier

* update CHANGELOG

* address PR feedback
2021-06-29 10:51:48 +02:00
Quinlan Jung 90852b9557 update changelog 2021-06-28 16:01:48 -07:00
133 changed files with 3939 additions and 2514 deletions

No files matched your search

+2
View File
@@ -10,6 +10,7 @@ module.exports = {
jasmine: false,
},
rules: {
'no-console':'warn',
'no-constant-condition': ['warn', { checkLoops: false }],
'sort-imports': [
'warn',
@@ -18,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
+56 -3
View File
@@ -8,14 +8,68 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 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))
## [0.19.1](https://github.com/expo/eas-cli/releases/tag/v0.19.1) - 2021-07-02
### 🎉 New features
- Improve project workflow detection (fixes the case where the `android` and/or `ios` directories are git-ignored). ([#490](https://github.com/expo/eas-cli/pull/490) by [@dsokal](https://github.com/dsokal))
- Improve credentials workflow with project creation and current working directories ([#491](https://github.com/expo/eas-cli/pull/491) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Better debugging experience ([#492](https://github.com/expo/eas-cli/pull/492) by [@quinlanj](https://github.com/quinlanj))
## [0.19.0](https://github.com/expo/eas-cli/releases/tag/v0.19.0) - 2021-07-01
### 🎉 New features
- Auto-suggest application id and bundle identifier when running `eas build:configure` for a managed project. ([#487](https://github.com/expo/eas-cli/pull/487) by [@dsokal](https://github.com/dsokal))
- Support configuring one platform at a time when running `eas build`. ([#483](https://github.com/expo/eas-cli/pull/483) by [@brentvatne](https://github.com/brentvatne))
## [0.18.3](https://github.com/expo/eas-cli/releases/tag/v0.18.3) - 2021-06-28
### 🎉 New features
- Support bundle identifiers that begin with `.`. ([#459](https://github.com/expo/eas-cli/pull/459) by [@EvanBacon](https://github.com/EvanBacon))
- Push Key setup integrated with ios build command ([#473](https://github.com/expo/eas-cli/pull/473) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Fix android builds for PKCS12 keystores. ([#484](https://github.com/expo/eas-cli/pull/484) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
## [0.18.2](https://github.com/expo/eas-cli/releases/tag/v0.18.2) - 2021-06-25
### 🎉 New features
@@ -47,7 +101,6 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Support bundle identifiers that begin with `.`. ([#459](https://github.com/expo/eas-cli/pull/459) by [@EvanBacon](https://github.com/EvanBacon))
- Make credentials manager work with multi-target iOS projects. ([#441](https://github.com/expo/eas-cli/pull/441) by [@dsokal](https://github.com/dsokal))
- Copy over credentials from Expo Classic to EAS. ([#445](https://github.com/expo/eas-cli/pull/445) by [@quinlanj](https://github.com/quinlanj))
- Add Big Sur image for iOS builds. ([#457](https://github.com/expo/eas-cli/pull/457) by [@wkozyra95](https://github.com/wkozyra95))
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.18.3"
"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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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.18.3/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 -->
+7
View File
@@ -1,2 +1,9 @@
import { fs } from 'memfs';
// needed because of a weird bug with tempy (dependency of @expo/config-plugins)
fs.mkdirSync('/tmp');
if (process.env.TMPDIR) {
fs.mkdirSync(process.env.TMPDIR, { recursive: true });
}
module.exports = fs;
-3
View File
@@ -1,3 +0,0 @@
module.exports = async () => {
process.env.TZ = 'UTC';
};
+1608 -982
View File
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -2,5 +2,6 @@ module.exports = {
...require('../../jest/jest.config.js'),
rootDir: __dirname,
roots: ['src', '__mocks__'],
globalSetup: './global-setup.ts',
globalSetup: './jest/global-setup.ts',
setupFilesAfterEnv: ['./jest/setup-after-env.ts'],
};
+5
View File
@@ -0,0 +1,5 @@
module.exports = async () => {
process.env.TZ = 'UTC';
process.env.EAS_NO_VCS = '1';
process.env.EAS_PROJECT_ROOT = '/app';
};
+6
View File
@@ -0,0 +1,6 @@
/* eslint-disable no-console */
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
console.error = jest.fn();
});
+19 -7
View File
@@ -1,20 +1,20 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.18.3",
"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": "2.0.2",
"@expo/eas-build-job": "0.2.37",
"@expo/eas-json": "^0.18.2",
"@expo/config-plugins": "3.0.3",
"@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
View File
@@ -6,4 +6,5 @@ readmeContent = readmeContent.replace(/\[build\//g, '[src/');
readmeContent = readmeContent.replace(/build\/commands/g, 'packages/eas-cli/src/commands');
fs.writeFileSync('README.md', readmeContent);
// eslint-disable-next-line no-console
console.log('Patched README path generation');
+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,14 +94,13 @@ 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;
}
return JSON.parse(stringifiedRequestHeaders)['expo-channel-name'] ?? null;
} catch (err) {
console.log({ err });
return null;
}
}
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { readVersionCode, readVersionName } from '../version';
import { readVersionCodeAsync, readVersionNameAsync } from '../version';
jest.mock('fs');
@@ -20,35 +20,35 @@ beforeEach(() => {
fs.mkdirpSync(os.tmpdir());
});
describe(readVersionCode, () => {
describe(readVersionCodeAsync, () => {
describe('generic project', () => {
it('reads the version code from native code', () => {
it('reads the version code from native code', async () => {
const exp = initGenericProject();
const versionCode = readVersionCode('/repo', exp);
const versionCode = await readVersionCodeAsync('/repo', exp);
expect(versionCode).toBe(123);
});
});
describe('managed project', () => {
it('reads the version code from expo config', () => {
it('reads the version code from expo config', async () => {
const exp = initManagedProject();
const versionCode = readVersionCode('/repo', exp);
const versionCode = await readVersionCodeAsync('/repo', exp);
expect(versionCode).toBe(123);
});
});
});
describe(readVersionName, () => {
describe(readVersionNameAsync, () => {
describe('generic project', () => {
it('reads the version name from native code', () => {
it('reads the version name from native code', async () => {
const exp = initGenericProject();
const versionName = readVersionName('/repo', exp);
const versionName = await readVersionNameAsync('/repo', exp);
expect(versionName).toBe('1.0');
});
});
describe('managed project', () => {
it('reads the version from expo config', () => {
it('reads the version from expo config', async () => {
const exp = initManagedProject();
const versionName = readVersionName('/repo', exp);
const versionName = await readVersionNameAsync('/repo', exp);
expect(versionName).toBe('1.0.0');
});
});
@@ -72,6 +72,7 @@ function initGenericProject(): ExpoConfig {
versionName "1.0"
}
}`,
'./android/app/src/main/AndroidManifest.xml': 'fake',
},
'/repo'
);
+15 -44
View File
@@ -1,9 +1,7 @@
import { Android, Metadata, Workflow } from '@expo/eas-build-job';
import { EasConfig } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import path from 'path';
import AndroidCredentialsProvider, {
AndroidCredentials,
@@ -13,46 +11,31 @@ import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutatio
import Log from '../../log';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
getApplicationId,
getApplicationIdAsync,
} from '../../project/android/applicationId';
import { toggleConfirmAsync } from '../../prompts';
import { findAccountByName } from '../../user/Account';
import { CredentialsResult, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommandContext, createBuildContext } from '../context';
import { BuildContext, CommandContext, createBuildContextAsync } from '../context';
import { transformMetadata } from '../graphql';
import { Platform } from '../types';
import { logCredentialsSource } from '../utils/credentials';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { transformGenericJob, transformManagedJob } from './graphql';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
export async function prepareAndroidBuildAsync(
commandCtx: CommandContext,
easConfig: EasConfig
): Promise<() => Promise<string | undefined>> {
const buildCtx = createBuildContext<Platform.ANDROID>({
const buildCtx = await createBuildContextAsync<Platform.ANDROID>({
commandCtx,
platform: Platform.ANDROID,
easConfig,
});
const { buildProfile } = buildCtx;
if (
buildProfile.workflow === Workflow.GENERIC &&
!(await fs.pathExists(path.join(commandCtx.projectDir, 'android')))
) {
throw new Error(
`"android" directory not found. If you're trying to build a managed project, set ${chalk.bold(
`builds.android.${commandCtx.profile}.workflow`
)} in "eas.json" to "managed".`
);
}
if (
buildProfile.workflow === Workflow.GENERIC &&
buildProfile.distribution === 'internal' &&
buildProfile.gradleCommand?.match(/bundle/)
) {
if (buildProfile.distribution === 'internal' && buildProfile.gradleCommand?.match(/bundle/)) {
Log.addNewLineIfNone();
Log.warn(
`You're building your Android app for internal distribution. However, we've detected that the Gradle command you defined (${chalk.underline(
@@ -68,7 +51,7 @@ This means that it will most likely produce an AAB and you will not be able to i
}
}
if (buildCtx.buildProfile.workflow === Workflow.MANAGED) {
if (buildCtx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(commandCtx.projectDir, commandCtx.exp);
}
@@ -76,7 +59,7 @@ This means that it will most likely produce an AAB and you will not be able to i
ctx: buildCtx,
ensureCredentialsAsync: ensureAndroidCredentialsAsync,
ensureProjectConfiguredAsync: async () => {
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
if (buildCtx.workflow === Workflow.GENERIC) {
await validateAndSyncProjectConfigurationAsync(commandCtx.projectDir, commandCtx.exp);
}
},
@@ -87,30 +70,18 @@ This means that it will most likely produce an AAB and you will not be able to i
metadata: Metadata
): Promise<BuildResult> => {
const graphqlMetadata = transformMetadata(metadata);
if (job.type === Workflow.GENERIC) {
const graphqlJob = transformGenericJob(job);
return await BuildMutation.createAndroidGenericBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
} else {
const graphqlJob = transformManagedJob(job);
return await BuildMutation.createAndroidManagedBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
}
const graphqlJob = transformJob(job);
return await BuildMutation.createAndroidBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
},
});
}
function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean {
return (
ctx.buildProfile.workflow === Workflow.MANAGED ||
(ctx.buildProfile.workflow === Workflow.GENERIC && !ctx.buildProfile.withoutCredentials)
);
return !ctx.buildProfile.withoutCredentials;
}
async function ensureAndroidCredentialsAsync(
@@ -119,7 +90,7 @@ async function ensureAndroidCredentialsAsync(
if (!shouldProvideCredentials(ctx)) {
return;
}
const androidApplicationIdentifier = getApplicationId(
const androidApplicationIdentifier = await getApplicationIdAsync(
ctx.commandCtx.projectDir,
ctx.commandCtx.exp
);
+11 -26
View File
@@ -1,14 +1,11 @@
import { Android } from '@expo/eas-build-job';
import {
AndroidGenericJobInput,
AndroidManagedBuildType,
AndroidManagedJobInput,
} from '../../graphql/generated';
import { transformProjectArchive } from '../graphql';
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
import { transformProjectArchive, transformWorkflow } from '../graphql';
export function transformGenericJob(job: Android.GenericJob): AndroidGenericJobInput {
export function transformJob(job: Android.Job): AndroidJobInput {
return {
type: transformWorkflow(job.type),
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
@@ -18,29 +15,17 @@ export function transformGenericJob(job: Android.GenericJob): AndroidGenericJobI
cache: job.cache,
gradleCommand: job.gradleCommand,
artifactPath: job.artifactPath,
};
}
export function transformManagedJob(job: Android.ManagedJob): AndroidManagedJobInput {
return {
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
updates: job.updates,
secrets: job.secrets,
builderEnvironment: job.builderEnvironment,
cache: job.cache,
username: job.username,
buildType: transformBuildType(job.buildType),
buildType: job.buildType && transformBuildType(job.buildType),
};
}
function transformBuildType(buildType: Android.ManagedBuildType): AndroidManagedBuildType {
if (buildType === Android.ManagedBuildType.APK) {
return AndroidManagedBuildType.Apk;
} else if (buildType === Android.ManagedBuildType.APP_BUNDLE) {
return AndroidManagedBuildType.AppBundle;
function transformBuildType(buildType: Android.BuildType): AndroidBuildType {
if (buildType === Android.BuildType.APK) {
return AndroidBuildType.Apk;
} else if (buildType === Android.BuildType.APP_BUNDLE) {
return AndroidBuildType.AppBundle;
} else {
return AndroidManagedBuildType.DevelopmentClient;
return AndroidBuildType.DevelopmentClient;
}
}
@@ -1,5 +1,4 @@
import { Android, ArchiveSource, Cache, Job, Workflow, sanitizeJob } from '@expo/eas-build-job';
import { AndroidGenericBuildProfile, AndroidManagedBuildProfile } from '@expo/eas-json';
import { Android, ArchiveSource, Job, sanitizeJob } from '@expo/eas-build-job';
import path from 'path';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
@@ -18,33 +17,9 @@ export async function prepareJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Job> {
if (ctx.buildProfile.workflow === Workflow.GENERIC) {
const partialJob = await prepareGenericJobAsync(ctx, jobData, ctx.buildProfile);
return sanitizeJob(partialJob);
} else if (ctx.buildProfile.workflow === Workflow.MANAGED) {
const partialJob = await prepareManagedJobAsync(ctx, jobData, ctx.buildProfile);
return sanitizeJob(partialJob);
} else {
throw new Error("Unknown workflow. Shouldn't happen");
}
}
interface CommonJobProperties {
platform: Platform.ANDROID;
projectArchive: ArchiveSource;
builderEnvironment: Android.BuilderEnvironment;
cache: Cache;
secrets: {
buildCredentials?: {
keystore: Android.Keystore;
};
};
}
async function prepareJobCommonAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Partial<CommonJobProperties>> {
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
const projectRootDirectory =
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
const { credentials } = jobData;
const buildCredentials = credentials
? {
@@ -59,8 +34,16 @@ async function prepareJobCommonAsync(
}
: {};
return {
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,
projectRootDirectory,
projectArchive: jobData.projectArchive,
builderEnvironment: {
image: ctx.buildProfile.image,
@@ -77,42 +60,15 @@ async function prepareJobCommonAsync(
secrets: {
...buildCredentials,
},
};
}
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
async function prepareGenericJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData,
buildProfile: AndroidGenericBuildProfile
): Promise<Partial<Android.GenericJob>> {
const projectRootDirectory =
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
return {
...(await prepareJobCommonAsync(ctx, jobData)),
type: Workflow.GENERIC,
gradleCommand: buildProfile.gradleCommand,
artifactPath: buildProfile.artifactPath,
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
projectRootDirectory,
};
}
gradleCommand,
artifactPath: ctx.buildProfile.artifactPath,
async function prepareManagedJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData,
buildProfile: AndroidManagedBuildProfile
): Promise<Partial<Android.ManagedJob>> {
const projectRootDirectory =
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
return {
...(await prepareJobCommonAsync(ctx, jobData)),
type: Workflow.MANAGED,
username,
buildType: buildProfile.buildType,
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
projectRootDirectory,
buildType,
};
return sanitizeJob(job);
}
+12 -6
View File
@@ -3,10 +3,13 @@ import { AndroidConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import fs from 'fs-extra';
import { resolveWorkflow } from '../../project/workflow';
import { resolveWorkflowAsync } from '../../project/workflow';
export function readVersionCode(projectDir: string, exp: ExpoConfig): number | undefined {
const workflow = resolveWorkflow(projectDir, Platform.ANDROID);
export async function readVersionCodeAsync(
projectDir: string,
exp: ExpoConfig
): Promise<number | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
const buildGradle = readBuildGradle(projectDir);
const matchResult = buildGradle?.match(/versionCode (.*)/);
@@ -16,12 +19,15 @@ export function readVersionCode(projectDir: string, exp: ExpoConfig): number | u
return undefined;
}
} else {
return AndroidConfig.Version.getVersionCode(exp) ?? undefined;
return AndroidConfig.Version.getVersionCode(exp) ?? 1;
}
}
export function readVersionName(projectDir: string, exp: ExpoConfig): string | undefined {
const workflow = resolveWorkflow(projectDir, Platform.ANDROID);
export async function readVersionNameAsync(
projectDir: string,
exp: ExpoConfig
): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
const buildGradle = readBuildGradle(projectDir);
const matchResult = buildGradle?.match(/versionName ["'](.*)["']/);
+14
View File
@@ -102,6 +102,20 @@ export async function prepareBuildRequestForPlatformAsync<
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli version.');
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
) {
Log.error(
'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'
) {
+27 -13
View File
@@ -1,9 +1,11 @@
import { getConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import fs from 'fs-extra';
import path from 'path';
import Log from '../log';
import { resolveWorkflowAsync } from '../project/workflow';
import { promptAsync } from '../prompts';
import { ensureLoggedInAsync } from '../user/actions';
import vcs from '../vcs';
@@ -37,8 +39,10 @@ export async function configureAsync(options: {
options.platform
),
shouldConfigureIos: [RequestedPlatform.All, RequestedPlatform.Ios].includes(options.platform),
hasAndroidNativeProject: await fs.pathExists(path.join(options.projectDir, 'android')),
hasIosNativeProject: await fs.pathExists(path.join(options.projectDir, 'ios')),
hasAndroidNativeProject:
(await resolveWorkflowAsync(options.projectDir, Platform.ANDROID)) === Workflow.GENERIC,
hasIosNativeProject:
(await resolveWorkflowAsync(options.projectDir, Platform.IOS)) === Workflow.GENERIC,
};
Log.newLine();
@@ -59,32 +63,40 @@ export async function configureAsync(options: {
}
}
const MANAGED_DEFAULTS = {
const ANDROID_MANAGED_DEFAULTS = {
release: {
workflow: 'managed',
buildType: 'app-bundle',
},
development: {
workflow: 'managed',
buildType: 'development-client',
distribution: 'internal',
},
};
const ANDROID_GENERIC_DEFAULTS = {
release: {
workflow: 'generic',
gradleCommand: ':app:bundleRelease',
},
development: {
workflow: 'generic',
gradleCommand: ':app:assembleDebug',
distribution: 'internal',
},
};
const IOS_GENERIC_DEFAULTS = {
const IOS_MANAGED_DEFAULTS = {
release: {
workflow: 'generic',
buildType: 'release',
},
development: {
buildType: 'development-client',
distribution: 'internal',
},
};
const IOS_GENERIC_DEFAULTS = {
release: {
schemeBuildConfiguration: 'Release',
},
development: {
workflow: 'generic',
schemeBuildConfiguration: 'Debug',
distribution: 'internal',
},
@@ -121,15 +133,17 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
const easJson = {
builds: {
...existingEasJson,
...existingEasJson?.builds,
...(shouldInitAndroid
? {
android: ctx.hasAndroidNativeProject ? ANDROID_GENERIC_DEFAULTS : MANAGED_DEFAULTS,
android: ctx.hasAndroidNativeProject
? ANDROID_GENERIC_DEFAULTS
: ANDROID_MANAGED_DEFAULTS,
}
: null),
...(shouldInitIOS
? {
ios: ctx.hasIosNativeProject ? IOS_GENERIC_DEFAULTS : MANAGED_DEFAULTS,
ios: ctx.hasIosNativeProject ? IOS_GENERIC_DEFAULTS : IOS_MANAGED_DEFAULTS,
}
: null),
},
+28 -13
View File
@@ -6,6 +6,7 @@ import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
import { getProjectAccountName } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { findAccountByName } from '../user/Account';
import { Actor } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
@@ -92,9 +93,10 @@ export interface BuildContext<T extends Platform> {
trackingCtx: TrackingContext;
platform: T;
buildProfile: PlatformBuildProfile<T>;
workflow: Workflow;
}
export function createBuildContext<T extends Platform>({
export async function createBuildContextAsync<T extends Platform>({
platform,
easConfig,
commandCtx,
@@ -102,45 +104,58 @@ export function createBuildContext<T extends Platform>({
platform: T;
easConfig: EasConfig;
commandCtx: CommandContext;
}): BuildContext<T> {
}): Promise<BuildContext<T>> {
const buildProfile = easConfig.builds[platform] as PlatformBuildProfile<T> | undefined;
if (!buildProfile) {
throw new Error(`${platform} build profile does not exist`);
}
const workflow = await resolveWorkflowAsync(commandCtx.projectDir, platform);
const accountId = findAccountByName(commandCtx.user.accounts, commandCtx.accountName)?.id;
const devClientProperties = getDevClientEventProperties(platform, commandCtx, buildProfile);
const devClientProperties = getDevClientEventProperties({
platform,
projectDir: commandCtx.projectDir,
buildProfile,
workflow,
});
const trackingCtx = {
tracking_id: uuidv4(),
platform,
...(accountId && { account_id: accountId }),
account_name: commandCtx.accountName,
project_id: commandCtx.projectId,
project_type: buildProfile.workflow,
project_type: workflow,
...devClientProperties,
};
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
return {
commandCtx,
workflow,
trackingCtx,
platform,
buildProfile,
};
}
function getDevClientEventProperties(
platform: Platform,
commandCtx: CommandContext,
buildProfile: AndroidBuildProfile | IosBuildProfile
): Partial<TrackingContext> {
function getDevClientEventProperties({
platform,
projectDir,
buildProfile,
workflow,
}: {
platform: Platform;
projectDir: string;
buildProfile: AndroidBuildProfile | IosBuildProfile;
workflow: Workflow;
}): Partial<TrackingContext> {
let includesDevClient;
const version = tryGetDevClientVersion(commandCtx.projectDir);
if (buildProfile.workflow === Workflow.MANAGED) {
includesDevClient = buildProfile.buildType === 'development-client';
} else if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
const version = tryGetDevClientVersion(projectDir);
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
} else if (platform === Platform.IOS && 'schemeBuildConfiguration' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.schemeBuildConfiguration === 'Debug');
} else if ('buildType' in buildProfile) {
includesDevClient = buildProfile.buildType === 'development-client';
} else {
includesDevClient = false;
}
+15 -1
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),
};
}
@@ -55,10 +59,20 @@ function transformDistribution(distribution: Metadata['distribution']): Distribu
}
}
function transformWorkflow(workflow: Metadata['workflow']): BuildWorkflow {
export function transformWorkflow(workflow: Workflow): BuildWorkflow {
if (workflow === Workflow.GENERIC) {
return BuildWorkflow.Generic;
} else {
return BuildWorkflow.Managed;
}
}
export function transformIosEnterpriseProvisioning(
enterpriseProvisioning: Metadata['iosEnterpriseProvisioning']
): BuildIosEnterpriseProvisioning {
if (enterpriseProvisioning === 'adhoc') {
return BuildIosEnterpriseProvisioning.Adhoc;
} else {
return BuildIosEnterpriseProvisioning.Universal;
}
}
@@ -75,7 +75,7 @@ async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig)
async function readExpoPlistAsync(projectDir: string): Promise<IOSConfig.ExpoPlist> {
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
return await readPlistAsync(expoPlistPath);
return (await readPlistAsync(expoPlistPath)) as IOSConfig.ExpoPlist;
}
async function writeExpoPlistAsync(
@@ -15,12 +15,7 @@ import {
jest.mock('fs');
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.removeSync(os.tmpdir());
+10 -33
View File
@@ -1,8 +1,5 @@
import { Ios, Job, Metadata, Workflow } from '@expo/eas-build-job';
import { EasConfig } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { IosCredentials } from '../../credentials/ios/types';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
@@ -10,37 +7,26 @@ import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../pro
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { resolveTargetsAsync } from '../../project/ios/target';
import { JobData, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommandContext, createBuildContext } from '../context';
import { BuildContext, CommandContext, createBuildContextAsync } from '../context';
import { transformMetadata } from '../graphql';
import { Platform } from '../types';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { ensureIosCredentialsAsync } from './credentials';
import { transformGenericJob, transformManagedJob } from './graphql';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
export async function prepareIosBuildAsync(
commandCtx: CommandContext,
easConfig: EasConfig
): Promise<() => Promise<string | undefined>> {
const buildCtx = createBuildContext<Platform.IOS>({
const buildCtx = await createBuildContextAsync<Platform.IOS>({
commandCtx,
platform: Platform.IOS,
easConfig,
});
const { buildProfile } = buildCtx;
if (
buildProfile.workflow === Workflow.GENERIC &&
!(await fs.pathExists(path.join(commandCtx.projectDir, 'ios')))
) {
throw new Error(
`"ios" directory not found. If you're trying to build a managed project, set ${chalk.bold(
`builds.ios.${commandCtx.profile}.workflow`
)} in "eas.json" to "managed".`
);
}
if (buildProfile.workflow === Workflow.MANAGED) {
if (buildCtx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(
commandCtx.projectDir,
commandCtx.exp
@@ -87,21 +73,12 @@ export async function prepareIosBuildAsync(
metadata: Metadata
): Promise<BuildResult> => {
const graphqlMetadata = transformMetadata(metadata);
if (job.type === Workflow.GENERIC) {
const graphqlJob = transformGenericJob(job);
return await BuildMutation.createIosGenericBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
} else {
const graphqlJob = transformManagedJob(job);
return await BuildMutation.createIosManagedBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
}
const graphqlJob = transformJob(job);
return await BuildMutation.createIosBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
},
});
}
+4 -2
View File
@@ -1,5 +1,5 @@
import { ExpoConfig } from '@expo/config';
import { Workflow } from '@expo/eas-build-job';
import { Platform, Workflow } from '@expo/eas-build-job';
import { IosBuildProfile, VersionAutoIncrement } from '@expo/eas-json';
import Log from '../../log';
@@ -7,6 +7,7 @@ import {
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
warnIfBundleIdentifierDefinedInAppConfigForGenericProject,
} from '../../project/ios/bundleIdentifier';
import { resolveWorkflowAsync } from '../../project/workflow';
import { ConfigureContext } from '../context';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
@@ -35,7 +36,8 @@ export async function validateAndSyncProjectConfigurationAsync({
exp: ExpoConfig;
buildProfile: IosBuildProfile;
}): Promise<void> {
const { workflow, autoIncrement } = buildProfile;
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const { autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement);
if (workflow === Workflow.GENERIC) {
@@ -28,7 +28,7 @@ export async function ensureIosCredentialsAsync(
iosCapabilitiesOptions: {
entitlements: await resolveEntitlementsJsonAsync(
buildCtx.commandCtx.projectDir,
buildCtx.buildProfile.workflow
buildCtx.workflow
),
},
distribution: buildCtx.buildProfile.distribution ?? 'store',
+10 -21
View File
@@ -3,15 +3,15 @@ import nullthrows from 'nullthrows';
import {
DistributionType,
IosGenericJobInput,
IosBuildType,
IosJobInput,
IosJobSecretsInput,
IosManagedBuildType,
IosManagedJobInput,
} from '../../graphql/generated';
import { transformProjectArchive } from '../graphql';
import { transformProjectArchive, transformWorkflow } from '../graphql';
export function transformGenericJob(job: Ios.GenericJob): IosGenericJobInput {
export function transformJob(job: Ios.Job): IosJobInput {
return {
type: transformWorkflow(job.type),
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
@@ -20,22 +20,11 @@ export function transformGenericJob(job: Ios.GenericJob): IosGenericJobInput {
secrets: transformIosSecrets(job.secrets),
builderEnvironment: job.builderEnvironment,
cache: job.cache,
scheme: job.scheme,
buildConfiguration: job.buildConfiguration,
artifactPath: job.artifactPath,
};
}
export function transformManagedJob(job: Ios.ManagedJob): IosManagedJobInput {
return {
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
updates: job.updates,
distribution: job.distribution && transformDistributionType(job.distribution),
secrets: transformIosSecrets(job.secrets),
builderEnvironment: job.builderEnvironment,
cache: job.cache,
buildType: job.buildType && transformBuildType(job.buildType),
username: job.username,
};
@@ -73,10 +62,10 @@ function transformIosSecrets(secrets: {
};
}
function transformBuildType(buildType: Ios.ManagedBuildType): IosManagedBuildType {
if (buildType === Ios.ManagedBuildType.DEVELOPMENT_CLIENT) {
return IosManagedBuildType.DevelopmentClient;
function transformBuildType(buildType: Ios.BuildType): IosBuildType {
if (buildType === Ios.BuildType.DEVELOPMENT_CLIENT) {
return IosBuildType.DevelopmentClient;
} else {
return IosManagedBuildType.Release;
return IosBuildType.Release;
}
}
+23 -79
View File
@@ -1,5 +1,4 @@
import { ArchiveSource, Cache, Ios, Job, Workflow, sanitizeJob } from '@expo/eas-build-job';
import { IosGenericBuildProfile, IosManagedBuildProfile } from '@expo/eas-json';
import { ArchiveSource, Ios, Job, sanitizeJob } from '@expo/eas-build-job';
import path from 'path';
import semver from 'semver';
@@ -20,44 +19,22 @@ export async function prepareJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData
): Promise<Job> {
if (ctx.buildProfile.workflow === Workflow.GENERIC) {
const partialJob = await prepareGenericJobAsync(ctx, jobData, ctx.buildProfile);
return sanitizeJob(partialJob);
} else if (ctx.buildProfile.workflow === Workflow.MANAGED) {
const partialJob = await prepareManagedJobAsync(ctx, jobData, ctx.buildProfile);
return sanitizeJob(partialJob);
} else {
throw new Error("Unknown workflow. Shouldn't happen");
}
}
interface CommonJobProperties {
platform: Platform.IOS;
projectArchive: ArchiveSource;
builderEnvironment: Ios.BuilderEnvironment;
distribution?: Ios.DistributionType;
cache: Cache;
secrets: {
buildCredentials: Ios.BuildCredentials;
environmentSecrets?: Record<string, string>;
};
}
async function prepareJobCommonAsync(
ctx: BuildContext<Platform.IOS>,
{ credentials, projectArchive }: { credentials?: IosCredentials; projectArchive: ArchiveSource }
): Promise<Partial<CommonJobProperties>> {
const buildCredentials: CommonJobProperties['secrets']['buildCredentials'] = {};
if (credentials) {
const targetNames = Object.keys(credentials);
const projectRootDirectory =
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
if (jobData.credentials) {
const targetNames = Object.keys(jobData.credentials);
for (const targetName of targetNames) {
buildCredentials[targetName] = prepareTargetCredentials(credentials[targetName]);
buildCredentials[targetName] = prepareTargetCredentials(jobData.credentials[targetName]);
}
}
return {
const job = {
type: ctx.workflow,
platform: Platform.IOS,
projectArchive,
projectArchive: jobData.projectArchive,
projectRootDirectory,
distribution: ctx.buildProfile.distribution,
builderEnvironment: {
image: resolveImage(ctx),
@@ -76,7 +53,18 @@ async function prepareJobCommonAsync(
secrets: {
buildCredentials,
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
scheme: jobData.buildScheme,
buildConfiguration: ctx.buildProfile.schemeBuildConfiguration,
artifactPath: ctx.buildProfile.artifactPath,
buildType: ctx.buildProfile.buildType,
username,
};
return sanitizeJob(job);
}
function resolveImage(ctx: BuildContext<Platform.IOS>): Ios.BuilderEnvironment['image'] {
@@ -99,47 +87,3 @@ function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.Tar
},
};
}
async function prepareGenericJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData,
buildProfile: IosGenericBuildProfile
): Promise<Partial<Ios.GenericJob>> {
const projectRootDirectory =
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
return {
...(await prepareJobCommonAsync(ctx, {
credentials: jobData.credentials,
projectArchive: jobData.projectArchive,
})),
type: Workflow.GENERIC,
scheme: jobData.buildScheme,
buildConfiguration: buildProfile.schemeBuildConfiguration,
artifactPath: buildProfile.artifactPath,
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
projectRootDirectory,
};
}
async function prepareManagedJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData,
buildProfile: IosManagedBuildProfile
): Promise<Partial<Ios.ManagedJob>> {
const projectRootDirectory =
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
return {
...(await prepareJobCommonAsync(ctx, {
credentials: jobData.credentials,
projectArchive: jobData.projectArchive,
})),
type: Workflow.MANAGED,
buildType: buildProfile.buildType,
username,
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
projectRootDirectory,
};
}
+3 -3
View File
@@ -6,7 +6,7 @@ import nullthrows from 'nullthrows';
import semver from 'semver';
import Log from '../../log';
import { resolveWorkflow } from '../../project/workflow';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { readPlistAsync, writePlistAsync } from './plist';
@@ -106,7 +106,7 @@ export async function readShortVersionAsync(
projectDir: string,
exp: ExpoConfig
): Promise<string | undefined> {
const workflow = resolveWorkflow(projectDir, Platform.IOS);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir);
return infoPlist.CFBundleShortVersionString;
@@ -119,7 +119,7 @@ export async function readBuildNumberAsync(
projectDir: string,
exp: ExpoConfig
): Promise<string | undefined> {
const workflow = resolveWorkflow(projectDir, Platform.IOS);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir);
return infoPlist.CFBundleVersion;
+25 -14
View File
@@ -1,8 +1,8 @@
import { Metadata } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import { CredentialsSource, IosEnterpriseProvisioning } from '@expo/eas-json';
import { getApplicationId } from '../project/android/applicationId';
import { getBundleIdentifier } from '../project/ios/bundleIdentifier';
import { getApplicationIdAsync } from '../project/android/applicationId';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
import { getUsername } from '../project/projectUtils';
import { ensureLoggedInAsync } from '../user/actions';
import vcs from '../vcs';
@@ -10,7 +10,7 @@ import {
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
} from './android/UpdatesModule';
import { readVersionCode, readVersionName } from './android/version';
import { readVersionCodeAsync, readVersionNameAsync } from './android/version';
import { BuildContext } from './context';
import {
readChannelSafelyAsync as readIosChannelSafelyAsync,
@@ -37,23 +37,26 @@ export async function collectMetadata<T extends Platform>(
): Promise<Metadata> {
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
return {
// TODO: type error fixed in https://github.com/expo/eas-build/pull/34
// remove @ts-expect-error after upgrading @expo/eas-build-job
// @ts-expect-error error TS2322: not assignable to type 'Record<string, string | number>'
trackingContext: ctx.trackingCtx,
appVersion: await resolveAppVersionAsync(ctx),
appBuildVersion: await resolveAppBuildVersionAsync(ctx),
cliVersion: packageJSON.version,
workflow: ctx.buildProfile.workflow,
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,
appIdentifier: resolveAppIdentifier(ctx),
appIdentifier: await resolveAppIdentifierAsync(ctx),
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>
),
}),
};
}
@@ -63,7 +66,7 @@ async function resolveAppVersionAsync<T extends Platform>(
if (ctx.platform === Platform.IOS) {
return await readShortVersionAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
} else {
return readVersionName(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return await readVersionNameAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
}
}
@@ -73,16 +76,18 @@ async function resolveAppBuildVersionAsync<T extends Platform>(
if (ctx.platform === Platform.IOS) {
return await readBuildNumberAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
} else {
const versionCode = readVersionCode(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
const versionCode = await readVersionCodeAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return versionCode !== undefined ? String(versionCode) : undefined;
}
}
function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string {
async function resolveAppIdentifierAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<string> {
if (ctx.platform === Platform.IOS) {
return getBundleIdentifier(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return await getBundleIdentifierAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
} else {
return getApplicationId(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return await getApplicationIdAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
}
}
@@ -135,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 => {
@@ -85,7 +85,7 @@ function printBuildResult(accountName: string, build: BuildFragment): void {
account: accountName,
});
const installUrl = getInternalDistributionInstallUrl(build);
qrcodeTerminal.generate(installUrl, code => console.log(`${indentString(code, 2)}\n`));
qrcodeTerminal.generate(installUrl, code => Log.log(`${indentString(code, 2)}\n`));
Log.log(
`${appPlatformEmojis[build.platform]} Open this link on your ${
appPlatformDisplayNames[build.platform]
@@ -22,7 +22,7 @@ import { promptAsync, toggleConfirmAsync } from '../../prompts';
async function getBranchInfoAsync({
appId,
name,
}: GetBranchInfoQueryVariables): Promise<{ branchId: string }> {
}: GetBranchInfoQueryVariables): Promise<GetBranchInfoQuery> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<GetBranchInfoQuery, GetBranchInfoQueryVariables>(
@@ -46,11 +46,7 @@ async function getBranchInfoAsync({
)
.toPromise()
);
const branchId = data.app?.byId.updateBranchByName?.id;
if (!branchId) {
throw new Error(`Could not find branch ${name} on ${appId}`);
}
return { branchId };
return data;
}
async function deleteBranchOnAppAsync({
@@ -122,7 +118,11 @@ export default class BranchDelete extends Command {
}));
}
const { branchId } = await getBranchInfoAsync({ appId: projectId, name });
const data = await getBranchInfoAsync({ appId: projectId, name });
const branchId = data.app?.byId.updateBranchByName?.id;
if (!branchId) {
throw new Error(`Could not find branch ${name} on ${fullName}`);
}
if (!jsonFlag) {
Log.addNewLineIfNone();
@@ -100,6 +100,8 @@ export default class BranchList extends Command {
}),
])
);
Log.addNewLineIfNone();
Log.log(chalk.bold('Branches with their most recent update group:'));
Log.log(table.toString());
if (branches.length >= BRANCHES_LIMIT) {
+15 -38
View File
@@ -3,7 +3,6 @@ import { getRuntimeVersionForSDKVersion } from '@expo/sdk-runtime-versions';
import { Command, flags } from '@oclif/command';
import assert from 'assert';
import chalk from 'chalk';
import Table from 'cli-table3';
import dateFormat from 'dateformat';
import gql from 'graphql-tag';
import { uniqBy } from 'lodash';
@@ -19,11 +18,7 @@ import {
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import Log from '../../log';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import {
PublishPlatform,
buildBundlesAsync,
@@ -33,6 +28,7 @@ import {
} from '../../project/publish';
import { promptAsync, selectAsync } from '../../prompts';
import { formatUpdate } from '../../update/utils';
import formatFields from '../../utils/formatFields';
import vcs from '../../vcs';
import { listBranchesAsync } from './list';
import { viewUpdateBranchAsync } from './view';
@@ -134,9 +130,11 @@ export default class BranchPublish extends Command {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = await getProjectAccountNameAsync(exp);
let { slug, runtimeVersion, sdkVersion } = exp;
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
// construct the runtimeVersion with special meaning indicating that the runtime is an
@@ -265,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');
@@ -307,35 +305,14 @@ export default class BranchPublish extends Command {
if (jsonFlag) {
Log.log(JSON.stringify(newUpdateGroup));
} else {
const outputMessage = new Table({
wordWrap: true,
chars: {
top: '',
'top-mid': '',
'top-left': '',
'top-right': '',
bottom: '',
'bottom-mid': '',
'bottom-left': '',
'bottom-right': '',
left: '',
'left-mid': '',
mid: '',
'mid-mid': '',
right: '',
'right-mid': '',
middle: ' ',
},
style: { 'padding-left': 0, 'padding-right': 0 },
});
outputMessage.push(
[chalk.dim('project'), `@${accountName}/${slug}`],
[chalk.dim('branch'), name],
[chalk.dim('runtimeVersion'), runtimeVersion],
[chalk.dim('groupID'), newUpdateGroup.group],
[chalk.dim('message'), message]
Log.log(
formatFields([
{ label: 'branch', value: name },
{ label: 'runtime version', value: runtimeVersion },
{ label: 'update group ID', value: newUpdateGroup.group },
{ label: 'message', value: message },
])
);
Log.log(outputMessage.toString());
}
}
}
+11 -13
View File
@@ -16,18 +16,13 @@ import {
ViewBranchQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
import formatFields from '../../utils/formatFields';
const PAGE_LIMIT = 10_000;
type TruncatedUpdate = Pick<Update, 'group' | 'message' | 'createdAt' | 'actor'>;
export async function viewUpdateBranchAsync({
appId,
name,
@@ -121,7 +116,6 @@ export default class BranchView extends Command {
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!name) {
@@ -168,12 +162,16 @@ export default class BranchView extends Command {
]);
}
Log.withTick(
`️Branch: ${chalk.bold(UpdateBranch.name)} on project ${chalk.bold(
fullName
)}. Branch ID: ${chalk.bold(UpdateBranch.id)}`
Log.addNewLineIfNone();
Log.log(chalk.bold('Branch:'));
Log.log(
formatFields([
{ label: 'Name', value: UpdateBranch.name },
{ label: 'ID', value: UpdateBranch.id },
])
);
Log.log(chalk.bold('Recent update groups published on this branch:'));
Log.addNewLineIfNone();
Log.log(chalk.bold('Recently published update groups:'));
Log.log(groupTable.toString());
}
}
+48 -7
View File
@@ -1,4 +1,5 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { EasJsonReader } from '@expo/eas-json';
import { flags } from '@oclif/command';
import chalk from 'chalk';
import fs from 'fs-extra';
@@ -87,7 +88,7 @@ export default class Build extends EasCommand {
return;
}
exp = (await ensureProjectConfiguredAsync(projectDir)) ?? exp;
exp = (await ensureProjectConfiguredAsync(projectDir, platform)) ?? exp;
const commandCtx = await createCommandContextAsync({
requestedPlatform: platform,
@@ -101,6 +102,7 @@ export default class Build extends EasCommand {
skipProjectConfiguration: flags['skip-project-configuration'],
waitForBuildEnd: flags.wait,
});
await buildAsync(commandCtx);
}
}
@@ -140,17 +142,29 @@ async function promptForPlatformAsync(): Promise<RequestedPlatform> {
return platform;
}
async function ensureProjectConfiguredAsync(projectDir: string): Promise<ExpoConfig | null> {
if (await fs.pathExists(path.join(projectDir, 'eas.json'))) {
async function ensureProjectConfiguredAsync(
projectDir: string,
platform: RequestedPlatform
): Promise<ExpoConfig | null> {
const platformsToConfigure = await getPlatformsToConfigureAsync(projectDir, platform);
if (!platformsToConfigure) {
return null;
}
const confirm = await confirmAsync({
message: 'This app is not set up for building with EAS. Set it up now?',
});
// Ensure the prompt is consistent with the platforms we need to configure
let message = 'This project is not configured to build with EAS. Set it up now?';
if (platformsToConfigure === RequestedPlatform.Ios) {
message = 'Your iOS project is not configured to build with EAS. Set it up now?';
} else if (platformsToConfigure === RequestedPlatform.Android) {
message = 'Your Android project is not configured to build with EAS. Set it up now?';
}
const confirm = await confirmAsync({ message });
if (confirm) {
await configureAsync({
projectDir,
platform: RequestedPlatform.All,
platform: platformsToConfigure,
});
if (await vcs.hasUncommittedChangesAsync()) {
throw new Error(
@@ -167,3 +181,30 @@ async function ensureProjectConfiguredAsync(projectDir: string): Promise<ExpoCon
);
}
}
async function getPlatformsToConfigureAsync(
projectDir: string,
platform: RequestedPlatform
): Promise<RequestedPlatform | null> {
if (!(await fs.pathExists(path.join(projectDir, 'eas.json')))) {
return platform;
}
const easConfig = await new EasJsonReader(projectDir, platform).readRawAsync();
if (platform === RequestedPlatform.All) {
if (easConfig.builds?.android && easConfig.builds?.ios) {
return null;
} else if (easConfig.builds?.ios) {
return RequestedPlatform.Android;
} else if (easConfig.builds?.android) {
return RequestedPlatform.Ios;
}
} else if (
(platform === RequestedPlatform.Android || platform === RequestedPlatform.Ios) &&
easConfig.builds?.[platform]
) {
return null;
}
return platform;
}
@@ -16,6 +16,7 @@ import {
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import formatFields from '../../utils/formatFields';
import { createUpdateBranchOnAppAsync } from '../branch/create';
async function createUpdateChannelOnAppAsync({
@@ -87,7 +88,6 @@ export default class ChannelCreate extends Command {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!channelName) {
@@ -140,10 +140,26 @@ export default class ChannelCreate extends Command {
return;
}
Log.addNewLineIfNone();
Log.withTick(
`️Created a new channel ${chalk.bold(newChannel.name)} on project ${chalk.bold(
fullName
)}. ${branchMessage} and have pointed the channel at it. You can now update your app by publishing!`
`Created a new channel on project ${chalk.bold(await getProjectFullNameAsync(exp))}`
);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: newChannel.id },
])
);
Log.addNewLineIfNone();
Log.withTick(`${branchMessage} and have pointed the channel at it.`);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: branchId },
])
);
Log.addNewLineIfNone();
Log.log(chalk.bold('You can now update your app by publishing!'));
}
}
@@ -146,13 +146,6 @@ export default class ChannelEdit extends Command {
'There is a rollout in progress. Please manage it with "channel:rollout" instead.'
);
}
const existingBranch = existingChannel.updateBranches[0];
Log.addNewLineIfNone();
Log.log(
chalk`Channel {bold ${existingChannel.name}} is currently set to branch {bold ${existingBranch.name}}`
);
Log.addNewLineIfNone();
if (!branchName) {
const validationMessage = 'branch name may not be empty.';
@@ -185,6 +178,7 @@ export default class ChannelEdit extends Command {
Log.withTick(
chalk`Channel {bold ${channel.name}} is now set to branch {bold ${branch.name}}.\n`
);
Log.addNewLineIfNone();
Log.log(
chalk`Users with builds on channel {bold ${channel.name}} will now receive the active update on {bold ${branch.name}}.`
);
@@ -1,6 +1,5 @@
import { getConfig } from '@expo/config';
import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
@@ -10,6 +9,7 @@ import {
} from '../../graphql/generated';
import Log from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import formatFields from '../../utils/formatFields';
import { logChannelDetails } from './view';
const CHANNEL_LIMIT = 10_000;
@@ -99,9 +99,14 @@ export default class ChannelList extends Command {
return;
}
Log.log(); // spacing
for (const channel of channels) {
Log.log(`Details of channel ${chalk.bold(chalk.cyan(channel.name))}:`);
Log.addNewLineIfNone();
Log.log(
formatFields([
{ label: 'Name', value: channel.name },
{ label: 'ID', value: channel.id },
])
);
logChannelDetails(channel);
}
}
+10 -8
View File
@@ -11,11 +11,7 @@ import {
GetChannelByNameForAppQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import {
FormatUpdateParameter,
@@ -23,6 +19,7 @@ import {
formatUpdate,
getPlatformsForGroup,
} from '../../update/utils';
import formatFields from '../../utils/formatFields';
export type BranchMapping = {
version: number;
@@ -220,7 +217,6 @@ export default class ChannelView extends Command {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!channelName) {
@@ -250,9 +246,15 @@ export default class ChannelView extends Command {
return;
}
Log.withTick(
chalk`Channel: {bold ${channel.name}} on project {bold ${fullName}}. Channel ID: {bold ${channel.id}}`
Log.addNewLineIfNone();
Log.log(chalk.bold('Channel:'));
Log.log(
formatFields([
{ label: 'Name', value: channel.name },
{ label: 'ID', value: channel.id },
])
);
Log.addNewLineIfNone();
Log.log(
chalk`{bold Branches, pointed at by this channel, and their most recent update group:}`
);
@@ -45,7 +45,6 @@ If you are not sure what is the UDID of the device you are looking for, run:
spinner.fail(`Couldn't find a device with the UDID ${UDID}`);
}
} catch (e) {
console.log(e);
spinner.fail(`Something went wrong and we couldn't fetch the device with UDID ${UDID}`);
throw e;
}
@@ -0,0 +1,56 @@
import { getConfig } from '@expo/config';
import { Command } from '@oclif/command';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { AppInfoQuery, AppInfoQueryVariables } from '../../graphql/generated';
import Log from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import formatFields from '../../utils/formatFields';
async function projectInfoByIdAsync(appId: string): Promise<AppInfoQuery> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppInfoQuery, AppInfoQueryVariables>(
gql`
query AppInfo($appId: String!) {
app {
byId(appId: $appId) {
id
fullName
}
}
}
`,
{ appId }
)
.toPromise()
);
return data;
}
export default class ProjectInfo extends Command {
static description = 'information about the current project';
async run() {
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const { app } = await projectInfoByIdAsync(projectId);
if (!app) {
throw new Error(`Could not find project with ID: ${projectId}`);
}
Log.addNewLineIfNone();
Log.log(
formatFields([
{ label: 'fullName', value: app.byId.fullName },
{ label: 'ID', value: projectId },
])
);
}
}
@@ -2,12 +2,12 @@ import { getConfig } from '@expo/config';
import { Command } from '@oclif/command';
import chalk from 'chalk';
import Log from '../log';
import { findProjectRootAsync, setProjectIdAsync } from '../project/projectUtils';
import Log from '../../log';
import { findProjectRootAsync, setProjectIdAsync } from '../../project/projectUtils';
export default class InitView extends Command {
static hidden = true;
static description = 'Create or link an EAS project';
export default class ProjectInit extends Command {
static description = 'create or link an EAS project';
static aliases = ['init'];
async run() {
const projectDir = await findProjectRootAsync(process.cwd());
+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'],
@@ -3,7 +3,7 @@ import { nanoid } from 'nanoid';
import ora from 'ora';
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
import { getApplicationId } from '../../../project/android/applicationId';
import { getApplicationIdAsync } from '../../../project/android/applicationId';
import { getProjectAccountName, getProjectConfigDescription } from '../../../project/projectUtils';
import { promptAsync } from '../../../prompts';
import { findAccountByName } from '../../../user/Account';
@@ -86,7 +86,7 @@ export async function promptUserAndCopyLegacyCredentialsAsync(
spinner.succeed('Credentials copied to EAS.');
}
export function getAppLookupParamsFromContext(ctx: Context): AppLookupParams {
export async function getAppLookupParamsFromContextAsync(ctx: Context): Promise<AppLookupParams> {
ctx.ensureProjectContext();
const projectName = ctx.exp.slug;
const accountName = getProjectAccountName(ctx.exp, ctx.user);
@@ -95,7 +95,7 @@ export function getAppLookupParamsFromContext(ctx: Context): AppLookupParams {
throw new Error(`You do not have access to account: ${accountName}`);
}
const androidApplicationIdentifier = getApplicationId(ctx.projectDir, ctx.exp);
const androidApplicationIdentifier = await getApplicationIdAsync(ctx.projectDir, ctx.exp);
if (!androidApplicationIdentifier) {
throw new Error(
`android.package needs to be defined in your ${getProjectConfigDescription(
@@ -1,14 +1,14 @@
import { testLegacyAndroidFcmFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { AssignFcm } from '../AssignFcm';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
describe(AssignFcm, () => {
it('assigns an fcm api key in Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: false,
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const assignFcmAction = new AssignFcm(appLookupParams);
await assignFcmAction.runAsync(ctx, testLegacyAndroidFcmFragment);
@@ -22,7 +22,7 @@ describe(AssignFcm, () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const assignFcmAction = new AssignFcm(appLookupParams);
// dont fail if users are running in non-interactive mode
@@ -10,24 +10,13 @@ import {
import { createCtxMock } from '../../../__tests__/fixtures-context';
import {
canCopyLegacyCredentialsAsync,
getAppLookupParamsFromContext,
getAppLookupParamsFromContextAsync,
promptUserAndCopyLegacyCredentialsAsync,
} from '../BuildCredentialsUtils';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
describe('BuildCredentialsUtils', () => {
describe(canCopyLegacyCredentialsAsync, () => {
it('returns true if the user has legacy credentials and no modern ones', async () => {
@@ -41,7 +30,7 @@ describe('BuildCredentialsUtils', () => {
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
expect(canCopyLegacyCredentials).toBe(true);
});
@@ -58,7 +47,7 @@ describe('BuildCredentialsUtils', () => {
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
expect(canCopyLegacyCredentials).toBe(false);
});
@@ -71,7 +60,7 @@ describe('BuildCredentialsUtils', () => {
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(() => null),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
expect(canCopyLegacyCredentials).toBe(false);
});
@@ -86,7 +75,7 @@ describe('BuildCredentialsUtils', () => {
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
expect(canCopyLegacyCredentials).toBe(true);
});
@@ -109,7 +98,7 @@ describe('BuildCredentialsUtils', () => {
createFcmAsync: jest.fn(() => testLegacyAndroidFcmFragment),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
await promptUserAndCopyLegacyCredentialsAsync(ctx, appLookupParams);
expect(
@@ -131,7 +120,7 @@ describe('BuildCredentialsUtils', () => {
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
await expect(
promptUserAndCopyLegacyCredentialsAsync(ctx, appLookupParams)
).rejects.toThrowError();
@@ -1,6 +1,6 @@
import { promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateFcm } from '../CreateFcm';
jest.mock('../../../../prompts');
@@ -9,7 +9,7 @@ jest.mock('../../../../prompts');
describe(CreateFcm, () => {
it('creates an fcm api key in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createFcmAction = new CreateFcm(appLookupParams.account);
await createFcmAction.runAsync(ctx);
@@ -18,7 +18,7 @@ describe(CreateFcm, () => {
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createFcmAction = new CreateFcm(appLookupParams.account);
// fail if users are running in non-interactive mode
@@ -1,6 +1,6 @@
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateKeystore } from '../CreateKeystore';
jest.mock('../../../../prompts');
@@ -9,7 +9,7 @@ jest.mock('../../../../prompts');
describe('CreateKeystore', () => {
it('creates a keystore in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
await createKeystoreAction.runAsync(ctx);
@@ -18,7 +18,7 @@ describe('CreateKeystore', () => {
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
// fail if users are running in non-interactive mode
@@ -2,28 +2,18 @@ import fs from 'fs-extra';
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { DownloadKeystore } from '../DownloadKeystore';
jest.mock('fs-extra');
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
describe(DownloadKeystore, () => {
it('downloads a keystore', async () => {
const fsWriteFileSpy = jest.spyOn(fs, 'writeFile');
const ctx = createCtxMock({
nonInteractive: false,
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const downloadKeystoreAction = new DownloadKeystore({ app: appLookupParams });
await downloadKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment);
expect(fsWriteFileSpy as any).toHaveBeenCalledTimes(1);
@@ -36,7 +26,7 @@ describe(DownloadKeystore, () => {
});
it('works in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const downloadKeystoreAction = new DownloadKeystore({ app: appLookupParams });
await expect(
downloadKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment)
@@ -4,22 +4,12 @@ import {
testAndroidAppCredentialsFragment,
} from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { RemoveFcm } from '../RemoveFcm';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
describe(RemoveFcm, () => {
it('removes an FCM Api Key', async () => {
const ctx = createCtxMock({
@@ -31,14 +21,14 @@ describe(RemoveFcm, () => {
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const removeFcmApiKeyAction = new RemoveFcm(appLookupParams);
await removeFcmApiKeyAction.runAsync(ctx);
expect(ctx.android.deleteFcmAsync as any).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const removeFcmApiKeyAction = new RemoveFcm(appLookupParams);
await expect(removeFcmApiKeyAction.runAsync(ctx)).rejects.toThrowError();
});
@@ -1,36 +1,26 @@
import { confirmAsync } from '../../../../prompts';
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { RemoveKeystore } from '../RemoveKeystore';
jest.mock('fs-extra');
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
describe(RemoveKeystore, () => {
it('removes a keystore', async () => {
const ctx = createCtxMock({
nonInteractive: false,
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const removeKeystoreAction = new RemoveKeystore(appLookupParams);
await removeKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment);
expect(ctx.android.deleteKeystoreAsync as any).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const removeKeystoreAction = new RemoveKeystore(appLookupParams);
await expect(
removeKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment)
@@ -6,23 +6,12 @@ import {
} from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetupBuildCredentials } from '../SetupBuildCredentials';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
describe('SetupBuildCredentials', () => {
it('skips setup when there are prior credentials', async () => {
const ctx = createCtxMock({
@@ -34,7 +23,7 @@ describe('SetupBuildCredentials', () => {
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
await setupBuildCredentialsAction.runAsync(ctx);
@@ -50,7 +39,7 @@ describe('SetupBuildCredentials', () => {
createKeystoreAsync: jest.fn(() => testJksAndroidKeystoreFragment),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
await setupBuildCredentialsAction.runAsync(ctx);
@@ -65,7 +54,7 @@ describe('SetupBuildCredentials', () => {
...getNewAndroidApiMock(),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
@@ -12,7 +12,7 @@ import {
SelectAndroidBuildCredentials,
SelectAndroidBuildCredentialsResultType,
} from '../../../manager/SelectAndroidBuildCredentials';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetupBuildCredentialsFromCredentialsJson } from '../SetupBuildCredentialsFromCredentialsJson';
jest.mock('fs');
@@ -21,16 +21,6 @@ jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
jest.mock('../../../manager/SelectAndroidBuildCredentials');
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
beforeEach(() => {
vol.reset();
});
@@ -67,7 +57,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
}),
'keystore.jks': 'some-binary-content',
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
appLookupParams
);
@@ -117,7 +107,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
}),
'keystore.jks': 'some-binary-content',
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
appLookupParams
);
@@ -140,7 +130,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
appLookupParams
);
@@ -3,7 +3,7 @@ import mockdate from 'mockdate';
import Log from '../../../../log';
import { testAndroidAppCredentialsFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } from '../../actions/BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../../actions/BuildCredentialsUtils';
import { displayAndroidAppCredentials } from '../printCredentials';
jest.mock('../../../../log');
@@ -14,7 +14,7 @@ mockdate.set(new Date('4/20/2021'));
describe('print credentials', () => {
it('prints the AndroidAppCredentials fragment', async () => {
const ctx = createCtxMock();
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
displayAndroidAppCredentials({
appLookupParams,
appCredentials: testAndroidAppCredentialsFragment,
@@ -22,22 +22,6 @@ import { updateAndroidCredentialsAsync, updateIosCredentialsAsync } from '../upd
jest.mock('fs');
jest.mock('prompts');
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
const originalConsoleError = console.error;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
console.error = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
console.error = originalConsoleError;
});
beforeEach(() => {
vol.reset();
asMock(prompts).mockReset();
@@ -29,20 +29,6 @@ jest.mock('../validators/validateProvisioningProfile', () => ({
}));
jest.mock('../../../project/ios/bundleIdentifier');
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.error = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.error = originalConsoleError;
console.warn = originalConsoleWarn;
});
beforeEach(() => {
vol.reset();
});
@@ -12,14 +12,6 @@ import { RemoveDistributionCertificate } from '../RemoveDistributionCertificate'
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
});
describe('RemoveDistributionCertificate', () => {
it('deletes the distribution certificate on Expo and Apple servers when there are no App Dependencies in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
@@ -8,14 +8,6 @@ import { RemovePushKey } from '../RemovePushKey';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
});
describe(RemovePushKey, () => {
it('deletes the push key on Expo and Apple servers in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
@@ -13,14 +13,6 @@ jest.mock('../SetupAdhocProvisioningProfile');
jest.mock('../SetupProvisioningProfile');
jest.mock('../BuildCredentialsUtils', () => ({ getAllBuildCredentialsAsync: jest.fn() }));
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
});
beforeEach(() => {
asMock(promptAsync).mockReset();
@@ -9,14 +9,6 @@ import { SetupPushKey } from '../SetupPushKey';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
});
describe(SetupPushKey, () => {
it('skips setting up a Push Key if it is already configured', async () => {
const ctx = createCtxMock({
@@ -20,17 +20,6 @@ import { SetupTargetBuildCredentialsFromCredentialsJson } from '../SetupTargetBu
jest.mock('../../../../prompts');
jest.mock('../../../credentialsJson/read');
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
beforeEach(() => {
asMock(confirmAsync).mockReset();
(confirmAsync as jest.Mock).mockImplementation(() => true);
@@ -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));
}
@@ -1,12 +1,17 @@
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import assert from 'assert';
import Log, { learnMore } from '../../log';
import {
getProjectAccountName,
promptToCreateProjectIfNotExistsAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { findAccountByName } from '../../user/Account';
import { ensureActorHasUsername } from '../../user/actions';
import { AssignFcm } from '../android/actions/AssignFcm';
import {
canCopyLegacyCredentialsAsync,
getAppLookupParamsFromContext,
getAppLookupParamsFromContextAsync,
promptUserAndCopyLegacyCredentialsAsync,
} from '../android/actions/BuildCredentialsUtils';
import { CreateFcm } from '../android/actions/CreateFcm';
@@ -147,7 +152,13 @@ export class ManageAndroid {
throw new Error(`You do not have access to account: ${accountName}`);
}
if (ctx.hasProjectContext) {
const appLookupParams = getAppLookupParamsFromContext(ctx);
const maybeProjectId = await promptToCreateProjectIfNotExistsAsync(ctx.exp);
if (!maybeProjectId) {
throw new Error(
'Your project must be registered with EAS in order to use the credentials manager.'
);
}
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(
appLookupParams
);
@@ -196,61 +207,74 @@ export class ManageAndroid {
return await this.callingAction.runAsync(ctx);
}
}
const appLookupParams = getAppLookupParamsFromContext(ctx);
if (chosenAction === ActionType.CreateKeystore) {
const selectBuildCredentialsResult = await new SelectAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
const keystore = await new CreateKeystore(appLookupParams.account).runAsync(ctx);
if (
selectBuildCredentialsResult.resultType ===
SelectAndroidBuildCredentialsResultType.CREATE_REQUEST
) {
await ctx.android.createAndroidAppBuildCredentialsAsync(appLookupParams, {
...selectBuildCredentialsResult.result,
androidKeystoreId: keystore.id,
});
} else {
await ctx.android.updateAndroidAppBuildCredentialsAsync(
selectBuildCredentialsResult.result,
{
androidKeystoreId: keystore.id,
}
);
}
} else if (chosenAction === ActionType.DownloadKeystore) {
const buildCredentials = await new SelectExistingAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
if (buildCredentials) {
await new DownloadKeystore({ app: appLookupParams }).runAsync(ctx, buildCredentials);
}
} else if (chosenAction === ActionType.RemoveKeystore) {
const buildCredentials = await new SelectExistingAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
if (buildCredentials) {
await new RemoveKeystore(appLookupParams).runAsync(ctx, buildCredentials);
}
} else if (chosenAction === ActionType.CreateFcm) {
const fcm = await new CreateFcm(appLookupParams.account).runAsync(ctx);
await new AssignFcm(appLookupParams).runAsync(ctx, fcm);
} else if (chosenAction === ActionType.RemoveFcm) {
await new RemoveFcm(appLookupParams).runAsync(ctx);
} else if (chosenAction === ActionType.UpdateCredentialsJson) {
const buildCredentials = await new SelectExistingAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
if (buildCredentials) {
await new UpdateCredentialsJson().runAsync(ctx, buildCredentials);
}
} else if (chosenAction === ActionType.SetupBuildCredentialsFromCredentialsJson) {
await new SetupBuildCredentialsFromCredentialsJson(appLookupParams).runAsync(ctx);
}
await this.runProjectSpecificActionAsync(ctx, chosenAction);
} catch (err) {
Log.error(err);
if (process.env.DEBUG) {
throw err; // breaks out of the loop so we can get stack trace
}
Log.error(`Cryptic error? ${learnMore('https://expo.fyi/cryptic-error-eas')}`);
}
await new PressAnyKeyToContinue().runAsync();
}
}
private async runProjectSpecificActionAsync(ctx: Context, action: ActionType): Promise<void> {
assert(
ctx.hasProjectContext,
'You must be in your project directory in order to perform this action'
);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
if (action === ActionType.CreateKeystore) {
const selectBuildCredentialsResult = await new SelectAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
const keystore = await new CreateKeystore(appLookupParams.account).runAsync(ctx);
if (
selectBuildCredentialsResult.resultType ===
SelectAndroidBuildCredentialsResultType.CREATE_REQUEST
) {
await ctx.android.createAndroidAppBuildCredentialsAsync(appLookupParams, {
...selectBuildCredentialsResult.result,
androidKeystoreId: keystore.id,
});
} else {
await ctx.android.updateAndroidAppBuildCredentialsAsync(
selectBuildCredentialsResult.result,
{
androidKeystoreId: keystore.id,
}
);
}
} else if (action === ActionType.DownloadKeystore) {
const buildCredentials = await new SelectExistingAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
if (buildCredentials) {
await new DownloadKeystore({ app: appLookupParams }).runAsync(ctx, buildCredentials);
}
} else if (action === ActionType.RemoveKeystore) {
const buildCredentials = await new SelectExistingAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
if (buildCredentials) {
await new RemoveKeystore(appLookupParams).runAsync(ctx, buildCredentials);
}
} else if (action === ActionType.CreateFcm) {
const fcm = await new CreateFcm(appLookupParams.account).runAsync(ctx);
await new AssignFcm(appLookupParams).runAsync(ctx, fcm);
} else if (action === ActionType.RemoveFcm) {
await new RemoveFcm(appLookupParams).runAsync(ctx);
} else if (action === ActionType.UpdateCredentialsJson) {
const buildCredentials = await new SelectExistingAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
if (buildCredentials) {
await new UpdateCredentialsJson().runAsync(ctx, buildCredentials);
}
} else if (action === ActionType.SetupBuildCredentialsFromCredentialsJson) {
await new SetupBuildCredentialsFromCredentialsJson(appLookupParams).runAsync(ctx);
}
}
}
@@ -7,10 +7,13 @@ import {
IosAppBuildCredentialsFragment,
IosDistributionType as IosDistributionTypeGraphql,
} from '../../graphql/generated';
import Log from '../../log';
import Log, { learnMore } from '../../log';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { resolveTargetsAsync } from '../../project/ios/target';
import { getProjectAccountName } from '../../project/projectUtils';
import {
getProjectAccountName,
promptToCreateProjectIfNotExistsAsync,
} from '../../project/projectUtils';
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
import { Account, findAccountByName } from '../../user/Account';
import { ensureActorHasUsername } from '../../user/actions';
@@ -235,11 +238,15 @@ export class ManageIos {
return await this.callingAction.runAsync(ctx);
}
} else if (actionInfo.scope === Scope.Project) {
assert(
ctx.hasProjectContext,
'You must be in your project directory in order to perform this action'
);
await this.runProjectSpecificActionAsync(
ctx,
nullthrows(app),
nullthrows(targets),
nullthrows(buildProfile),
nullthrows(app, 'app must be defined in project context'),
nullthrows(targets, 'targets must be defined in project context'),
nullthrows(buildProfile, 'buildProfile must be defined in project context'),
chosenAction
);
} else if (actionInfo.scope === Scope.Account) {
@@ -249,6 +256,10 @@ export class ManageIos {
}
} catch (err) {
Log.error(err);
if (process.env.DEBUG) {
throw err; // breaks out of the loop so we can get stack trace
}
Log.error(`Cryptic error? ${learnMore('https://expo.fyi/cryptic-error-eas')}`);
}
await new PressAnyKeyToContinue().runAsync();
}
@@ -266,6 +277,13 @@ export class ManageIos {
};
}
const maybeProjectId = await promptToCreateProjectIfNotExistsAsync(ctx.exp);
if (!maybeProjectId) {
throw new Error(
'Your project must be registered with EAS in order to use the credentials manager.'
);
}
const app = { account, projectName: ctx.exp.slug };
const easJsonReader = new EasJsonReader(ctx.projectDir, 'ios');
const easConfig = await new SelectBuildProfileFromEasJson(easJsonReader).runAsync(ctx);
@@ -4,7 +4,7 @@ import {
testAndroidBuildCredentialsFragment,
} from '../../__tests__/fixtures-android';
import { createCtxMock } from '../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } from '../../android/actions/BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../../android/actions/BuildCredentialsUtils';
import {
SelectAndroidBuildCredentials,
SelectAndroidBuildCredentialsResultType,
@@ -28,7 +28,7 @@ describe(SelectAndroidBuildCredentials, () => {
getAndroidAppBuildCredentialsListAsync: jest.fn(() => []),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const selectAndroidBuildCredentialsAction = new SelectAndroidBuildCredentials(appLookupParams);
const buildCredentialsMetadataInput = await selectAndroidBuildCredentialsAction.runAsync(ctx);
expect(buildCredentialsMetadataInput).toMatchObject({
@@ -50,7 +50,7 @@ describe(SelectAndroidBuildCredentials, () => {
]),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const selectAndroidBuildCredentialsAction = new SelectAndroidBuildCredentials(appLookupParams);
const buildCredentialsMetadataInput = await selectAndroidBuildCredentialsAction.runAsync(ctx);
expect(buildCredentialsMetadataInput).toMatchObject({
@@ -70,7 +70,7 @@ describe(SelectAndroidBuildCredentials, () => {
getAndroidAppBuildCredentialsListAsync: jest.fn(() => []),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const selectAndroidBuildCredentialsAction = new SelectAndroidBuildCredentials(appLookupParams);
const buildCredentialsMetadataInput = await selectAndroidBuildCredentialsAction.runAsync(ctx);
expect(buildCredentialsMetadataInput).toMatchObject({
@@ -90,7 +90,7 @@ describe(SelectAndroidBuildCredentials, () => {
]),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const selectAndroidBuildCredentialsAction = new SelectAndroidBuildCredentials(appLookupParams);
const buildCredentialsMetadataInput = await selectAndroidBuildCredentialsAction.runAsync(ctx);
expect(buildCredentialsMetadataInput).toMatchObject({
@@ -1,6 +1,7 @@
import prompts from 'prompts';
import { asMock } from '../../__tests__/utils';
import Log from '../../log';
import { Actor } from '../../user/User';
import { AccountResolver } from '../manager';
@@ -69,15 +70,11 @@ describe(AccountResolver, () => {
accounts: [user.accounts[0]],
};
const originalConsoleWarn = console.warn;
console.warn = jest.fn();
jest.spyOn(Log, 'warn');
const resolver = new AccountResolver(exp, userWithAccessToProjectAccount);
const account = await resolver.resolveAccountAsync();
expect(account).toEqual(user.accounts[0]);
expect(console.warn).toBeCalledWith(expect.stringMatching(/doesn't have access to the/));
console.warn = originalConsoleWarn;
expect(Log.warn).toBeCalledWith(expect.stringMatching(/doesn't have access to the/));
});
});
@@ -6,14 +6,6 @@ import DeviceCreateAction, { RegistrationMethod } from '../action';
import { runInputMethodAsync } from '../inputMethod';
import { runRegistrationUrlMethodAsync } from '../registrationUrlMethod';
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
});
jest.mock('prompts');
jest.mock('../registrationUrlMethod');
jest.mock('../inputMethod');
@@ -8,17 +8,6 @@ import { runInputMethodAsync } from '../inputMethod';
jest.mock('prompts');
jest.mock('../../../../credentials/ios/api/graphql/mutations/AppleDeviceMutation');
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
beforeEach(() => {
asMock(prompts).mockReset();
asMock(prompts).mockImplementation(() => {
@@ -14,7 +14,7 @@ export async function runRegistrationUrlMethodAsync(
): Promise<void> {
const registrationURL = await generateDeviceRegistrationURLAsync(accountId, appleTeam);
Log.newLine();
qrcodeTerminal.generate(registrationURL, code => console.log(`${indentString(code, 2)}\n`));
qrcodeTerminal.generate(registrationURL, code => Log.log(`${indentString(code, 2)}\n`));
Log.log(
'Open the following link on your iOS devices (or scan the QR code) and follow the instructions to install the development profile:'
);
+275 -205
View File
@@ -58,13 +58,11 @@ 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;
/** Search for Snacks */
search: Array<SearchResult>;
/** @deprecated Use 'search' root field. */
searchUsersAndApps: Array<Maybe<SearchResult>>;
snack: SnackQuery;
submissions: SubmissionQuery;
/** Top-level query object for querying UserInvitationPublicData publicly. */
@@ -113,22 +111,6 @@ export type RootQueryAllPublicAppsArgs = {
};
export type RootQuerySearchArgs = {
type: SearchType;
query: Scalars['String'];
offset: Scalars['Int'];
limit: Scalars['Int'];
};
export type RootQuerySearchUsersAndAppsArgs = {
type: SearchType;
query: Scalars['String'];
offset: Scalars['Int'];
limit: Scalars['Int'];
};
export type RootQueryUserByUserIdArgs = {
userId: Scalars['String'];
};
@@ -208,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 */
@@ -299,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.
@@ -431,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>;
@@ -447,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>;
@@ -558,6 +559,7 @@ export type AppUpdateBranchByNameArgs = {
export type AppActivityTimelineProjectActivitiesArgs = {
limit: Scalars['Int'];
createdBefore?: Maybe<Scalars['DateTime']>;
filterTypes?: Maybe<Array<ActivityTimelineProjectActivityType>>;
};
@@ -637,13 +639,16 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
appVersion?: Maybe<Scalars['String']>;
appBuildVersion?: Maybe<Scalars['String']>;
sdkVersion?: Maybe<Scalars['String']>;
runtimeVersion?: Maybe<Scalars['String']>;
releaseChannel?: Maybe<Scalars['String']>;
channel?: Maybe<Scalars['String']>;
metrics?: Maybe<BuildMetrics>;
distribution?: Maybe<DistributionType>;
iosEnterpriseProvisioning?: Maybe<BuildIosEnterpriseProvisioning>;
buildProfile?: Maybe<Scalars['String']>;
gitCommitHash?: Maybe<Scalars['String']>;
error?: Maybe<BuildError>;
submissions: Array<Submission>;
};
export type BuildOrBuildJob = {
@@ -685,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.
@@ -722,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']>>;
@@ -799,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';
@@ -818,6 +840,11 @@ export enum DistributionType {
Simulator = 'SIMULATOR'
}
export enum BuildIosEnterpriseProvisioning {
Adhoc = 'ADHOC',
Universal = 'UNIVERSAL'
}
export type BuildError = {
__typename?: 'BuildError';
errorCode: Scalars['String'];
@@ -825,6 +852,72 @@ export type BuildError = {
docsUrl?: Maybe<Scalars['String']>;
};
/** Represents an EAS Submission */
export type Submission = ActivityTimelineProjectActivity & {
__typename?: 'Submission';
id: Scalars['ID'];
actor?: Maybe<Actor>;
activityTimestamp: Scalars['DateTime'];
app?: Maybe<App>;
initiatingActor?: Maybe<Actor>;
submittedBuild?: Maybe<Build>;
platform: AppPlatform;
status: SubmissionStatus;
androidConfig?: Maybe<AndroidSubmissionConfig>;
iosConfig?: Maybe<IosSubmissionConfig>;
logsUrl?: Maybe<Scalars['String']>;
error?: Maybe<SubmissionError>;
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
};
export enum SubmissionStatus {
InQueue = 'IN_QUEUE',
InProgress = 'IN_PROGRESS',
Finished = 'FINISHED',
Errored = 'ERRORED'
}
export type AndroidSubmissionConfig = {
__typename?: 'AndroidSubmissionConfig';
applicationIdentifier: Scalars['String'];
archiveType: SubmissionAndroidArchiveType;
track: SubmissionAndroidTrack;
releaseStatus?: Maybe<SubmissionAndroidReleaseStatus>;
};
export enum SubmissionAndroidArchiveType {
Apk = 'APK',
Aab = 'AAB'
}
export enum SubmissionAndroidTrack {
Production = 'PRODUCTION',
Internal = 'INTERNAL',
Alpha = 'ALPHA',
Beta = 'BETA'
}
export enum SubmissionAndroidReleaseStatus {
Draft = 'DRAFT',
InProgress = 'IN_PROGRESS',
Halted = 'HALTED',
Completed = 'COMPLETED'
}
export type IosSubmissionConfig = {
__typename?: 'IosSubmissionConfig';
ascAppIdentifier: Scalars['String'];
appleIdUsername: Scalars['String'];
appleAppSpecificPasswordId?: Maybe<Scalars['String']>;
};
export type SubmissionError = {
__typename?: 'SubmissionError';
errorCode?: Maybe<Scalars['String']>;
message?: Maybe<Scalars['String']>;
};
/** Represents an Standalone App build job */
export type BuildJob = ActivityTimelineProjectActivity & BuildOrBuildJob & {
__typename?: 'BuildJob';
@@ -891,71 +984,6 @@ export type SubmissionFilter = {
status?: Maybe<SubmissionStatus>;
};
export enum SubmissionStatus {
InQueue = 'IN_QUEUE',
InProgress = 'IN_PROGRESS',
Finished = 'FINISHED',
Errored = 'ERRORED'
}
/** Represents an EAS Submission */
export type Submission = ActivityTimelineProjectActivity & {
__typename?: 'Submission';
id: Scalars['ID'];
actor?: Maybe<Actor>;
activityTimestamp: Scalars['DateTime'];
app?: Maybe<App>;
initiatingActor?: Maybe<Actor>;
platform: AppPlatform;
status: SubmissionStatus;
androidConfig?: Maybe<AndroidSubmissionConfig>;
iosConfig?: Maybe<IosSubmissionConfig>;
logsUrl?: Maybe<Scalars['String']>;
error?: Maybe<SubmissionError>;
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
};
export type AndroidSubmissionConfig = {
__typename?: 'AndroidSubmissionConfig';
applicationIdentifier: Scalars['String'];
archiveType: SubmissionAndroidArchiveType;
track: SubmissionAndroidTrack;
releaseStatus?: Maybe<SubmissionAndroidReleaseStatus>;
};
export enum SubmissionAndroidArchiveType {
Apk = 'APK',
Aab = 'AAB'
}
export enum SubmissionAndroidTrack {
Production = 'PRODUCTION',
Internal = 'INTERNAL',
Alpha = 'ALPHA',
Beta = 'BETA'
}
export enum SubmissionAndroidReleaseStatus {
Draft = 'DRAFT',
InProgress = 'IN_PROGRESS',
Halted = 'HALTED',
Completed = 'COMPLETED'
}
export type IosSubmissionConfig = {
__typename?: 'IosSubmissionConfig';
ascAppIdentifier: Scalars['String'];
appleIdUsername: Scalars['String'];
appleAppSpecificPasswordId?: Maybe<Scalars['String']>;
};
export type SubmissionError = {
__typename?: 'SubmissionError';
errorCode?: Maybe<Scalars['String']>;
message?: Maybe<Scalars['String']>;
};
export type IosAppCredentialsFilter = {
appleAppIdentifierId?: Maybe<Scalars['String']>;
};
@@ -1513,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>;
};
@@ -1529,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;
@@ -1550,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 */
@@ -1590,19 +1655,6 @@ export type ProjectQueryByPathsArgs = {
paths?: Maybe<Array<Maybe<Scalars['String']>>>;
};
export enum SearchType {
All = 'ALL',
Users = 'USERS',
Apps = 'APPS',
Snacks = 'SNACKS'
}
/** Represents a search result for an app */
export type SearchResult = {
id: Scalars['ID'];
rank?: Maybe<Scalars['Int']>;
};
export type SnackQuery = {
__typename?: 'SnackQuery';
/**
@@ -1742,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 */
@@ -2352,14 +2405,24 @@ export type BuildMutation = {
* @deprecated Use cancelBuild instead
*/
cancel: Build;
/** Create an Android generic build */
/**
* Create an Android generic build
* @deprecated Use createAndroidBuild instead
*/
createAndroidGenericBuild: CreateBuildResult;
/** Create an Android managed build */
/**
* Create an Android managed build
* @deprecated Use createAndroidBuild instead
*/
createAndroidManagedBuild: CreateBuildResult;
/** Create an Android build */
createAndroidBuild: CreateBuildResult;
/** Create an iOS generic build */
createIosGenericBuild: CreateBuildResult;
/** Create an iOS managed build */
createIosManagedBuild: CreateBuildResult;
/** Create an iOS build */
createIosBuild: CreateBuildResult;
};
@@ -2387,6 +2450,13 @@ export type BuildMutationCreateAndroidManagedBuildArgs = {
};
export type BuildMutationCreateAndroidBuildArgs = {
appId: Scalars['ID'];
job: AndroidJobInput;
metadata?: Maybe<BuildMetadataInput>;
};
export type BuildMutationCreateIosGenericBuildArgs = {
appId: Scalars['ID'];
job: IosGenericJobInput;
@@ -2400,6 +2470,13 @@ export type BuildMutationCreateIosManagedBuildArgs = {
metadata?: Maybe<BuildMetadataInput>;
};
export type BuildMutationCreateIosBuildArgs = {
appId: Scalars['ID'];
job: IosJobInput;
metadata?: Maybe<BuildMetadataInput>;
};
export type AndroidGenericJobInput = {
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
@@ -2468,9 +2545,11 @@ export type BuildMetadataInput = {
workflow?: Maybe<BuildWorkflow>;
credentialsSource?: Maybe<BuildCredentialsSource>;
sdkVersion?: Maybe<Scalars['String']>;
runtimeVersion?: Maybe<Scalars['String']>;
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']>;
@@ -2523,6 +2602,27 @@ export enum AndroidManagedBuildType {
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export type AndroidJobInput = {
type: BuildWorkflow;
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: Maybe<Scalars['String']>;
updates?: Maybe<BuildUpdatesInput>;
secrets?: Maybe<AndroidJobSecretsInput>;
builderEnvironment?: Maybe<AndroidBuilderEnvironmentInput>;
cache?: Maybe<BuildCacheInput>;
gradleCommand?: Maybe<Scalars['String']>;
artifactPath?: Maybe<Scalars['String']>;
buildType?: Maybe<AndroidBuildType>;
username?: Maybe<Scalars['String']>;
};
export enum AndroidBuildType {
Apk = 'APK',
AppBundle = 'APP_BUNDLE',
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export type IosGenericJobInput = {
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
@@ -2588,6 +2688,28 @@ export enum IosManagedBuildType {
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export type IosJobInput = {
type: BuildWorkflow;
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: Maybe<Scalars['String']>;
updates?: Maybe<BuildUpdatesInput>;
distribution?: Maybe<DistributionType>;
secrets?: Maybe<IosJobSecretsInput>;
builderEnvironment?: Maybe<IosBuilderEnvironmentInput>;
cache?: Maybe<BuildCacheInput>;
scheme?: Maybe<Scalars['String']>;
buildConfiguration?: Maybe<Scalars['String']>;
artifactPath?: Maybe<Scalars['String']>;
buildType?: Maybe<IosBuildType>;
username?: Maybe<Scalars['String']>;
};
export enum IosBuildType {
Release = 'RELEASE',
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export type IosAppBuildCredentialsMutation = {
__typename?: 'IosAppBuildCredentialsMutation';
/** Create a set of build credentials for an iOS app */
@@ -2740,6 +2862,7 @@ export type CreateSubmissionInput = {
appId: Scalars['ID'];
platform: AppPlatform;
config: Scalars['JSONObject'];
submittedBuildId?: Maybe<Scalars['ID']>;
};
export type CreateSubmissionResult = {
@@ -2751,6 +2874,7 @@ export type CreateSubmissionResult = {
export type CreateIosSubmissionInput = {
appId: Scalars['ID'];
config: NewIosSubmissionConfig;
submittedBuildId?: Maybe<Scalars['ID']>;
};
export type NewIosSubmissionConfig = {
@@ -2832,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>;
};
@@ -2884,6 +3008,7 @@ export type UpdateInfoGroup = {
export type PartialManifest = {
launchAsset: PartialManifestAsset;
assets: Array<Maybe<PartialManifestAsset>>;
extra?: Maybe<Scalars['JSONObject']>;
};
export type PartialManifestAsset = {
@@ -3167,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>;
@@ -3283,44 +3422,6 @@ export type DeleteWebhookResult = {
id: Scalars['ID'];
};
export type BaseSearchResult = SearchResult & {
__typename?: 'BaseSearchResult';
/** @deprecated Use SearchResult instead */
id: Scalars['ID'];
/** @deprecated Use SearchResult instead */
rank?: Maybe<Scalars['Int']>;
};
/** Represents a search result for an app */
export type AppSearchResult = SearchResult & {
__typename?: 'AppSearchResult';
/** @deprecated Field no longer supported */
id: Scalars['ID'];
/** @deprecated Field no longer supported */
rank?: Maybe<Scalars['Int']>;
/** @deprecated Field no longer supported */
app: App;
};
/** Represents a search result for a user */
export type UserSearchResult = SearchResult & {
__typename?: 'UserSearchResult';
/** @deprecated Field no longer supported */
id: Scalars['ID'];
/** @deprecated Field no longer supported */
rank?: Maybe<Scalars['Int']>;
/** @deprecated Field no longer supported */
user: User;
};
/** Represents a search result for a snack */
export type SnackSearchResult = SearchResult & {
__typename?: 'SnackSearchResult';
id: Scalars['ID'];
rank?: Maybe<Scalars['Int']>;
snack: Snack;
};
export enum CacheControlScope {
Public = 'PUBLIC',
Private = 'PRIVATE'
@@ -3624,6 +3725,22 @@ export type GetChannelByNameForAppQuery = (
)> }
);
export type AppInfoQueryVariables = Exact<{
appId: Scalars['String'];
}>;
export type AppInfoQuery = (
{ __typename?: 'RootQuery' }
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
& Pick<App, 'id' | 'fullName'>
) }
)> }
);
export type DeleteUpdateGroupMutationVariables = Exact<{
group: Scalars['ID'];
}>;
@@ -4497,18 +4614,18 @@ export type CreateAppMutation = (
)> }
);
export type CreateAndroidGenericBuildMutationVariables = Exact<{
export type CreateAndroidBuildMutationVariables = Exact<{
appId: Scalars['ID'];
job: AndroidGenericJobInput;
job: AndroidJobInput;
metadata?: Maybe<BuildMetadataInput>;
}>;
export type CreateAndroidGenericBuildMutation = (
export type CreateAndroidBuildMutation = (
{ __typename?: 'RootMutation' }
& { build?: Maybe<(
{ __typename?: 'BuildMutation' }
& { createAndroidGenericBuild: (
& { createAndroidBuild: (
{ __typename?: 'CreateBuildResult' }
& { build: (
{ __typename?: 'Build' }
@@ -4521,66 +4638,18 @@ export type CreateAndroidGenericBuildMutation = (
)> }
);
export type CreateAndroidManagedBuildMutationVariables = Exact<{
export type CreateIosBuildMutationVariables = Exact<{
appId: Scalars['ID'];
job: AndroidManagedJobInput;
job: IosJobInput;
metadata?: Maybe<BuildMetadataInput>;
}>;
export type CreateAndroidManagedBuildMutation = (
export type CreateIosBuildMutation = (
{ __typename?: 'RootMutation' }
& { build?: Maybe<(
{ __typename?: 'BuildMutation' }
& { createAndroidManagedBuild: (
{ __typename?: 'CreateBuildResult' }
& { build: (
{ __typename?: 'Build' }
& Pick<Build, 'id'>
), deprecationInfo?: Maybe<(
{ __typename?: 'EASBuildDeprecationInfo' }
& Pick<EasBuildDeprecationInfo, 'type' | 'message'>
)> }
) }
)> }
);
export type CreateIosGenericBuildMutationVariables = Exact<{
appId: Scalars['ID'];
job: IosGenericJobInput;
metadata?: Maybe<BuildMetadataInput>;
}>;
export type CreateIosGenericBuildMutation = (
{ __typename?: 'RootMutation' }
& { build?: Maybe<(
{ __typename?: 'BuildMutation' }
& { createIosGenericBuild: (
{ __typename?: 'CreateBuildResult' }
& { build: (
{ __typename?: 'Build' }
& Pick<Build, 'id'>
), deprecationInfo?: Maybe<(
{ __typename?: 'EASBuildDeprecationInfo' }
& Pick<EasBuildDeprecationInfo, 'type' | 'message'>
)> }
) }
)> }
);
export type CreateIosManagedBuildMutationVariables = Exact<{
appId: Scalars['ID'];
job: IosManagedJobInput;
metadata?: Maybe<BuildMetadataInput>;
}>;
export type CreateIosManagedBuildMutation = (
{ __typename?: 'RootMutation' }
& { build?: Maybe<(
{ __typename?: 'BuildMutation' }
& { createIosManagedBuild: (
& { createIosBuild: (
{ __typename?: 'CreateBuildResult' }
& { build: (
{ __typename?: 'Build' }
@@ -4670,10 +4739,10 @@ export type UpdatePublishMutation = (
{ __typename?: 'RootMutation' }
& { updateBranch: (
{ __typename?: 'UpdateBranchMutation' }
& { publishUpdateGroup: Array<Maybe<(
& { publishUpdateGroup: Array<(
{ __typename?: 'Update' }
& Pick<Update, 'id' | 'group'>
)>> }
)> }
) }
);
@@ -4681,6 +4750,7 @@ export type CreateSubmissionMutationVariables = Exact<{
appId: Scalars['ID'];
platform: AppPlatform;
config: Scalars['JSONObject'];
submittedBuildId?: Maybe<Scalars['ID']>;
}>;
@@ -4826,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<(
@@ -4993,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'>
@@ -3,20 +3,14 @@ import nullthrows from 'nullthrows';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import {
AndroidGenericJobInput,
AndroidManagedJobInput,
AndroidJobInput,
BuildMetadataInput,
CreateAndroidGenericBuildMutation,
CreateAndroidGenericBuildMutationVariables,
CreateAndroidManagedBuildMutation,
CreateAndroidManagedBuildMutationVariables,
CreateIosGenericBuildMutation,
CreateIosGenericBuildMutationVariables,
CreateIosManagedBuildMutation,
CreateIosManagedBuildMutationVariables,
CreateAndroidBuildMutation,
CreateAndroidBuildMutationVariables,
CreateIosBuildMutation,
CreateIosBuildMutationVariables,
EasBuildDeprecationInfo,
IosGenericJobInput,
IosManagedJobInput,
IosJobInput,
} from '../generated';
export interface BuildResult {
@@ -25,22 +19,22 @@ export interface BuildResult {
}
const BuildMutation = {
async createAndroidGenericBuildAsync(input: {
async createAndroidBuildAsync(input: {
appId: string;
job: AndroidGenericJobInput;
job: AndroidJobInput;
metadata: BuildMetadataInput;
}): Promise<BuildResult> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateAndroidGenericBuildMutation, CreateAndroidGenericBuildMutationVariables>(
.mutation<CreateAndroidBuildMutation, CreateAndroidBuildMutationVariables>(
gql`
mutation CreateAndroidGenericBuildMutation(
mutation CreateAndroidBuildMutation(
$appId: ID!
$job: AndroidGenericJobInput!
$job: AndroidJobInput!
$metadata: BuildMetadataInput
) {
build {
createAndroidGenericBuild(appId: $appId, job: $job, metadata: $metadata) {
createAndroidBuild(appId: $appId, job: $job, metadata: $metadata) {
build {
id
}
@@ -56,24 +50,24 @@ const BuildMutation = {
)
.toPromise()
);
return nullthrows(data.build?.createAndroidGenericBuild);
return nullthrows(data.build?.createAndroidBuild);
},
async createAndroidManagedBuildAsync(input: {
async createIosBuildAsync(input: {
appId: string;
job: AndroidManagedJobInput;
job: IosJobInput;
metadata: BuildMetadataInput;
}): Promise<BuildResult> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateAndroidManagedBuildMutation, CreateAndroidManagedBuildMutationVariables>(
.mutation<CreateIosBuildMutation, CreateIosBuildMutationVariables>(
gql`
mutation CreateAndroidManagedBuildMutation(
mutation CreateIosBuildMutation(
$appId: ID!
$job: AndroidManagedJobInput!
$job: IosJobInput!
$metadata: BuildMetadataInput
) {
build {
createAndroidManagedBuild(appId: $appId, job: $job, metadata: $metadata) {
createIosBuild(appId: $appId, job: $job, metadata: $metadata) {
build {
id
}
@@ -89,73 +83,7 @@ const BuildMutation = {
)
.toPromise()
);
return nullthrows(data.build?.createAndroidManagedBuild);
},
async createIosGenericBuildAsync(input: {
appId: string;
job: IosGenericJobInput;
metadata: BuildMetadataInput;
}): Promise<BuildResult> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateIosGenericBuildMutation, CreateIosGenericBuildMutationVariables>(
gql`
mutation CreateIosGenericBuildMutation(
$appId: ID!
$job: IosGenericJobInput!
$metadata: BuildMetadataInput
) {
build {
createIosGenericBuild(appId: $appId, job: $job, metadata: $metadata) {
build {
id
}
deprecationInfo {
type
message
}
}
}
}
`,
input
)
.toPromise()
);
return nullthrows(data.build?.createIosGenericBuild);
},
async createIosManagedBuildAsync(input: {
appId: string;
job: IosManagedJobInput;
metadata: BuildMetadataInput;
}): Promise<BuildResult> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateIosManagedBuildMutation, CreateIosManagedBuildMutationVariables>(
gql`
mutation CreateIosManagedBuildMutation(
$appId: ID!
$job: IosManagedJobInput!
$metadata: BuildMetadataInput
) {
build {
createIosManagedBuild(appId: $appId, job: $job, metadata: $metadata) {
build {
id
}
deprecationInfo {
type
message
}
}
}
}
`,
input
)
.toPromise()
);
return nullthrows(data.build?.createIosManagedBuild);
return nullthrows(data.build?.createIosBuild);
},
};
@@ -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".
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import chalk from 'chalk';
import figures from 'figures';
import { boolish } from 'getenv';
@@ -1,28 +1,24 @@
import { vol } from 'memfs';
import { asMock } from '../../__tests__/utils';
import { confirmAsync } from '../../prompts';
import { Actor, getUserAsync } from '../../user/User';
import {
ensureProjectExistsAsync,
findProjectIdByAccountNameAndSlugNullableAsync,
} from '../ensureProjectExists';
import {
findProjectRootAsync,
getProjectAccountName,
getProjectAccountNameAsync,
promptToCreateProjectIfNotExistsAsync,
} from '../projectUtils';
jest.mock('@expo/config');
jest.mock('fs');
jest.mock('../../user/User');
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
jest.mock('../ensureProjectExists');
describe(findProjectRootAsync, () => {
beforeEach(() => {
@@ -168,3 +164,62 @@ describe(getProjectAccountNameAsync, () => {
);
});
});
describe(promptToCreateProjectIfNotExistsAsync, () => {
beforeEach(() => {
asMock(getUserAsync).mockReset();
asMock(findProjectIdByAccountNameAndSlugNullableAsync).mockReset();
});
it('returns the existing project', async () => {
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
}));
asMock(findProjectIdByAccountNameAndSlugNullableAsync).mockImplementation(
() => 'already-existing-project-id'
);
const exp: any = {};
const projectId = await promptToCreateProjectIfNotExistsAsync(exp);
expect(projectId).toBe('already-existing-project-id');
});
it('makes a new project if none exists', async () => {
jest.mock('../../prompts');
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
}));
asMock(findProjectIdByAccountNameAndSlugNullableAsync).mockImplementation(() => null);
asMock(ensureProjectExistsAsync).mockImplementation(() => 'new-project-id');
// user agrees to make new project
(confirmAsync as jest.Mock).mockImplementation(() => true);
const exp: any = {};
const projectId = await promptToCreateProjectIfNotExistsAsync(exp);
expect(projectId).toBe('new-project-id');
});
it('aborts making a new project if the user declines', async () => {
jest.mock('../../prompts');
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
}));
asMock(findProjectIdByAccountNameAndSlugNullableAsync).mockImplementation(() => null);
asMock(ensureProjectExistsAsync).mockImplementation(() => 'new-project-id');
// user doesnt agree to make new project
(confirmAsync as jest.Mock).mockImplementation(() => false);
const exp: any = {};
const projectId = await promptToCreateProjectIfNotExistsAsync(exp);
expect(projectId).toBe(null);
});
});
@@ -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',
},
},
},
});
});
@@ -0,0 +1,59 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { vol } from 'memfs';
import { resolveWorkflowAsync } from '../workflow';
jest.mock('fs');
describe(resolveWorkflowAsync, () => {
beforeEach(() => {
vol.reset();
});
const projectDir = '/app';
test('generic workflow for both platforms', async () => {
vol.fromJSON(
{
'./ios/helloworld.xcodeproj/project.pbxproj': 'fake',
'./android/app/src/main/AndroidManifest.xml': 'fake',
},
projectDir
);
await expect(resolveWorkflowAsync(projectDir, Platform.ANDROID)).resolves.toBe(
Workflow.GENERIC
);
await expect(resolveWorkflowAsync(projectDir, Platform.IOS)).resolves.toBe(Workflow.GENERIC);
});
test('generic workflow for single platform', async () => {
vol.fromJSON(
{
'./ios/helloworld.xcodeproj/project.pbxproj': 'fake',
},
projectDir
);
await expect(resolveWorkflowAsync(projectDir, Platform.ANDROID)).resolves.toBe(
Workflow.MANAGED
);
await expect(resolveWorkflowAsync(projectDir, Platform.IOS)).resolves.toBe(Workflow.GENERIC);
});
test('android/ios directories are ignored', async () => {
vol.fromJSON(
{
'./ios/helloworld.xcodeproj/project.pbxproj': 'fake',
'./android/app/src/main/AndroidManifest.xml': 'fake',
'./.easignore': 'android/\nios/',
},
projectDir
);
await expect(resolveWorkflowAsync(projectDir, Platform.ANDROID)).resolves.toBe(
Workflow.MANAGED
);
await expect(resolveWorkflowAsync(projectDir, Platform.IOS)).resolves.toBe(Workflow.MANAGED);
});
});
@@ -3,19 +3,16 @@ import { vol } from 'memfs';
import os from 'os';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { promptAsync } from '../../../prompts';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
getApplicationId,
getApplicationIdAsync,
} from '../applicationId';
jest.mock('fs');
jest.mock('../../../prompts');
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.warn = jest.fn();
});
jest.mock('../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
beforeEach(() => {
vol.reset();
@@ -26,71 +23,69 @@ beforeEach(() => {
asMock(promptAsync).mockReset();
});
afterAll(() => {
fs.removeSync(os.tmpdir());
console.warn = originalConsoleWarn;
});
describe(getApplicationId, () => {
describe(getApplicationIdAsync, () => {
describe('generic projects', () => {
it('reads applicationId from build.gradle', () => {
it('reads applicationId from build.gradle', async () => {
vol.fromJSON(
{
'android/app/build.gradle': `
applicationId "com.expo.notdominik"
`,
'android/app/src/main/AndroidManifest.xml': 'fake',
},
'/app'
);
const applicationId = getApplicationId('/app', {} as any);
const applicationId = await getApplicationIdAsync('/app', {} as any);
expect(applicationId).toBe('com.expo.notdominik');
});
it('throws an error if build.gradle does not exist', () => {
it('throws an error if build.gradle does not exist', async () => {
vol.fromJSON(
{
'android/gradle.properties': 'fake file',
'android/app/src/main/AndroidManifest.xml': 'fake',
},
'/app'
);
expect(() => {
getApplicationId('/app', {} as any);
}).toThrowError(/Could not read application id/);
await expect(getApplicationIdAsync('/app', {} as any)).rejects.toThrowError(
/Could not read application id/
);
});
it('throws an error if the project does not have applicationId defined in build.gradle', () => {
it('throws an error if the project does not have applicationId defined in build.gradle', async () => {
vol.fromJSON(
{
'android/app/build.gradle': `fake build.gradle`,
'android/app/src/main/AndroidManifest.xml': 'fake',
},
'/app'
);
expect(() => {
getApplicationId('/app', {} as any);
}).toThrowError(/Could not read application id/);
await expect(getApplicationIdAsync('/app', {} as any)).rejects.toThrowError(
/Could not read application id/
);
});
});
describe('managed projects', () => {
it('reads applicationId (Android package) from app config', () => {
const applicationId = getApplicationId('/app', {
it('reads applicationId (Android package) from app config', async () => {
const applicationId = await getApplicationIdAsync('/app', {
android: { package: 'com.expo.notdominik' },
} as any);
expect(applicationId).toBe('com.expo.notdominik');
});
it('throws an error if Android package is not defined in app config', () => {
expect(() => {
getApplicationId('/app', {} as any);
}).toThrowError(/Specify "android.package"/);
it('throws an error if Android package is not defined in app config', async () => {
await expect(getApplicationIdAsync('/app', {} as any)).rejects.toThrowError(
/Specify "android.package"/
);
});
it('throws an error if Android package in app config is invalid', () => {
expect(() => {
getApplicationId('/app', { android: { package: '1com.expo.notdominik' } } as any);
}).toThrowError(/Specify "android.package"/);
it('throws an error if Android package in app config is invalid', async () => {
await expect(
getApplicationIdAsync('/app', { android: { package: '1com.expo.notdominik' } } as any)
).rejects.toThrowError(/Specify "android.package"/);
});
});
});
@@ -1,15 +1,17 @@
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { AndroidConfig, IOSConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import assert from 'assert';
import chalk from 'chalk';
import fs from 'fs-extra';
import once from 'lodash/once';
import nullthrows from 'nullthrows';
import Log from '../../log';
import { getProjectConfigDescription } from '../../project/projectUtils';
import Log, { learnMore } from '../../log';
import { getProjectConfigDescription, getUsername } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { resolveWorkflow } from '../workflow';
import { ensureLoggedInAsync } from '../../user/actions';
import { resolveWorkflowAsync } from '../workflow';
const INVALID_APPLICATION_ID_MESSAGE = `Invalid format of Android applicationId. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`;
@@ -17,18 +19,18 @@ export async function ensureApplicationIdIsDefinedForManagedProjectAsync(
projectDir: string,
exp: ExpoConfig
): Promise<string> {
const workflow = resolveWorkflow(projectDir, Platform.ANDROID);
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
assert(workflow === Workflow.MANAGED, 'This function should be called only for managed projects');
try {
return getApplicationId(projectDir, exp);
return await getApplicationIdAsync(projectDir, exp);
} catch (err) {
return await configureApplicationIdAsync(projectDir, exp);
}
}
export function getApplicationId(projectDir: string, exp: ExpoConfig): string {
const workflow = resolveWorkflow(projectDir, Platform.ANDROID);
export async function getApplicationIdAsync(projectDir: string, exp: ExpoConfig): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
warnIfAndroidPackageDefinedInAppConfigForGenericProject(projectDir, exp);
@@ -71,10 +73,19 @@ async function configureApplicationIdAsync(projectDir: string, exp: ExpoConfig):
assert(paths.staticConfigPath, 'app.json must exist');
Log.addNewLineIfNone();
Log.log(
`${chalk.bold(`📝 Android application id`)} ${chalk.dim(
learnMore('https://expo.fyi/android-package')
)}`
);
const suggestedAndroidApplicationId = await getSuggestedApplicationIdAsync(exp);
const { packageName } = await promptAsync({
name: 'packageName',
type: 'text',
message: `What would you like your Android application id to be?`,
initial: suggestedAndroidApplicationId,
validate: value => (isApplicationIdValid(value) ? true : INVALID_APPLICATION_ID_MESSAGE),
});
@@ -111,3 +122,19 @@ function _warnIfAndroidPackageDefinedInAppConfigForGenericProject(
export const warnIfAndroidPackageDefinedInAppConfigForGenericProject = once(
_warnIfAndroidPackageDefinedInAppConfigForGenericProject
);
async function getSuggestedApplicationIdAsync(exp: ExpoConfig): Promise<string | undefined> {
// Attempt to use the ios bundle id first since it's convenient to have them aligned.
const maybeBundleId = IOSConfig.BundleIdentifier.getBundleIdentifier(exp);
if (maybeBundleId && isApplicationIdValid(maybeBundleId)) {
return maybeBundleId;
} else {
const username = getUsername(exp, await ensureLoggedInAsync());
// It's common to use dashes in your node project name, strip them from the suggested package name.
const possibleId = `com.${username}.${exp.slug}`.split('-').join('');
if (isApplicationIdValid(possibleId)) {
return possibleId;
}
}
return undefined;
}
@@ -29,15 +29,10 @@ export async function ensureProjectExistsAsync(projectInfo: ProjectInfo): Promis
const spinner = ora(`Linking to project ${chalk.bold(projectFullName)}`).start();
try {
const id = await findProjectIdByAccountNameAndSlugAsync(accountName, projectName);
const maybeId = await findProjectIdByAccountNameAndSlugNullableAsync(accountName, projectName);
if (maybeId) {
spinner.succeed(`Linked to project ${chalk.bold(projectFullName)}`);
return id;
} catch (err) {
if (err.graphQLErrors?.some((it: any) => it.extensions?.errorCode !== 'EXPERIENCE_NOT_FOUND')) {
spinner.fail(`Something went wrong while looking for ${chalk.bold(projectFullName)} on Expo`);
throw err;
}
return maybeId;
}
try {
@@ -55,6 +50,26 @@ export async function ensureProjectExistsAsync(projectInfo: ProjectInfo): Promis
}
}
/**
* Finds project by `@accountName/slug` and returns its ID, return null if the project does not exist
* @param accountName account name
* @param slug project slug
* @returns A promise resolving to Project ID, null if it doesn't exist
*/
export async function findProjectIdByAccountNameAndSlugNullableAsync(
accountName: string,
slug: string
): Promise<string | null> {
try {
return await findProjectIdByAccountNameAndSlugAsync(accountName, slug);
} catch (err) {
if (err.graphQLErrors?.some((it: any) => it.extensions?.errorCode !== 'EXPERIENCE_NOT_FOUND')) {
throw err;
}
return null;
}
}
/**
* Finds project by `@accountName/slug` and returns its ID
* @param accountName account name
@@ -73,7 +88,7 @@ async function findProjectIdByAccountNameAndSlugAsync(
* Registers new project on EAS servers
* @returns Created project's ID
*/
async function registerNewProjectAsync({
export async function registerNewProjectAsync({
accountId,
projectName,
privacy,
@@ -4,20 +4,17 @@ import os from 'os';
import path from 'path';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { promptAsync } from '../../../prompts';
import {
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
getBundleIdentifier,
getBundleIdentifierAsync,
isWildcardBundleIdentifier,
} from '../bundleIdentifier';
jest.mock('fs');
jest.mock('../../../prompts');
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.warn = jest.fn();
});
jest.mock('../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
beforeEach(() => {
vol.reset();
@@ -28,16 +25,11 @@ beforeEach(() => {
asMock(promptAsync).mockReset();
});
afterAll(() => {
fs.removeSync(os.tmpdir());
console.warn = originalConsoleWarn;
});
const originalFs = jest.requireActual('fs');
describe(getBundleIdentifier, () => {
describe(getBundleIdentifierAsync, () => {
describe('generic projects', () => {
it('reads bundle identifier from project.', () => {
it('reads bundle identifier from project.', async () => {
vol.fromJSON(
{
'ios/myproject.xcodeproj/project.pbxproj': originalFs.readFileSync(
@@ -48,11 +40,11 @@ describe(getBundleIdentifier, () => {
'/app'
);
const bundleIdentifier = getBundleIdentifier('/app', {} as any);
const bundleIdentifier = await getBundleIdentifierAsync('/app', {} as any);
expect(bundleIdentifier).toBe('org.name.testproject');
});
it('throws an error if the pbxproj is not configured with bundle id', () => {
it('throws an error if the pbxproj is not configured with bundle id', async () => {
vol.fromJSON(
{
'ios/myproject.xcodeproj/project.pbxproj': originalFs.readFileSync(
@@ -63,30 +55,30 @@ describe(getBundleIdentifier, () => {
'/app'
);
expect(() => {
getBundleIdentifier('/app', {} as any);
}).toThrowError(/Could not read bundle identifier/);
await expect(getBundleIdentifierAsync('/app', {} as any)).rejects.toThrowError(
/Could not read bundle identifier/
);
});
});
describe('managed projects', () => {
it('reads bundleIdentifier from app config', () => {
const applicationId = getBundleIdentifier('/app', {
it('reads bundleIdentifier from app config', async () => {
const applicationId = await getBundleIdentifierAsync('/app', {
ios: { bundleIdentifier: 'com.expo.notdominik' },
} as any);
expect(applicationId).toBe('com.expo.notdominik');
});
it('throws an error if bundleIdentifier is not defined in app config', () => {
expect(() => {
getBundleIdentifier('/app', {} as any);
}).toThrowError(/Specify "ios.bundleIdentifier"/);
it('throws an error if bundleIdentifier is not defined in app config', async () => {
await expect(getBundleIdentifierAsync('/app', {} as any)).rejects.toThrowError(
/Specify "ios.bundleIdentifier"/
);
});
it('throws an error if bundleIdentifier in app config is invalid', () => {
expect(() => {
getBundleIdentifier('/app', { ios: { bundleIdentifier: '' } } as any);
}).toThrowError(/Specify "ios.bundleIdentifier"/);
it('throws an error if bundleIdentifier in app config is invalid', async () => {
await expect(
getBundleIdentifierAsync('/app', { ios: { bundleIdentifier: '' } } as any)
).rejects.toThrowError(/Specify "ios.bundleIdentifier"/);
});
});
});
@@ -9,11 +9,6 @@ import { selectSchemeAsync } from '../scheme';
jest.mock('fs');
jest.mock('../../../prompts');
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
});
beforeEach(() => {
vol.reset();
// do not remove the following line
@@ -23,11 +18,6 @@ beforeEach(() => {
asMock(promptAsync).mockReset();
});
afterAll(() => {
fs.removeSync(os.tmpdir());
console.log = originalConsoleLog;
});
describe(selectSchemeAsync, () => {
const projectDir = '/app';
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff. Show more