Compare commits

...
71 Commits
Author SHA1 Message Date
Brent Vatne e773e77185 v0.23.0 2021-08-05 15:19:01 -07:00
Brent Vatne 893cbb2ec5 [eas-cli] Pass through build settings metadata context to app version resolver (#550) 2021-08-05 15:17:30 -07:00
jkh 1898de2609 [eas-cli] add type to generated manifest fragment (#535) 2021-08-05 06:50:20 -07:00
jkh fe03cc28c4 [eas-cli] stop sending deprecated storageBucket field (#547) 2021-08-03 13:55:00 -07:00
Wojciech Kozyra 5309b7b6ce Update CHANGELOG 2021-08-02 13:23:20 +02:00
Wojciech Kozyra bc9b8442d7 v0.22.4 2021-08-02 13:07:52 +02:00
Wojciech Kozyra a84762b00d [eas-cli] override distribution when building for simulator 2021-08-02 12:57:59 +02:00
Wojciech Kozyra 2d117e1d7e v0.22.3 2021-08-02 12:49:51 +02:00
Sallar 986674942d [eas-json][eas-cli] add simulator key to build profile json validator (#546)
* Add simulator to build profile json validator

* Update CHANGELOG.md

* Fix PR number

* Change the order of simulator key
2021-08-02 12:46:21 +02:00
Wojciech Kozyra 13c224589b Update CHANGELOG 2021-08-02 10:57:16 +02:00
Wojciech Kozyra 20b091fbfa v0.22.2 2021-08-02 10:35:08 +02:00
Wojciech Kozyra 168b6afdfa [eas-cli] do not read BuildSettings on managed project 2021-08-02 10:10:03 +02:00
Dominik Sokal 3c6320b929 Update CHANGELOG.md 2021-07-30 14:03:18 +02:00
Dominik Sokal 4f1dc582bd [ENG-1570] evaluate interpolated iOS version strings (#541)
* evaluate interpolated ios version strings

* update CHANGELOG
2021-07-30 14:00:31 +02:00
Wojciech Kozyra daf2ef1e30 [eas-cli] fix unknown state error (#543)
* [eas-cli] fix unknown state error

* update CHANGELOG
2021-07-30 13:32:22 +02:00
Wojciech Kozyra f703bd8b5a [eas-cli] validate metadata in build request (#542)
* [eas-cli] validate metadata in build request

* Update CHANGELOG
2021-07-30 13:23:47 +02:00
Dominik Sokal 997cd7058a [ENG-1475] Additional filters for eas build:list (#540)
* generate graphql code

* use new App.builds api

* add more filters to build:list

* update CHANGELOG
2021-07-30 10:31:51 +02:00
Wojciech Kozyra e896c2f6c5 Update CHANGELOG 2021-07-29 14:55:04 +02:00
Wojciech Kozyra 3d723ca426 Use new eas.json format + auto migrate from old format (#537)
* Use new eas.json format + aot migrate old format

* review feedback

* review feedback 2

* fix
2021-07-29 14:50:08 +02:00
Dominik Sokal c17e1b3fa2 [ENG-1600] warn when building managed project that does not extend @expo/metro-config in metro.config.js (#534)
* warn when building managed project that does not extend @expo/metro-config in metro.config.js

* update CHANGELOG

* address PR feedback

* fix lint warnings/errors
2021-07-29 09:33:37 +02:00
Quinlan Jung d70fc36485 update changelog 2021-07-28 15:48:22 -07:00
Quinlan Jung 47784ca6d3 v0.22.1 2021-07-28 14:58:33 -07:00
Quinlan Jung 3e5a3aca27 [eas-cli] fix keystore upload bug (#538)
* [eas-cli] fix upload bug

* changelog
2021-07-28 14:55:36 -07:00
Dominik Sokal 89b0227594 [chore] upgrade: eslint, prettier, etc. (#536)
* upgrade: eslint, prettier, etc.

* run prettier
2021-07-28 11:12:06 +02:00
Dominik Sokal b98a30e7c6 install missing dep: fast-glob 2021-07-27 13:16:23 +02:00
Dominik Sokal 5c7862a685 upgrade typescript + clean up deps (#533)
* upgrade typescript to 4.3.5

* remove unused dependencies

* upgrade tslib

* update CHANGELOG
2021-07-27 12:22:39 +02:00
Bartłomiej KlocekandDominik Sokal 82833a9eb4 [eas submit][android] Autodetect google-services JSON key path (#520)
* foundation

* detection logic

* add tests

* Update CHANGELOG, ignore `node_modules`

* Display a chooser for multiple files

* Improve tests

* move `filterAsync` to a separate file

* Apply suggestions from code review

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

* Fix indent

Co-authored-by: Dominik Sokal <[email protected]>
2021-07-27 12:22:25 +02:00
Dominik Sokal 66171c3c4b [ENG-1524] use envs from eas.json when resolving app config (#532)
* use envs from eas.json when resolving app config

* update CHANGELOG

* address PR feedback
2021-07-26 15:38:42 +02:00
Wojciech KozyraandDominik Sokal 179def91c7 [eas-cli] use .pbxproj file to check if native Xcode project is present (#531)
* [eas-cli] use .pbxproj file to check if native Xcode project is present

* Update CHANGELOG.md

* Update packages/eas-cli/src/project/ios/scheme.ts

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-07-26 15:33:44 +02:00
Dominik Sokal 9f63ad4527 update CHANGELOG 2021-07-23 10:33:19 +02:00
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
175 changed files with 8324 additions and 4371 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
+106 -3
View File
@@ -6,16 +6,120 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
- Stop including deprecated storageBucket field when publishing ([547](https://github.com/expo/eas-cli/pull/547)) by [@jkhales](https://github.com/jkhales)
### 🎉 New features
### 🐛 Bug fixes
- Pass through metadata context to app version resolver. ([#550](https://github.com/expo/eas-cli/pull/550) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
## [0.22.4](https://github.com/expo/eas-cli/releases/tag/v0.22.4) - 2021-08-02
### 🐛 Bug fixes
- Fix build profile schema validation when `simulator` key is present for iOS ([#546](https://github.com/expo/eas-cli/pull/546) by [@sallar](https://github.com/sallar))
## [0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2) - 2021-08-02
### 🛠 Breaking changes
- Introduce new format of eas.json (old eas.json will be migrated automatically). ([#537](https://github.com/expo/eas-cli/pull/537) by [@wkozyra95](https://github.com/wkozyra95))
### 🎉 New features
- Validate metro config for managed workflow projects. ([#534](https://github.com/expo/eas-cli/pull/534) by [@dsokal](https://github.com/dsokal))
- Add more filter params to `build:list`. ([#540](https://github.com/expo/eas-cli/pull/540) by [@dsokal](https://github.com/dsokal))
- Evaluate interpolated iOS version strings for metadata. ([#541](https://github.com/expo/eas-cli/pull/541) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Validate metadata client side to print better errors. ([#542](https://github.com/expo/eas-cli/pull/542) by [@wkozyra95](https://github.com/wkozyra95))
- Fix `new` build status when building both paltforms. ([#543](https://github.com/expo/eas-cli/pull/543) by [@wkozyra95](https://github.com/wkozyra95))
## [0.22.1](https://github.com/expo/eas-cli/releases/tag/v0.22.1) - 2021-07-28
### 🎉 New features
- Include the file extension for update's assets in the manifest fragment. ([#532](https://github.com/expo/eas-cli/pull/535) by [@jkhales](https://github.com/jkhales))
- Use env variables from `eas.json` when evaluating `app.config.js`. ([#532](https://github.com/expo/eas-cli/pull/532) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix workflow detection when xcodeproj is an empty directory. ([#531](https://github.com/expo/eas-cli/pull/531) by [@wkozyra95](https://github.com/wkozyra95))
- Fix Android Keystore upload. ([#538](https://github.com/expo/eas-cli/pull/538) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Upgrade typescript to 4.3.5. ([#533](https://github.com/expo/eas-cli/pull/533) by [@dsokal](https://github.com/dsokal))
## [0.22.0](https://github.com/expo/eas-cli/releases/tag/v0.22.0) - 2021-07-23
### 🎉 New features
- Added support for automatically creating and linking iOS capability identifiers (Apple Pay, iCloud Containers, App Groups). ([#524](https://github.com/expo/eas-cli/pull/524) by [@EvanBacon](https://github.com/EvanBacon))
- Add `secret:create --force` command to overwrite existing secrets. ([#513](https://github.com/expo/eas-cli/pull/513) by [@bycedric](https://github.com/bycedric))
- Fall back to APK when building for internal distribution. ([#527](https://github.com/expo/eas-cli/pull/527) by [@dsokal](https://github.com/dsokal))
- Add `iosEnterpriseProvisioning` to build metadata. ([#522](https://github.com/expo/eas-cli/pull/522) by [@dsokal](https://github.com/dsokal))
- Autodetect Google Services JSON key path in `eas submit -p android`. ([#520](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
### 🐛 Bug fixes
- Fix iOS capability syncing on build. ([#521](https://github.com/expo/eas-cli/pull/521) by [@EvanBacon](https://github.com/EvanBacon))
- Fix unhandled error when amplitude domains are blocked. ([#512](https://github.com/expo/eas-cli/pull/512) by [@wkozyra95](https://github.com/wkozyra95))
- Use default value for `appBuildVersion` in build metadata when building an Android managed project. ([#526](https://github.com/expo/eas-cli/pull/526) by [@dsokal](https://github.com/dsokal))
## [0.21.0](https://github.com/expo/eas-cli/releases/tag/v0.21.0) - 2021-07-12
### 🎉 New features
- Add `project:*` commands. ([#500](https://github.com/expo/eas-cli/pull/500) by [@jkhales](https://github.com/jkhales))
- Added support for iOS 15 capabilities: Communication Notifications, Time Sensitive Notifications, Group Activities, and Family Controls. ([#499](https://github.com/expo/eas-cli/pull/499) by [@EvanBacon](https://github.com/EvanBacon))
- Show more build metadata in `build:view` and `build:list`. ([#504](https://github.com/expo/eas-cli/pull/504) and [#508](https://github.com/expo/eas-cli/pull/508) by [@dsokal](https://github.com/dsokal))
- Add runtime version to build metadata. ([#493](https://github.com/expo/eas-cli/pull/493) by [@dsokal](https://github.com/dsokal))
## [0.20.0](https://github.com/expo/eas-cli/releases/tag/v0.20.0) - 2021-07-09
### 🛠 Breaking changes
- Unifify generic and managed workflow, deprecate `workflow` field. ([#497](https://github.com/expo/eas-cli/pull/497) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Fix runtime version checks. ([#495](https://github.com/expo/eas-cli/pull/495) by [@dsokal](https://github.com/dsokal))
- Resolve `--android-package` correctly in `eas submit` command. ([#494](https://github.com/expo/eas-cli/pull/494) by [@wkozyra95](https://github.com/wkozyra95))
## [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 +151,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.23.0"
}
+4 -4
View File
@@ -17,12 +17,12 @@
"devDependencies": {
"@expo/oclif-dev-cli": "1.24.0-expo",
"@types/jest": "^26.0.13",
"eslint": "^6.6.0",
"eslint-config-universe": "^4.0.0",
"eslint-plugin-graphql": "^4.0.0",
"eslint": "7.31.0",
"eslint-config-universe": "7.0.1",
"eslint-plugin-graphql": "4.0.0",
"jest": "^26.4.2",
"jest-watch-typeahead": "^0.6.1",
"lerna": "^3.22.1",
"prettier": "^2.0.5"
"prettier": "2.3.2"
}
}
+2
View File
@@ -0,0 +1,2 @@
graphql.schema.json linguist-generated
src/graphql/generated.ts linguist-generated
+74 -24
View File
@@ -42,11 +42,14 @@ eas --help COMMAND
* [`eas build:configure`](#eas-buildconfigure)
* [`eas build:list`](#eas-buildlist)
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
* [`eas config`](#eas-config)
* [`eas credentials`](#eas-credentials)
* [`eas device:create`](#eas-devicecreate)
* [`eas device:list`](#eas-devicelist)
* [`eas device:view [UDID]`](#eas-deviceview-udid)
* [`eas help [COMMAND]`](#eas-help-command)
* [`eas project:info`](#eas-projectinfo)
* [`eas project:init`](#eas-projectinit)
* [`eas secret:create`](#eas-secretcreate)
* [`eas secret:delete`](#eas-secretdelete)
* [`eas secret:list`](#eas-secretlist)
@@ -69,7 +72,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.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.23.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -83,7 +86,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.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.23.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -97,7 +100,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.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.23.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -108,7 +111,7 @@ USAGE
$ eas analytics [STATUS]
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.23.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas build`
@@ -128,7 +131,7 @@ OPTIONS
--[no-]wait Wait for build(s) to complete
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.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.23.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -139,7 +142,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.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.23.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -153,7 +156,7 @@ OPTIONS
-p, --platform=(android|ios|all) Platform to configure
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.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.23.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:list`
@@ -164,12 +167,21 @@ USAGE
$ eas build:list
OPTIONS
--appBuildVersion=appBuildVersion
--appIdentifier=appIdentifier
--appVersion=appVersion
--buildProfile=buildProfile
--channel=channel
--distribution=(store|internal|simulator)
--gitCommitHash=gitCommitHash
--limit=limit
--platform=(all|android|ios)
--runtimeVersion=runtimeVersion
--sdkVersion=sdkVersion
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.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.23.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:view [BUILD_ID]`
@@ -180,7 +192,20 @@ 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.23.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas config`
Show the eas.json config
```
USAGE
$ eas config
OPTIONS
-p, --platform=(android|ios)
--profile=profile
```
## `eas credentials`
@@ -191,7 +216,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.23.0/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -202,7 +227,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.23.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:list`
@@ -216,7 +241,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.23.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -227,7 +252,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.23.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas help [COMMAND]`
@@ -246,6 +271,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.23.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.23.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 +305,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.23.0/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -277,7 +328,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.23.0/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -288,7 +339,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.23.0/packages/eas-cli/src/commands/secret/list.ts)_
## `eas submit --platform=(android|ios)`
@@ -314,8 +365,7 @@ OPTIONS
iOS specific options=asc-app-id App Store Connect unique application Apple ID
number. Providing this param results in skipping
app creation step. Learn more:
https://expo.fyi/asc-app-id
app creation step. Learn more.
iOS specific options=bundle-identifier Your iOS Bundle Identifier (default:
expo.ios.bundleIdentifier from app config)
@@ -368,7 +418,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.23.0/packages/eas-cli/src/commands/submit.ts)_
## `eas webhook:create`
@@ -387,7 +437,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.23.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -401,7 +451,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.23.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -415,7 +465,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.23.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -435,7 +485,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.23.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -449,5 +499,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.23.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';
};
+1989 -1066
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();
});
+24 -16
View File
@@ -1,20 +1,20 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.18.3",
"version": "0.23.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.45",
"@expo/eas-json": "^0.23.0",
"@expo/json-file": "8.2.25",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.11",
@@ -33,7 +33,7 @@
"cli-table3": "0.6.0",
"dateformat": "3.0.3",
"env-paths": "2.2.0",
"fast-glob": "3.2.4",
"fast-glob": "3.2.7",
"figures": "3.2.0",
"form-data": "3.0.0",
"fs-extra": "9.0.1",
@@ -64,7 +64,7 @@
"tempy": "0.7.0",
"terminal-link": "2.1.1",
"timeago.js": "4.0.2",
"tslib": "1.14.1",
"tslib": "2.3.0",
"turndown": "7.0.0",
"untildify": "4.0.0",
"uuid": "8.3.0",
@@ -79,7 +79,6 @@
"@graphql-codegen/introspection": "^1.18.0",
"@graphql-codegen/typescript": "^1.17.11",
"@graphql-codegen/typescript-operations": "^1.17.10",
"@types/color-string": "^1.5.0",
"@types/dateformat": "^3.0.1",
"@types/form-data": "^2.5.0",
"@types/fs-extra": "^9.0.1",
@@ -93,18 +92,15 @@
"@types/prompts": "^2.0.9",
"@types/semver": "^7.3.4",
"@types/tar": "^4.0.3",
"@types/tough-cookie": "^4.0.0",
"@types/tough-cookie": "4.0.1",
"@types/uuid": "^8.3.0",
"@types/wrap-ansi": "^3.0.0",
"@vercel/ncc": "^0.23.0",
"axios": "^0.21.0",
"axios": "0.21.1",
"babel-jest": "^26.3.0",
"memfs": "^3.2.0",
"mockdate": "^3.0.2",
"nock": "^13.0.5",
"ts-node": "^8",
"typescript": "^3.9.7",
"wonka": "^4.0.14"
"typescript": "4.3.5"
},
"engines": {
"node": ">=8.0.0"
@@ -131,15 +127,27 @@
],
"helpClass": "./build/help",
"topics": {
"account": {
"description": "manage your account"
},
"build": {
"description": "build app binaries"
},
"channel": {
"description": "manage update channels"
},
"device": {
"description": "manage your Apple devices for internal distribution"
},
"project": {
"description": "manage your project"
},
"release": {
"description": "manage update releases"
},
"secret": {
"description": "manage project and account secrets"
},
"update": {
"description": "manage individual updates"
},
+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) });
}
}
@@ -14,10 +14,11 @@ export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig)
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
const updatedBuildGradleContents = AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
projectDir,
buildGradleContents
);
const updatedBuildGradleContents =
AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
projectDir,
buildGradleContents
);
await fs.writeFile(buildGradlePath, updatedBuildGradleContents);
}
@@ -94,14 +95,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'
);
+32 -65
View File
@@ -1,9 +1,6 @@
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 +10,29 @@ 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 {
BuildRequestSender,
CredentialsResult,
prepareBuildRequestForPlatformAsync,
} from '../build';
import { BuildContext } 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>({
commandCtx,
platform: Platform.ANDROID,
easConfig,
});
const { buildProfile } = buildCtx;
ctx: BuildContext<Platform.ANDROID>
): Promise<BuildRequestSender> {
const { buildProfile } = ctx;
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,18 +48,19 @@ This means that it will most likely produce an AAB and you will not be able to i
}
}
if (buildCtx.buildProfile.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(commandCtx.projectDir, commandCtx.exp);
if (ctx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
}
return await prepareBuildRequestForPlatformAsync({
ctx: buildCtx,
ctx,
ensureCredentialsAsync: ensureAndroidCredentialsAsync,
ensureProjectConfiguredAsync: async () => {
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
await validateAndSyncProjectConfigurationAsync(commandCtx.projectDir, commandCtx.exp);
if (ctx.workflow === Workflow.GENERIC) {
await validateAndSyncProjectConfigurationAsync(ctx.projectDir, ctx.exp);
}
},
getMetadataContext: () => ({}),
prepareJobAsync,
sendBuildRequestAsync: async (
appId: string,
@@ -87,30 +68,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,21 +88,19 @@ async function ensureAndroidCredentialsAsync(
if (!shouldProvideCredentials(ctx)) {
return;
}
const androidApplicationIdentifier = getApplicationId(
ctx.commandCtx.projectDir,
ctx.commandCtx.exp
);
const androidApplicationIdentifier = await getApplicationIdAsync(ctx.projectDir, ctx.exp);
const provider = new AndroidCredentialsProvider(
await createCredentialsContextAsync(ctx.commandCtx.projectDir, {
nonInteractive: ctx.commandCtx.nonInteractive,
await createCredentialsContextAsync(ctx.projectDir, {
exp: ctx.exp,
nonInteractive: ctx.nonInteractive,
}),
{
app: {
account: nullthrows(
findAccountByName(ctx.commandCtx.user.accounts, ctx.commandCtx.accountName),
`You do not have access to account: ${ctx.commandCtx.accountName}`
findAccountByName(ctx.user.accounts, ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.commandCtx.projectName,
projectName: ctx.projectName,
androidApplicationIdentifier,
},
}
+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,5 @@
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 { AndroidBuildProfile } from '@expo/eas-json';
import path from 'path';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
@@ -14,37 +14,19 @@ interface JobData {
credentials?: AndroidCredentials;
}
const cacheDefaults = {
disabled: false,
customPaths: [],
cacheDefaultPaths: true,
};
export async function prepareJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Job> {
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.exp, await ensureLoggedInAsync());
const buildProfile: AndroidBuildProfile = ctx.buildProfile;
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
const { credentials } = jobData;
const buildCredentials = credentials
? {
@@ -59,60 +41,41 @@ async function prepareJobCommonAsync(
}
: {};
return {
let buildType = ctx.buildProfile.buildType;
if (!buildType && !buildProfile.gradleCommand && ctx.buildProfile.distribution === 'internal') {
buildType = Android.BuildType.APK;
}
const job: Android.Job = {
type: ctx.workflow,
platform: Platform.ANDROID,
projectRootDirectory,
projectArchive: jobData.projectArchive,
builderEnvironment: {
image: ctx.buildProfile.image,
node: ctx.buildProfile.node,
yarn: ctx.buildProfile.yarn,
ndk: ctx.buildProfile.ndk,
expoCli: ctx.buildProfile.expoCli,
env: ctx.buildProfile.env,
image: buildProfile.image ?? 'default',
node: buildProfile.node,
yarn: buildProfile.yarn,
ndk: buildProfile.ndk,
expoCli: buildProfile.expoCli,
env: buildProfile.env,
},
cache: {
...ctx.buildProfile.cache,
clear: ctx.commandCtx.clearCache,
...cacheDefaults,
...buildProfile.cache,
clear: ctx.clearCache,
},
secrets: {
...buildCredentials,
},
};
}
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,
};
}
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: buildProfile.developmentClient ? Android.BuildType.DEVELOPMENT_CLIENT : 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 ["'](.*)["']/);
+137 -24
View File
@@ -1,19 +1,24 @@
import { ArchiveSource, ArchiveSourceType, Job, Metadata } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import ora from 'ora';
import { UploadSessionType } from '../graphql/generated';
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
import { BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log from '../log';
import { promptAsync } from '../prompts';
import { uploadAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
import { sleep } from '../utils/promise';
import vcs from '../vcs';
import { requestedPlatformDisplayNames } from './constants';
import { BuildContext } from './context';
import { runLocalBuildAsync } from './local';
import { collectMetadata } from './metadata';
import { MetadataContext, collectMetadata } from './metadata';
import { Platform, TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
import { printDeprecationWarnings } from './utils/printBuildInfo';
@@ -36,63 +41,62 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
ctx: BuildContext<TPlatform>
): Promise<CredentialsResult<Credentials> | undefined>;
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
getMetadataContext: () => MetadataContext<TPlatform>;
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
}
export type BuildRequestSender = () => Promise<BuildFragment | undefined>;
export async function prepareBuildRequestForPlatformAsync<
TPlatform extends Platform,
Credentials,
TJob extends Job
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<() => Promise<string | undefined>> {
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
const { ctx } = builder;
const credentialsResult = await withAnalyticsAsync(
async () => await builder.ensureCredentialsAsync(builder.ctx),
async () => await builder.ensureCredentialsAsync(ctx),
{
successEvent: Event.GATHER_CREDENTIALS_SUCCESS,
failureEvent: Event.GATHER_CREDENTIALS_FAIL,
trackingCtx: builder.ctx.trackingCtx,
trackingCtx: ctx.trackingCtx,
}
);
if (!builder.ctx.commandCtx.skipProjectConfiguration) {
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(builder.ctx), {
if (!ctx.skipProjectConfiguration) {
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(ctx), {
successEvent: Event.CONFIGURE_PROJECT_SUCCESS,
failureEvent: Event.CONFIGURE_PROJECT_FAIL,
trackingCtx: builder.ctx.trackingCtx,
trackingCtx: ctx.trackingCtx,
});
}
if (await vcs.hasUncommittedChangesAsync()) {
Log.addNewLineIfNone();
await reviewAndCommitChangesAsync(
`[EAS Build] Run EAS Build for ${
requestedPlatformDisplayNames[builder.ctx.platform as Platform]
}`,
{
nonInteractive: builder.ctx.commandCtx.nonInteractive,
}
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
{ nonInteractive: ctx.nonInteractive }
);
}
const projectArchive = builder.ctx.commandCtx.local
const projectArchive = ctx.local
? ({
type: ArchiveSourceType.PATH,
path: (await makeProjectTarballAsync()).path,
} as const)
: ({
type: ArchiveSourceType.S3,
bucketKey: await uploadProjectAsync(builder.ctx),
bucketKey: await uploadProjectAsync(ctx),
} as const);
const metadata = await collectMetadata(builder.ctx, {
credentialsSource: credentialsResult?.source,
});
const job = await builder.prepareJobAsync(builder.ctx, {
const metadataContext = builder.getMetadataContext();
const metadata = await collectMetadata(ctx, metadataContext);
const job = await builder.prepareJobAsync(ctx, {
projectArchive,
credentials: credentialsResult?.credentials,
});
return async () => {
if (builder.ctx.commandCtx.local) {
if (ctx.local) {
await runLocalBuildAsync(job);
return undefined;
} else {
@@ -102,6 +106,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'
) {
@@ -163,7 +181,7 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
builder: Builder<TPlatform, Credentials, TJob>,
job: TJob,
metadata: Metadata
): Promise<string> {
): Promise<BuildFragment> {
const { ctx } = builder;
return await withAnalyticsAsync(
async () => {
@@ -172,13 +190,13 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
}
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
ctx.commandCtx.projectId,
ctx.projectId,
job,
metadata
);
printDeprecationWarnings(deprecationInfo);
return build.id;
return build;
},
{
successEvent: Event.BUILD_REQUEST_SUCCESS,
@@ -260,3 +278,98 @@ async function reviewAndCommitChangesAsync(
});
}
}
export async function waitForBuildEndAsync(
buildIds: string[],
{ timeoutSec = 3600, intervalSec = 30 } = {}
): Promise<(BuildFragment | null)[]> {
Log.log(
`Waiting for build${buildIds.length > 1 ? 's' : ''} to complete. You can press Ctrl+C to exit.`
);
const spinner = ora().start();
let time = new Date().getTime();
const endTime = time + timeoutSec * 1000;
while (time <= endTime) {
const builds: (BuildFragment | null)[] = await Promise.all(
buildIds.map(async buildId => {
try {
return await BuildQuery.byIdAsync(buildId, { useCache: false });
} catch (err) {
return null;
}
})
);
if (builds.length === 1) {
const build = nullthrows(builds[0]);
switch (build.status) {
case BuildStatus.Finished:
spinner.succeed('Build finished');
return builds;
case BuildStatus.New:
spinner.text = 'Build created';
break;
case BuildStatus.InQueue:
spinner.text = 'Build queued...';
break;
case BuildStatus.Canceled:
spinner.text = 'Build canceled';
spinner.stopAndPersist();
return builds;
case BuildStatus.InProgress:
spinner.text = 'Build in progress...';
break;
case BuildStatus.Errored:
spinner.fail('Build failed');
if (build.error) {
return builds;
} else {
throw new Error(`Standalone build failed!`);
}
default:
spinner.warn('Unknown status.');
throw new Error(`Unknown status: ${builds} - aborting!`);
}
} else {
if (builds.filter(build => build?.status === BuildStatus.Finished).length === builds.length) {
spinner.succeed('All builds have finished');
return builds;
} else if (
builds.filter(build =>
build?.status
? [BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(
build.status
)
: false
).length === builds.length
) {
spinner.fail('Some of the builds were canceled or failed.');
return builds;
} else {
const newBuilds = builds.filter(build => build?.status === BuildStatus.New).length;
const inQueue = builds.filter(build => build?.status === BuildStatus.InQueue).length;
const inProgress = builds.filter(build => build?.status === BuildStatus.InProgress).length;
const errored = builds.filter(build => build?.status === BuildStatus.Errored).length;
const finished = builds.filter(build => build?.status === BuildStatus.Finished).length;
const canceled = builds.filter(build => build?.status === BuildStatus.Canceled).length;
const unknownState = builds.length - newBuilds - inQueue - inProgress - errored - finished;
spinner.text = [
newBuilds && `Builds created: ${newBuilds}`,
inQueue && `Builds in queue: ${inQueue}`,
inProgress && `Builds in progress: ${inProgress}`,
canceled && `Builds canceled: ${canceled}`,
errored && chalk.red(`Builds failed: ${errored}`),
finished && chalk.green(`Builds finished: ${finished}`),
unknownState && chalk.red(`Builds in unknown state: ${unknownState}`),
]
.filter(i => i)
.join('\t');
}
}
time = new Date().getTime();
await sleep(intervalSec * 1000);
}
spinner.warn('Timed out');
throw new Error(
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
);
}
+61 -64
View File
@@ -1,10 +1,13 @@
import { getConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import Log from '../log';
import { promptAsync } from '../prompts';
import Log, { learnMore } from '../log';
import { resolveWorkflowAsync } from '../project/workflow';
import { confirmAsync, promptAsync } from '../prompts';
import { ensureLoggedInAsync } from '../user/actions';
import vcs from '../vcs';
import { configureAndroidAsync } from './android/configure';
@@ -19,6 +22,37 @@ const configureCommitMessage = {
[RequestedPlatform.All]: 'Configure EAS Build',
};
export async function ensureProjectConfiguredAsync(
projectDir: string,
requestedPlatform: RequestedPlatform
): Promise<void> {
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir))) {
return;
}
const message = 'This project is not configured to build with EAS. Set it up now?';
const confirm = await confirmAsync({ message });
if (confirm) {
await configureAsync({
projectDir,
platform: requestedPlatform,
});
if (await vcs.hasUncommittedChangesAsync()) {
error(
'Build process requires clean working tree, please commit all your changes and run `eas build` again',
{ exit: 1 }
);
}
} else {
error(
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
'https://docs.expo.io/build/eas-json'
)})`,
{ exit: 1 }
);
}
}
export async function configureAsync(options: {
platform: RequestedPlatform;
projectDir: string;
@@ -37,8 +71,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();
@@ -60,84 +96,45 @@ export async function configureAsync(options: {
}
const MANAGED_DEFAULTS = {
release: {
workflow: 'managed',
},
release: {},
development: {
workflow: 'managed',
buildType: 'development-client',
developmentClient: true,
distribution: 'internal',
},
};
const ANDROID_GENERIC_DEFAULTS = {
release: {
workflow: 'generic',
},
const GENERIC_DEFAULTS = {
release: {},
development: {
workflow: 'generic',
gradleCommand: ':app:assembleDebug',
distribution: 'internal',
},
};
const IOS_GENERIC_DEFAULTS = {
release: {
workflow: 'generic',
},
development: {
workflow: 'generic',
schemeBuildConfiguration: 'Debug',
distribution: 'internal',
android: {
gradleCommand: ':app:assembleDebug',
},
ios: {
buildConfiguration: 'Debug',
},
},
};
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
const easJsonPath = path.join(ctx.projectDir, 'eas.json');
let existingEasJson;
const easJsonPath = EasJsonReader.formatEasJsonPath(ctx.projectDir);
if (await fs.pathExists(easJsonPath)) {
const reader = new EasJsonReader(ctx.projectDir, ctx.requestedPlatform);
await reader.validateAsync();
const reader = new EasJsonReader(ctx.projectDir);
await reader.readAndValidateAsync();
existingEasJson = await reader.readRawAsync();
Log.withTick('Validated eas.json');
// If we have already populated eas.json with the default fields for the
// platform then proceed
if (ctx.requestedPlatform !== 'all' && existingEasJson.builds[ctx.requestedPlatform]) {
return;
} else if (
ctx.requestedPlatform === 'all' &&
existingEasJson.builds.ios &&
existingEasJson.builds.android
) {
return;
}
return;
}
const shouldInitIOS =
['all', 'ios'].includes(ctx.requestedPlatform) && !existingEasJson?.builds.ios;
const shouldInitAndroid =
['all', 'android'].includes(ctx.requestedPlatform) && !existingEasJson?.builds.android;
const easJson = {
builds: {
...existingEasJson,
...(shouldInitAndroid
? {
android: ctx.hasAndroidNativeProject ? ANDROID_GENERIC_DEFAULTS : MANAGED_DEFAULTS,
}
: null),
...(shouldInitIOS
? {
ios: ctx.hasIosNativeProject ? IOS_GENERIC_DEFAULTS : MANAGED_DEFAULTS,
}
: null),
},
};
const easJson =
ctx.hasAndroidNativeProject && ctx.hasIosNativeProject
? { build: GENERIC_DEFAULTS }
: { build: MANAGED_DEFAULTS };
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
await vcs.trackFileAsync(easJsonPath);
Log.withTick(`${existingEasJson ? 'Updated' : 'Generated'} eas.json`);
Log.withTick('Generated eas.json');
}
enum ShouldCommitChanges {
+79 -98
View File
@@ -1,77 +1,19 @@
import { ExpoConfig } from '@expo/config';
import { Workflow } from '@expo/eas-build-job';
import { AndroidBuildProfile, EasConfig, IosBuildProfile } from '@expo/eas-json';
import { AndroidBuildProfile, BuildProfile, IosBuildProfile } from '@expo/eas-json';
import JsonFile from '@expo/json-file';
import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
import { getProjectAccountName } from '../project/projectUtils';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { findAccountByName } from '../user/Account';
import { Actor } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
import { Platform, RequestedPlatform, TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
export interface CommandContext {
requestedPlatform: RequestedPlatform;
profile: string;
projectDir: string;
projectId: string;
user: Actor;
accountName: string;
projectName: string;
exp: ExpoConfig;
nonInteractive: boolean;
local: boolean;
clearCache: boolean;
skipProjectConfiguration: boolean;
waitForBuildEnd: boolean;
}
export async function createCommandContextAsync({
requestedPlatform,
profile,
exp,
projectDir,
projectId,
nonInteractive = false,
local,
clearCache = false,
skipProjectConfiguration = false,
waitForBuildEnd,
}: {
requestedPlatform: RequestedPlatform;
profile: string;
exp: ExpoConfig;
projectId: string;
projectDir: string;
nonInteractive: boolean;
local: boolean;
clearCache: boolean;
skipProjectConfiguration: boolean;
waitForBuildEnd: boolean;
}): Promise<CommandContext> {
const user = await ensureLoggedInAsync();
const accountName = getProjectAccountName(exp, user);
const projectName = exp.slug;
return {
requestedPlatform,
profile,
projectDir,
projectId,
user,
accountName,
projectName,
exp,
nonInteractive,
local,
clearCache,
skipProjectConfiguration,
waitForBuildEnd,
};
}
export interface ConfigureContext {
user: Actor;
projectDir: string;
@@ -83,64 +25,103 @@ export interface ConfigureContext {
hasIosNativeProject: boolean;
}
type PlatformBuildProfile<T extends Platform> = T extends Platform.ANDROID
? AndroidBuildProfile
: IosBuildProfile;
export interface BuildContext<T extends Platform> {
commandCtx: CommandContext;
trackingCtx: TrackingContext;
accountName: string;
buildProfile: BuildProfile<T>;
buildProfileName: string;
clearCache: boolean;
exp: ExpoConfig;
local: boolean;
nonInteractive: boolean;
platform: T;
buildProfile: PlatformBuildProfile<T>;
projectDir: string;
projectId: string;
projectName: string;
skipProjectConfiguration: boolean;
trackingCtx: TrackingContext;
user: Actor;
workflow: Workflow;
}
export function createBuildContext<T extends Platform>({
export async function createBuildContextAsync<T extends Platform>({
buildProfileName,
buildProfile,
clearCache = false,
local,
nonInteractive = false,
platform,
easConfig,
commandCtx,
projectDir,
skipProjectConfiguration = false,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
clearCache: boolean;
local: boolean;
nonInteractive: boolean;
platform: T;
easConfig: EasConfig;
commandCtx: CommandContext;
}): BuildContext<T> {
const buildProfile = easConfig.builds[platform] as PlatformBuildProfile<T> | undefined;
if (!buildProfile) {
throw new Error(`${platform} build profile does not exist`);
}
projectDir: string;
skipProjectConfiguration: boolean;
}): Promise<BuildContext<T>> {
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
const accountId = findAccountByName(commandCtx.user.accounts, commandCtx.accountName)?.id;
const devClientProperties = getDevClientEventProperties(platform, commandCtx, buildProfile);
const user = await ensureLoggedInAsync();
const accountName = getProjectAccountName(exp, user);
const projectName = exp.slug;
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
const workflow = await resolveWorkflowAsync(projectDir, platform);
const accountId = findAccountByName(user.accounts, accountName)?.id;
const devClientProperties = getDevClientEventProperties({
platform,
projectDir,
buildProfile,
});
const trackingCtx = {
tracking_id: uuidv4(),
platform,
...(accountId && { account_id: accountId }),
account_name: commandCtx.accountName,
project_id: commandCtx.projectId,
project_type: buildProfile.workflow,
account_name: accountName,
project_id: projectId,
project_type: workflow,
...devClientProperties,
};
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
return {
commandCtx,
trackingCtx,
platform,
accountName,
buildProfile,
buildProfileName,
clearCache,
exp,
local,
nonInteractive,
platform,
projectDir,
projectId,
projectName,
skipProjectConfiguration,
trackingCtx,
user,
workflow,
};
}
function getDevClientEventProperties(
platform: Platform,
commandCtx: CommandContext,
buildProfile: AndroidBuildProfile | IosBuildProfile
): Partial<TrackingContext> {
function getDevClientEventProperties({
platform,
projectDir,
buildProfile,
}: {
platform: Platform;
projectDir: string;
buildProfile: AndroidBuildProfile | IosBuildProfile;
}): Partial<TrackingContext> {
let includesDevClient;
const version = tryGetDevClientVersion(commandCtx.projectDir);
if (buildProfile.workflow === Workflow.MANAGED) {
includesDevClient = buildProfile.buildType === 'development-client';
} else if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
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 (platform === Platform.IOS && 'buildConfiguration' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.buildConfiguration === 'Debug');
} else if (buildProfile.developmentClient) {
includesDevClient = true;
} else {
includesDevClient = false;
}
-173
View File
@@ -1,173 +0,0 @@
import { EasJsonReader } from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import ora from 'ora';
import { BuildFragment, BuildStatus } from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log from '../log';
import { sleep } from '../utils/promise';
import vcs from '../vcs';
import { prepareAndroidBuildAsync } from './android/build';
import { CommandContext } from './context';
import { prepareIosBuildAsync } from './ios/build';
import { Platform, RequestedPlatform } from './types';
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
import { ensureRepoIsCleanAsync } from './utils/repository';
export async function buildAsync(commandCtx: CommandContext): Promise<void> {
await vcs.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(commandCtx.nonInteractive);
const scheduledBuilds = await startBuildsAsync(commandCtx);
if (commandCtx.local) {
return;
}
Log.newLine();
printLogsUrls(commandCtx.accountName, scheduledBuilds);
Log.newLine();
if (commandCtx.waitForBuildEnd) {
const builds = await waitForBuildEndAsync(scheduledBuilds.map(i => i.buildId));
printBuildResults(commandCtx.accountName, builds);
exitWithNonZeroCodeIfSomeBuildsFailed(builds);
}
}
async function startBuildsAsync(
commandCtx: CommandContext
): Promise<{ platform: Platform; buildId: string }[]> {
const shouldBuildAndroid = [RequestedPlatform.Android, RequestedPlatform.All].includes(
commandCtx.requestedPlatform
);
const shouldBuildIos = [RequestedPlatform.Ios, RequestedPlatform.All].includes(
commandCtx.requestedPlatform
);
const easConfig = await new EasJsonReader(
commandCtx.projectDir,
commandCtx.requestedPlatform
).readAsync(commandCtx.profile);
const builds: {
platform: Platform;
sendBuildRequestAsync: () => Promise<string | undefined>;
}[] = [];
if (shouldBuildAndroid) {
const sendBuildRequestAsync = await prepareAndroidBuildAsync(commandCtx, easConfig);
builds.push({ platform: Platform.ANDROID, sendBuildRequestAsync });
}
if (shouldBuildIos) {
const sendBuildRequestAsync = await prepareIosBuildAsync(commandCtx, easConfig);
builds.push({ platform: Platform.IOS, sendBuildRequestAsync });
}
return (
await Promise.all(
builds.map(async ({ platform, sendBuildRequestAsync }) => ({
platform,
buildId: await sendBuildRequestAsync(),
}))
)
).filter((build): build is { platform: Platform; buildId: string } => !!build.buildId);
}
async function waitForBuildEndAsync(
buildIds: string[],
{ timeoutSec = 3600, intervalSec = 30 } = {}
): Promise<(BuildFragment | null)[]> {
Log.log('Waiting for build to complete. You can press Ctrl+C to exit.');
const spinner = ora().start();
let time = new Date().getTime();
const endTime = time + timeoutSec * 1000;
while (time <= endTime) {
const builds: (BuildFragment | null)[] = await Promise.all(
buildIds.map(async buildId => {
try {
return await BuildQuery.byIdAsync(buildId, { useCache: false });
} catch (err) {
return null;
}
})
);
if (builds.length === 1) {
const build = nullthrows(builds[0]);
switch (build.status) {
case BuildStatus.Finished:
spinner.succeed('Build finished');
return builds;
case BuildStatus.New:
spinner.text = 'Build created';
break;
case BuildStatus.InQueue:
spinner.text = 'Build queued...';
break;
case BuildStatus.Canceled:
spinner.text = 'Build canceled';
spinner.stopAndPersist();
return builds;
case BuildStatus.InProgress:
spinner.text = 'Build in progress...';
break;
case BuildStatus.Errored:
spinner.fail('Build failed');
if (build.error) {
return builds;
} else {
throw new Error(`Standalone build failed!`);
}
default:
spinner.warn('Unknown status.');
throw new Error(`Unknown status: ${builds} - aborting!`);
}
} else {
if (builds.filter(build => build?.status === BuildStatus.Finished).length === builds.length) {
spinner.succeed('All builds have finished');
return builds;
} else if (
builds.filter(build =>
build?.status
? [BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(
build.status
)
: false
).length === builds.length
) {
spinner.fail('Some of the builds were canceled or failed.');
return builds;
} else {
const newBuilds = builds.filter(build => build?.status === BuildStatus.New).length;
const inQueue = builds.filter(build => build?.status === BuildStatus.InQueue).length;
const inProgress = builds.filter(build => build?.status === BuildStatus.InProgress).length;
const errored = builds.filter(build => build?.status === BuildStatus.Errored).length;
const finished = builds.filter(build => build?.status === BuildStatus.Finished).length;
const canceled = builds.filter(build => build?.status === BuildStatus.Canceled).length;
const unknownState = builds.length - inQueue - inProgress - errored - finished;
spinner.text = [
newBuilds && `Builds created: ${newBuilds}`,
inQueue && `Builds in queue: ${inQueue}`,
inProgress && `Builds in progress: ${inProgress}`,
canceled && `Builds canceled: ${canceled}`,
errored && chalk.red(`Builds failed: ${errored}`),
finished && chalk.green(`Builds finished: ${finished}`),
unknownState && chalk.red(`Builds in unknown state: ${unknownState}`),
]
.filter(i => i)
.join('\t');
}
}
time = new Date().getTime();
await sleep(intervalSec * 1000);
}
spinner.warn('Timed out');
throw new Error(
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
);
}
function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
i => i.status === BuildStatus.Errored
);
if (failedBuilds.length > 0) {
process.exit(1);
}
}
+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());
@@ -154,7 +149,7 @@ describe(readBuildNumberAsync, () => {
describe('generic project', () => {
it('reads the build number from native code', async () => {
const exp = initGenericProject();
const buildNumber = await readBuildNumberAsync('/repo', exp);
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
expect(buildNumber).toBe('1');
});
});
@@ -162,7 +157,7 @@ describe(readBuildNumberAsync, () => {
describe('managed project', () => {
it('reads the build number from expo config', async () => {
const exp = initManagedProject();
const buildNumber = await readBuildNumberAsync('/repo', exp);
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
expect(buildNumber).toBe('1');
});
});
@@ -172,21 +167,33 @@ describe(readShortVersionAsync, () => {
describe('generic project', () => {
it('reads the short version from native code', async () => {
const exp = initGenericProject();
const shortVersion = await readShortVersionAsync('/repo', exp);
const shortVersion = await readShortVersionAsync('/repo', exp, {});
expect(shortVersion).toBe('1.0.0');
});
it('evaluates interpolated build number', async () => {
const exp = initGenericProject({
shortVersion: '$(CURRENT_PROJECT_VERSION)',
});
const buildNumber = await readShortVersionAsync('/repo', exp, {
CURRENT_PROJECT_VERSION: '1.0.0',
});
expect(buildNumber).toBe('1.0.0');
});
});
describe('managed project', () => {
it('reads the version from app config', async () => {
const exp = initGenericProject();
const shortVersion = await readShortVersionAsync('/repo', exp);
const shortVersion = await readShortVersionAsync('/repo', exp, {});
expect(shortVersion).toBe('1.0.0');
});
});
});
function initGenericProject(): ExpoConfig {
function initGenericProject({
shortVersion = '1.0.0',
version = '1',
}: { shortVersion?: string; version?: string } = {}): ExpoConfig {
vol.fromJSON(
{
'./app.json': JSON.stringify({
@@ -202,9 +209,9 @@ function initGenericProject(): ExpoConfig {
<plist version="1.0">
<dict>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<string>${shortVersion}</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>${version}</string>
</dict>
</plist>`,
},
+38 -56
View File
@@ -1,80 +1,71 @@
import { IOSConfig } from '@expo/config-plugins';
import { Ios, Job, Metadata, Workflow } from '@expo/eas-build-job';
import { EasConfig } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { IosCredentials } from '../../credentials/ios/types';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { resolveTargetsAsync } from '../../project/ios/target';
import { JobData, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommandContext, createBuildContext } from '../context';
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext } from '../context';
import { transformMetadata } from '../graphql';
import { IosMetadataContext } from '../metadata';
import { Platform } from '../types';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { ensureIosCredentialsAsync } from './credentials';
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>({
commandCtx,
platform: Platform.IOS,
easConfig,
});
const { buildProfile } = buildCtx;
ctx: BuildContext<Platform.IOS>
): Promise<BuildRequestSender> {
const { buildProfile } = ctx;
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) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(
commandCtx.projectDir,
commandCtx.exp
);
if (ctx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
}
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
projectDir: commandCtx.projectDir,
nonInteractive: commandCtx.nonInteractive,
exp: commandCtx.exp,
projectDir: ctx.projectDir,
nonInteractive: ctx.nonInteractive,
exp: ctx.exp,
},
buildProfile
);
const targets = await resolveTargetsAsync(
{
projectDir: commandCtx.projectDir,
exp: commandCtx.exp,
projectDir: ctx.projectDir,
exp: ctx.exp,
},
xcodeBuildContext
);
return await prepareBuildRequestForPlatformAsync({
ctx: buildCtx,
ctx,
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
return ensureIosCredentialsAsync(ctx, targets);
},
ensureProjectConfiguredAsync: async () => {
await validateAndSyncProjectConfigurationAsync({
projectDir: commandCtx.projectDir,
exp: commandCtx.exp,
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile,
});
},
getMetadataContext: (): IosMetadataContext => {
if (ctx.workflow === Workflow.MANAGED) {
return { buildSettings: {} };
}
const applicationTarget = findApplicationTarget(targets);
const project = IOSConfig.XcodeUtils.getPbxproj(ctx.projectDir);
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
targetName: applicationTarget.targetName,
buildConfiguration: applicationTarget.buildConfiguration,
});
const buildSettings = xcBuildConfiguration?.buildSettings ?? {};
return { buildSettings };
},
prepareJobAsync: async (
ctx: BuildContext<Platform.IOS>,
jobData: JobData<IosCredentials>
@@ -87,21 +78,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,
});
},
});
}
+5 -3
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,8 +36,9 @@ export async function validateAndSyncProjectConfigurationAsync({
exp: ExpoConfig;
buildProfile: IosBuildProfile;
}): Promise<void> {
const { workflow, autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const { autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
if (isExpoUpdatesInstalled(projectDir)) {
+7 -11
View File
@@ -16,20 +16,16 @@ export async function ensureIosCredentialsAsync(
return;
}
const { projectDir } = buildCtx.commandCtx;
const ctx = await createCredentialsContextAsync(projectDir, {
nonInteractive: buildCtx.commandCtx.nonInteractive,
const credentialsCtx = await createCredentialsContextAsync(buildCtx.projectDir, {
exp: buildCtx.exp,
nonInteractive: buildCtx.nonInteractive,
});
const provider = new IosCredentialsProvider(ctx, {
app: getAppFromContext(ctx),
const provider = new IosCredentialsProvider(credentialsCtx, {
app: getAppFromContext(credentialsCtx),
targets,
iosCapabilitiesOptions: {
entitlements: await resolveEntitlementsJsonAsync(
buildCtx.commandCtx.projectDir,
buildCtx.buildProfile.workflow
),
entitlements: await resolveEntitlementsJsonAsync(buildCtx.projectDir, buildCtx.workflow),
},
distribution: buildCtx.buildProfile.distribution ?? 'store',
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
@@ -45,5 +41,5 @@ export async function ensureIosCredentialsAsync(
}
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
return buildCtx.buildProfile.distribution !== 'simulator';
return !buildCtx.buildProfile.simulator;
}
+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;
}
}
+34 -83
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';
@@ -16,49 +15,32 @@ interface JobData {
buildScheme: string;
}
const cacheDefaults = {
disabled: false,
customPaths: [],
cacheDefaultPaths: true,
};
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.projectDir) || '.';
const username = getUsername(ctx.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: Ios.Job = {
type: ctx.workflow,
platform: Platform.IOS,
projectArchive,
distribution: ctx.buildProfile.distribution,
projectArchive: jobData.projectArchive,
projectRootDirectory,
distribution: ctx.buildProfile.simulator ? 'simulator' : ctx.buildProfile.distribution,
builderEnvironment: {
image: resolveImage(ctx),
node: ctx.buildProfile.node,
@@ -70,19 +52,32 @@ async function prepareJobCommonAsync(
env: ctx.buildProfile.env,
},
cache: {
...cacheDefaults,
...ctx.buildProfile.cache,
clear: ctx.commandCtx.clearCache,
clear: ctx.clearCache,
},
secrets: {
buildCredentials,
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
scheme: jobData.buildScheme,
buildConfiguration: ctx.buildProfile.buildConfiguration,
artifactPath: ctx.buildProfile.artifactPath,
buildType: ctx.buildProfile.developmentClient
? Ios.BuildType.DEVELOPMENT_CLIENT
: Ios.BuildType.RELEASE,
username,
};
return sanitizeJob(job);
}
function resolveImage(ctx: BuildContext<Platform.IOS>): Ios.BuilderEnvironment['image'] {
// see https://linear.app/expo/issue/ENG-1396/make-default-image-dependent-on-sdk-version
if (!ctx.buildProfile.image && ctx.commandCtx.exp.sdkVersion) {
const majorSdkVersion = semver.major(ctx.commandCtx.exp.sdkVersion);
if (!ctx.buildProfile.image && ctx.exp.sdkVersion) {
const majorSdkVersion = semver.major(ctx.exp.sdkVersion);
if (majorSdkVersion <= 41) {
return 'macos-catalina-10.15-xcode-12.4';
}
@@ -99,47 +94,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,
};
}
+14 -7
View File
@@ -4,11 +4,13 @@ import { Platform, Workflow } from '@expo/eas-build-job';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import semver from 'semver';
import type { XCBuildConfiguration } from 'xcode';
import Log from '../../log';
import { resolveWorkflow } from '../../project/workflow';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { evaluateString } from '../utils/template';
import { readPlistAsync, writePlistAsync } from './plist';
export enum BumpStrategy {
@@ -104,12 +106,16 @@ export async function bumpVersionInAppJsonAsync({
export async function readShortVersionAsync(
projectDir: string,
exp: ExpoConfig
exp: ExpoConfig,
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<string | undefined> {
const workflow = resolveWorkflow(projectDir, Platform.IOS);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir);
return infoPlist.CFBundleShortVersionString;
return (
infoPlist.CFBundleShortVersionString &&
evaluateString(infoPlist.CFBundleShortVersionString, buildSettings)
);
} else {
return exp.version;
}
@@ -117,12 +123,13 @@ export async function readShortVersionAsync(
export async function readBuildNumberAsync(
projectDir: string,
exp: ExpoConfig
exp: ExpoConfig,
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<string | undefined> {
const workflow = resolveWorkflow(projectDir, Platform.IOS);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir);
return infoPlist.CFBundleVersion;
return infoPlist.CFBundleVersion && evaluateString(infoPlist.CFBundleVersion, buildSettings);
} else {
return IOSConfig.Version.getBuildNumber(exp);
}
+58 -37
View File
@@ -1,8 +1,9 @@
import { Metadata } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import { Metadata, sanitizeMetadata } from '@expo/eas-build-job';
import { IosEnterpriseProvisioning } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import { 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 +11,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,
@@ -27,69 +28,83 @@ import { isExpoUpdatesInstalled } from './utils/updates';
*/
const packageJSON = require('../../package.json');
export type MetadataContext<T extends Platform> = T extends Platform.ANDROID
? object
: IosMetadataContext;
export interface IosMetadataContext {
buildSettings: XCBuildConfiguration['buildSettings'];
}
export async function collectMetadata<T extends Platform>(
ctx: BuildContext<T>,
{
credentialsSource,
}: {
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
}
platformContext?: MetadataContext<T>
): Promise<Metadata> {
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
return {
// 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>'
const metadata = {
trackingContext: ctx.trackingCtx,
appVersion: await resolveAppVersionAsync(ctx),
appBuildVersion: await resolveAppBuildVersionAsync(ctx),
appVersion: await resolveAppVersionAsync(ctx, platformContext),
appBuildVersion: await resolveAppBuildVersionAsync(ctx, platformContext),
cliVersion: packageJSON.version,
workflow: ctx.buildProfile.workflow,
credentialsSource,
sdkVersion: ctx.commandCtx.exp.sdkVersion,
workflow: ctx.workflow,
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: ctx.exp.runtimeVersion,
...channelOrReleaseChannel,
distribution: ctx.buildProfile.distribution ?? 'store',
appName: ctx.commandCtx.exp.name,
appIdentifier: resolveAppIdentifier(ctx),
buildProfile: ctx.commandCtx.profile,
appName: ctx.exp.name,
appIdentifier: await resolveAppIdentifierAsync(ctx),
buildProfile: ctx.buildProfileName,
gitCommitHash: await vcs.getCommitHashAsync(),
username: getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync()),
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
ctx as BuildContext<Platform.IOS>
),
}),
};
return sanitizeMetadata(metadata);
}
async function resolveAppVersionAsync<T extends Platform>(
ctx: BuildContext<T>
ctx: BuildContext<T>,
platformContext?: MetadataContext<T>
): Promise<string | undefined> {
if (ctx.platform === Platform.IOS) {
return await readShortVersionAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
const iosContext = platformContext as IosMetadataContext | undefined;
return await readShortVersionAsync(ctx.projectDir, ctx.exp, iosContext?.buildSettings ?? {});
} else {
return readVersionName(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return await readVersionNameAsync(ctx.projectDir, ctx.exp);
}
}
async function resolveAppBuildVersionAsync<T extends Platform>(
ctx: BuildContext<T>
ctx: BuildContext<T>,
platformContext?: MetadataContext<T>
): Promise<string | undefined> {
if (ctx.platform === Platform.IOS) {
return await readBuildNumberAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
const iosContext = platformContext as IosMetadataContext | undefined;
return await readBuildNumberAsync(ctx.projectDir, ctx.exp, iosContext?.buildSettings ?? {});
} else {
const versionCode = readVersionCode(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
const versionCode = await readVersionCodeAsync(ctx.projectDir, ctx.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.projectDir, ctx.exp);
} else {
return getApplicationId(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return await getApplicationIdAsync(ctx.projectDir, ctx.exp);
}
}
async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<{ channel: string } | { releaseChannel: string } | null> {
if (!isExpoUpdatesInstalled(ctx.commandCtx.projectDir)) {
if (!isExpoUpdatesInstalled(ctx.projectDir)) {
return null;
}
if (ctx.buildProfile.channel) {
@@ -111,10 +126,10 @@ async function getNativeReleaseChannelAsync<T extends Platform>(
): Promise<string> {
switch (ctx.platform) {
case Platform.ANDROID: {
return (await readAndroidReleaseChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? 'default';
return (await readAndroidReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
}
case Platform.IOS: {
return (await readIosReleaseChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? 'default';
return (await readIosReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
}
default:
return 'default';
@@ -126,12 +141,18 @@ async function getNativeChannelAsync<T extends Platform>(
): Promise<string | undefined> {
switch (ctx.platform) {
case Platform.ANDROID: {
return (await readAndroidChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? undefined;
return (await readAndroidChannelSafelyAsync(ctx.projectDir)) ?? undefined;
}
case Platform.IOS: {
return (await readIosChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? undefined;
return (await readIosChannelSafelyAsync(ctx.projectDir)) ?? undefined;
}
}
return undefined;
}
function resolveIosEnterpriseProvisioning(
ctx: BuildContext<Platform.IOS>
): IosEnterpriseProvisioning | undefined {
return ctx.buildProfile.enterpriseProvisioning;
}
+6
View File
@@ -15,4 +15,10 @@ export enum BuildStatus {
CANCELED = 'canceled',
}
export enum BuildDistributionType {
STORE = 'store',
INTERNAL = 'internal',
SIMULATOR = 'simulator',
}
export type TrackingContext = Record<string, string | number | boolean>;
@@ -7,9 +7,10 @@ import { requestedPlatformDisplayNames } from '../constants';
export function logCredentialsSource(credentialsSource: CredentialsSource, platform: Platform) {
let message = `Using ${credentialsSource} ${requestedPlatformDisplayNames[platform]} credentials`;
if (credentialsSource === CredentialsSource.LOCAL)
if (credentialsSource === CredentialsSource.LOCAL) {
message += ` ${chalk.dim('(credentials.json)')}`;
else if (credentialsSource === CredentialsSource.REMOTE)
} else if (credentialsSource === CredentialsSource.REMOTE) {
message += ` ${chalk.dim('(Expo server)')}`;
}
Log.succeed(message);
}
@@ -1,14 +1,17 @@
import chalk from 'chalk';
import { BuildFragment, BuildStatus as GraphQLBuildStatus } from '../../graphql/generated';
import {
AppPlatform,
BuildFragment,
BuildStatus as GraphQLBuildStatus,
} from '../../graphql/generated';
import formatFields from '../../utils/formatFields';
import { appPlatformDisplayNames } from '../constants';
import { getBuildLogsUrl } from './url';
export function formatGraphQLBuild(build: BuildFragment) {
const actor = getActorName(build);
const account = build.project.__typename === 'App' ? build.project.ownerAccount.name : 'unknown';
const fields: { label: string; value: string }[] = [
const fields: { label: string; value?: string | null }[] = [
{ label: 'ID', value: build.id },
{
label: 'Platform',
@@ -37,15 +40,43 @@ export function formatGraphQLBuild(build: BuildFragment) {
},
{
label: 'Distribution',
value: build.distribution?.toLowerCase() ?? chalk.gray('unknown'),
value: build.distribution?.toLowerCase(),
},
{
label: 'Enterprise Provisioning',
value: build.iosEnterpriseProvisioning?.toLowerCase(),
},
{
label: 'Release Channel',
value: build.releaseChannel ?? chalk.gray('unknown'),
value: build.releaseChannel,
},
{
label: 'Channel',
value: build.channel,
},
{
label: 'SDK Version',
value: build.sdkVersion,
},
{
label: 'Runtime Version',
value: build.runtimeVersion,
},
{
label: 'Version',
value: build.appVersion,
},
{
label: build.platform === AppPlatform.Android ? 'Version code' : 'Build number',
value: build.appBuildVersion,
},
{
label: 'Commit',
value: build.gitCommitHash,
},
{
label: 'Logs',
value: getBuildLogsUrl({ buildId: build.id, account }),
value: getBuildLogsUrl(build),
},
{
label: 'Artifact',
@@ -57,13 +88,13 @@ export function formatGraphQLBuild(build: BuildFragment) {
return '<in progress>';
case GraphQLBuildStatus.Canceled:
case GraphQLBuildStatus.Errored:
return '---------';
return null;
case GraphQLBuildStatus.Finished: {
const url = build.artifacts?.buildUrl;
return url ? url : chalk.red('not found');
}
default:
return 'unknown';
return null;
}
},
},
@@ -81,7 +112,11 @@ export function formatGraphQLBuild(build: BuildFragment) {
{ label: 'Started by', value: actor ?? 'unknown' },
];
return formatFields(fields);
const filteredFields = fields.filter(({ value }) => value !== undefined && value !== null) as {
label: string;
value: string;
}[];
return formatFields(filteredFields);
}
const getActorName = (build: BuildFragment): string => {
@@ -12,51 +12,34 @@ import {
EasBuildDeprecationInfoType,
} from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import {
appPlatformDisplayNames,
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../constants';
import { appPlatformDisplayNames, appPlatformEmojis } from '../constants';
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
export function printLogsUrls(
accountName: string,
builds: { platform: 'android' | 'ios'; buildId: string }[]
): void {
export function printLogsUrls(builds: BuildFragment[]): void {
if (builds.length === 1) {
const { buildId } = builds[0];
const logsUrl = getBuildLogsUrl({
buildId,
account: accountName,
});
Log.log(`Build details: ${chalk.underline(logsUrl)}`);
Log.log(`Build details: ${chalk.underline(getBuildLogsUrl(builds[0]))}`);
} else {
builds.forEach(({ buildId, platform }) => {
const logsUrl = getBuildLogsUrl({
buildId,
account: accountName,
});
builds.forEach(build => {
const logsUrl = getBuildLogsUrl(build);
Log.log(
`${requestedPlatformDisplayNames[platform]} build details: ${chalk.underline(logsUrl)}`
`${appPlatformDisplayNames[build.platform]} build details: ${chalk.underline(logsUrl)}`
);
});
}
}
export function printBuildResults(accountName: string, builds: (BuildFragment | null)[]): void {
export function printBuildResults(builds: (BuildFragment | null)[]): void {
Log.newLine();
if (builds.length === 1) {
const [build] = builds;
assert(build, 'Build should be defined');
printBuildResult(accountName, build);
printBuildResult(build);
} else {
(builds.filter(i => i) as BuildFragment[]).forEach(build =>
printBuildResult(accountName, build)
);
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
}
}
function printBuildResult(accountName: string, build: BuildFragment): void {
function printBuildResult(build: BuildFragment): void {
Log.addNewLineIfNone();
if (build.status === BuildStatus.Errored) {
const userError = build.error;
@@ -80,12 +63,9 @@ function printBuildResult(accountName: string, build: BuildFragment): void {
}
if (build.distribution === DistributionType.Internal) {
const logsUrl = getBuildLogsUrl({
buildId: build.id,
account: accountName,
});
const logsUrl = getBuildLogsUrl(build);
const installUrl = getInternalDistributionInstallUrl(build);
qrcodeTerminal.generate(installUrl, code => 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]
@@ -0,0 +1,5 @@
import template from 'lodash/template';
export function evaluateString(s: string, vars: Record<string, any>): string {
return template(s, { interpolate: /\$\(([\s\S]+?)\)/g })(vars);
}
+9 -8
View File
@@ -3,14 +3,15 @@ import assert from 'assert';
import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api';
import { AppPlatform, BuildFragment } from '../../graphql/generated';
export function getBuildLogsUrl({
buildId,
account,
}: {
buildId: string;
account: string;
}): string {
return `${getExpoWebsiteBaseUrl()}/accounts/${account}/builds/${buildId}`;
export function getBuildLogsUrl(build: BuildFragment): string {
const { project } = build;
if (project.__typename === 'App') {
return `${getExpoWebsiteBaseUrl()}/accounts/${project.ownerAccount.name}/projects/${
project.name
}/builds/${build.id}`;
} else {
return `${getExpoWebsiteBaseUrl()}/builds/${build.id}`;
}
}
export function getArtifactUrl(artifactId: string): string {
@@ -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());
}
}
@@ -67,9 +67,9 @@ async function selectBuildToCancelAsync(
let builds;
try {
const [newBuilds, inQueueBuilds, inProgressBuilds] = await Promise.all([
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.New }),
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.InQueue }),
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.InProgress }),
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.New } }),
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.InQueue } }),
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.InProgress } }),
]);
spinner.stop();
builds = [...newBuilds, ...inQueueBuilds, ...inProgressBuilds];
+198 -105
View File
@@ -1,23 +1,54 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { configureAsync } from '../../build/configure';
import { createCommandContextAsync } from '../../build/context';
import { buildAsync } from '../../build/create';
import { RequestedPlatform } from '../../build/types';
import EasCommand from '../../commandUtils/EasCommand';
import Log, { learnMore } from '../../log';
import { Workflow } from '@expo/eas-build-job';
import {
EasJsonReader,
hasMismatchedExtendsAsync,
isUsingDeprecatedFormatAsync,
migrateAsync,
} from '@expo/eas-json';
import { flags } from '@oclif/command';
import { error, exit } from '@oclif/errors';
import { prepareAndroidBuildAsync } from '../../build/android/build';
import { BuildRequestSender, waitForBuildEndAsync } from '../../build/build';
import { ensureProjectConfiguredAsync } from '../../build/configure';
import { BuildContext, createBuildContextAsync } from '../../build/context';
import { prepareIosBuildAsync } from '../../build/ios/build';
import { Platform, RequestedPlatform } from '../../build/types';
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { BuildFragment, BuildStatus } from '../../graphql/generated';
import Log from '../../log';
import {
EAS_UNAVAILABLE_MESSAGE,
isEasEnabledForProjectAsync,
warnEasUnavailable,
} from '../../project/isEasEnabledForProject';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
import { findProjectRootAsync } from '../../project/projectUtils';
import { confirmAsync, promptAsync } from '../../prompts';
import vcs from '../../vcs';
interface RawBuildFlags {
platform?: string;
'skip-credentials-check': boolean;
'skip-project-configuration': boolean;
profile: string;
'non-interactive': boolean;
local: boolean;
wait: boolean;
'clear-cache': boolean;
}
interface BuildFlags {
requestedPlatform: RequestedPlatform;
skipProjectConfiguration: boolean;
profile: string;
nonInteractive: boolean;
local: boolean;
wait: boolean;
clearCache: boolean;
}
export default class Build extends EasCommand {
static description = 'Start a build';
@@ -55,115 +86,177 @@ export default class Build extends EasCommand {
};
async run(): Promise<void> {
const { flags } = this.parse(Build);
const { flags: rawFlags } = this.parse(Build);
const flags = await this.sanitizeFlagsAsync(rawFlags);
const { requestedPlatform } = flags;
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await vcs.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
const easJsonReader = new EasJsonReader(projectDir);
const platformsToBuild = this.getPlatformsToBuild(requestedPlatform);
const startedBuilds: BuildFragment[] = [];
let metroConfigValidated = false;
for (const platform of platformsToBuild) {
const ctx = await createBuildContextAsync({
buildProfileName: flags.profile,
clearCache: flags.clearCache,
buildProfile: await easJsonReader.readBuildProfileAsync(flags.profile, platform),
local: flags.local,
nonInteractive: flags.nonInteractive,
platform,
projectDir,
skipProjectConfiguration: flags.skipProjectConfiguration,
});
if (ctx.workflow === Workflow.MANAGED && !metroConfigValidated) {
await validateMetroConfigForManagedWorkflowAsync(ctx);
metroConfigValidated = true;
}
if (!ctx.local && !(await isEasEnabledForProjectAsync(ctx.projectId))) {
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
}
const maybeBuild = await this.startBuildAsync(ctx);
if (maybeBuild) {
startedBuilds.push(maybeBuild);
}
}
if (flags.local) {
return;
}
Log.newLine();
printLogsUrls(startedBuilds);
Log.newLine();
if (flags.wait) {
const builds = await waitForBuildEndAsync(startedBuilds.map(build => build.id));
printBuildResults(builds);
this.exitWithNonZeroCodeIfSomeBuildsFailed(builds);
}
}
private async sanitizeFlagsAsync(flags: RawBuildFlags): Promise<BuildFlags> {
const nonInteractive = flags['non-interactive'];
if (!flags.platform && nonInteractive) {
throw new Error('--platform is required when building in non-interactive mode');
}
const platform =
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
const requestedPlatform =
(flags.platform as RequestedPlatform | undefined) ?? (await this.promptForPlatformAsync());
if (flags.local) {
if (requestedPlatform === RequestedPlatform.All) {
error('Builds for multiple platforms are not supported with flag --local', { exit: 1 });
} else if (process.platform !== 'darwin' && requestedPlatform === RequestedPlatform.Ios) {
error('Unsupported platform, macOS is required to build apps for iOS', { exit: 1 });
}
}
if (flags['skip-credentials-check']) {
Log.warnDeprecatedFlag(
'skip-credentials-check',
'Build credential validation is always skipped with the --non-interactive flag. You can also skip interactively.'
'Build credentials validation is always skipped with the --non-interactive flag. You can also skip interactively.'
);
Log.newLine();
}
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
let { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
if (!flags.local && !(await isEasEnabledForProjectAsync(projectId))) {
warnEasUnavailable();
process.exitCode = 1;
return;
}
if (flags.local && !verifyOptionsForLocalBuilds(platform)) {
process.exitCode = 1;
return;
}
exp = (await ensureProjectConfiguredAsync(projectDir)) ?? exp;
const commandCtx = await createCommandContextAsync({
requestedPlatform: platform,
profile: flags.profile,
exp,
projectDir,
projectId,
nonInteractive,
clearCache: flags['clear-cache'],
local: flags.local,
return {
requestedPlatform,
skipProjectConfiguration: flags['skip-project-configuration'],
waitForBuildEnd: flags.wait,
profile: flags['profile'],
nonInteractive,
local: flags['local'],
wait: flags['wait'],
clearCache: flags['clear-cache'],
};
}
private async promptForPlatformAsync(): Promise<RequestedPlatform> {
const { platform } = await promptAsync({
type: 'select',
message: 'Build for platforms',
name: 'platform',
choices: [
{ title: 'All', value: RequestedPlatform.All },
{ title: 'iOS', value: RequestedPlatform.Ios },
{ title: 'Android', value: RequestedPlatform.Android },
],
});
await buildAsync(commandCtx);
return platform;
}
}
function verifyOptionsForLocalBuilds(platform: RequestedPlatform): boolean {
if (platform === RequestedPlatform.All) {
Log.error('Builds for multiple platforms are not supported with flag --local');
return false;
} else if (process.platform !== 'darwin' && platform === RequestedPlatform.Ios) {
Log.error('Unsupported platform, macOS is required to build apps for iOS');
return false;
} else {
return true;
}
}
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
const { platform } = await promptAsync({
type: 'select',
message: 'Build for platforms',
name: 'platform',
choices: [
{
title: 'All',
value: RequestedPlatform.All,
},
{
title: 'iOS',
value: RequestedPlatform.Ios,
},
{
title: 'Android',
value: RequestedPlatform.Android,
},
],
});
return platform;
}
async function ensureProjectConfiguredAsync(projectDir: string): Promise<ExpoConfig | null> {
if (await fs.pathExists(path.join(projectDir, 'eas.json'))) {
return null;
}
const confirm = await confirmAsync({
message: 'This app is not set up for building with EAS. Set it up now?',
});
if (confirm) {
await configureAsync({
projectDir,
platform: RequestedPlatform.All,
});
if (await vcs.hasUncommittedChangesAsync()) {
throw new Error(
'Build process requires clean working tree, please commit all your changes and run `eas build` again'
);
private getPlatformsToBuild(requestedPlatform: RequestedPlatform): Platform[] {
if (requestedPlatform === RequestedPlatform.All) {
return [Platform.ANDROID, Platform.IOS];
} else if (requestedPlatform === RequestedPlatform.Android) {
return [Platform.ANDROID];
} else {
return [Platform.IOS];
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
return exp;
} else {
throw new Error(
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
'https://docs.expo.io/build/eas-json'
)})`
}
private async startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
let sendBuildRequestAsync: BuildRequestSender;
if (ctx.platform === Platform.ANDROID) {
sendBuildRequestAsync = await prepareAndroidBuildAsync(ctx as BuildContext<Platform.ANDROID>);
} else {
sendBuildRequestAsync = await prepareIosBuildAsync(ctx as BuildContext<Platform.IOS>);
}
return await sendBuildRequestAsync();
}
private exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
i => i.status === BuildStatus.Errored
);
if (failedBuilds.length > 0) {
exit(1);
}
}
}
export async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
if (await isUsingDeprecatedFormatAsync(projectDir)) {
const hasMismatchedExtendsKeys = await hasMismatchedExtendsAsync(projectDir);
if (nonInteractive) {
Log.error('We detected that your eas.json is using a deprecated format.');
Log.error(
'We will convert it automatically if run this command without --non-interactive flag. Alternatively, you can update eas.json manually according to https://docs.expo.io/build/eas-json'
);
error('Unsupported eas.json format', { exit: 1 });
}
const confirm = await confirmAsync({
message:
'We detected that your eas.json is using a deprecated format, do you want to migrate to the new format?',
});
if (confirm) {
await migrateAsync(projectDir);
if (hasMismatchedExtendsKeys) {
Log.warn(
'"extends" keyword can only be migrated automatically to the new format if both Android and iOS profiles extend base profiles with the same names for both platforms'
);
Log.warn(
'Migration was successful, but you need to manually adjust the extend rules for your profiles'
);
error('Fix eas.json manually', { exit: 1 });
}
} else {
Log.error(
"Aborting, update your eas.json according to https://docs.expo.io/build/eas-json and run 'eas build' again"
);
error('Unsupported eas.json format', { exit: 1 });
}
}
}
+57 -8
View File
@@ -3,9 +3,13 @@ import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import ora from 'ora';
import { BuildStatus, RequestedPlatform } from '../../build/types';
import { BuildDistributionType, BuildStatus, RequestedPlatform } from '../../build/types';
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
import { AppPlatform, BuildStatus as GraphQLBuildStatus } from '../../graphql/generated';
import {
AppPlatform,
DistributionType,
BuildStatus as GraphQLBuildStatus,
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import {
@@ -31,16 +35,36 @@ export default class BuildList extends Command {
BuildStatus.CANCELED,
],
}),
distribution: flags.enum({
options: [
BuildDistributionType.STORE,
BuildDistributionType.INTERNAL,
BuildDistributionType.SIMULATOR,
],
}),
channel: flags.string(),
appVersion: flags.string(),
appBuildVersion: flags.string(),
sdkVersion: flags.string(),
runtimeVersion: flags.string(),
appIdentifier: flags.string(),
buildProfile: flags.string(),
gitCommitHash: flags.string(),
limit: flags.integer(),
};
async run() {
const { platform: requestedPlatform, status: buildStatus, limit = 10 } = this.parse(
BuildList
).flags;
const { flags } = this.parse(BuildList);
const {
platform: requestedPlatform,
status: buildStatus,
distribution: buildDistribution,
limit = 10,
} = flags;
const platform = toAppPlatform(requestedPlatform);
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution);
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -52,8 +76,19 @@ export default class BuildList extends Command {
try {
const builds = await BuildQuery.allForAppAsync(projectId, {
limit,
platform,
status: graphqlBuildStatus,
filter: {
platform,
status: graphqlBuildStatus,
distribution: graphqlBuildDistribution,
channel: flags.channel,
appVersion: flags.appVersion,
appBuildVersion: flags.appBuildVersion,
sdkVersion: flags.sdkVersion,
runtimeVersion: flags.runtimeVersion,
appIdentifier: flags.appIdentifier,
buildProfile: flags.buildProfile,
gitCommitHash: flags.gitCommitHash,
},
});
if (builds.length) {
@@ -90,7 +125,7 @@ const toAppPlatform = (requestedPlatform?: RequestedPlatform): AppPlatform | und
}
};
const toGraphQLBuildStatus = (buildStatus: BuildStatus): GraphQLBuildStatus | undefined => {
const toGraphQLBuildStatus = (buildStatus?: BuildStatus): GraphQLBuildStatus | undefined => {
if (!buildStatus) {
return undefined;
} else if (buildStatus === BuildStatus.NEW) {
@@ -107,3 +142,17 @@ const toGraphQLBuildStatus = (buildStatus: BuildStatus): GraphQLBuildStatus | un
return GraphQLBuildStatus.Canceled;
}
};
const toGraphQLBuildDistribution = (
buildDistribution?: BuildDistributionType
): DistributionType | undefined => {
if (buildDistribution === BuildDistributionType.STORE) {
return DistributionType.Store;
} else if (buildDistribution === BuildDistributionType.INTERNAL) {
return DistributionType.Internal;
} else if (buildDistribution === BuildDistributionType.SIMULATOR) {
return DistributionType.Simulator;
} else {
return undefined;
}
};
@@ -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);
}
}
@@ -37,9 +37,7 @@ async function promptForRolloutPercentAsync({
return rolloutPercent;
}
function getRolloutInfo(
getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery
): {
function getRolloutInfo(getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery): {
newBranch: Pick<UpdateBranch, 'name' | 'id'>;
oldBranch: Pick<UpdateBranch, 'name' | 'id'>;
currentPercent: number;
@@ -48,12 +46,14 @@ function getRolloutInfo(
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.branchMapping
);
const [newBranchId, oldBranchId] = branchMapping.data.map(d => d.branchId);
const newBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === newBranchId
)[0];
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
const newBranch =
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === newBranchId
)[0];
const oldBranch =
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
if (!newBranch || !oldBranch) {
throw new Error(
@@ -140,9 +140,10 @@ async function startRolloutAsync({
branchMapping: JSON.stringify(newBranchMapping),
});
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
const oldBranch =
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
if (!oldBranch) {
throw new Error(
`Branch mapping is missing its only branch for channel "${channelName}" on app "${fullName}"`
+15 -11
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;
@@ -40,9 +37,11 @@ export type BranchMapping = {
* Get the branch mapping and determine whether it is a rollout.
* Ensure that the branch mapping is properly formatted.
*/
export function getBranchMapping(
branchMappingString?: string
): { branchMapping: BranchMapping; isRollout: boolean; rolloutPercent?: number } {
export function getBranchMapping(branchMappingString?: string): {
branchMapping: BranchMapping;
isRollout: boolean;
rolloutPercent?: number;
} {
if (!branchMappingString) {
throw new Error('Missing branch mapping.');
}
@@ -220,7 +219,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 +248,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:}`
);
+68
View File
@@ -0,0 +1,68 @@
import { getProjectConfigDescription } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { Command, flags } from '@oclif/command';
import Log from '../log';
import { getExpoConfig } from '../project/expoConfig';
import { findProjectRootAsync } from '../project/projectUtils';
import { selectAsync } from '../prompts';
import { handleDeprecatedEasJsonAsync } from './build';
export { Platform } from '@expo/eas-build-job';
export default class Config extends Command {
static description = 'Show the eas.json config';
static flags = {
platform: flags.enum({ char: 'p', options: ['android', 'ios'] }),
profile: flags.string(),
};
async run() {
const { flags } = this.parse(Config);
const { platform: maybePlatform, profile: maybeProfile } = flags as {
platform?: Platform;
profile?: string;
};
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
await handleDeprecatedEasJsonAsync(projectDir, false);
const reader = new EasJsonReader(projectDir);
const profileName =
maybeProfile ??
(await selectAsync(
'Select build profile',
(
await reader.getBuildProfileNamesAsync()
).map(profileName => ({
title: profileName,
value: profileName,
}))
));
const platform =
maybePlatform ??
(await selectAsync('Select platform', [
{
title: 'Android',
value: Platform.ANDROID,
},
{
title: 'iOS',
value: Platform.IOS,
},
]));
const profile = await reader.readBuildProfileAsync(profileName, platform);
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
Log.log(getProjectConfigDescription(projectDir));
Log.newLine();
Log.log(JSON.stringify(config, null, 2));
Log.newLine();
Log.newLine();
Log.log(`Build profile "${profileName}" from eas.json for platform ${platform}`);
Log.newLine();
Log.log(JSON.stringify(profile, null, 2));
}
}
@@ -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'],
@@ -13,9 +13,8 @@ export class AssignFcm {
ctx: Context,
fcm: AndroidFcmFragment
): Promise<CommonAndroidAppCredentialsFragment> {
const appCredentials = await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
this.app
);
const appCredentials =
await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(this.app);
const updatedAppCredentials = await ctx.android.updateAndroidAppCredentialsAsync(
appCredentials,
{
@@ -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';
@@ -23,9 +23,8 @@ export async function canCopyLegacyCredentialsAsync(
return false; // modern credentials already exist
}
const legacyAppCredentials = await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(
app
);
const legacyAppCredentials =
await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(app);
return !!legacyAppCredentials; // user has some legacy credentials
}
@@ -41,16 +40,14 @@ export async function promptUserAndCopyLegacyCredentialsAsync(
const spinner = ora('Classic credentials detected, copying to EAS...').start();
try {
const legacyAppCredentials = await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(
app
);
const legacyAppCredentials =
await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(app);
if (!legacyAppCredentials) {
return;
}
const appCredentials = await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
app
);
const appCredentials =
await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(app);
const legacyFcm = legacyAppCredentials.androidFcm;
if (legacyFcm) {
const clonedFcm = await ctx.android.createFcmAsync(
@@ -86,7 +83,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 +92,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(
@@ -120,9 +117,8 @@ export async function createOrUpdateDefaultAndroidAppBuildCredentialsAsync(
!ctx.nonInteractive,
'createOrUpdateDefaultAndroidAppBuildCredentialsAsync must be run in interactive mode'
);
const existingDefaultBuildCredentials = await ctx.android.getDefaultAndroidAppBuildCredentialsAsync(
appLookupParams
);
const existingDefaultBuildCredentials =
await ctx.android.getDefaultAndroidAppBuildCredentialsAsync(appLookupParams);
if (existingDefaultBuildCredentials) {
return await ctx.android.updateAndroidAppBuildCredentialsAsync(
existingDefaultBuildCredentials,
@@ -25,6 +25,7 @@ export class CreateKeystore {
if (providedKeystore) {
const providedKeystoreWithType = getKeystoreWithType(providedKeystore);
validateKeystore(providedKeystoreWithType);
return providedKeystoreWithType;
}
return await generateRandomKeystoreAsync();
}
@@ -1,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,24 +1,42 @@
import { confirmAsync } from '../../../../prompts';
import { testKeystore } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { askForUserProvidedAsync } from '../../../utils/promptForCredentials';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateKeystore } from '../CreateKeystore';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
jest.mock('../../../utils/promptForCredentials');
describe('CreateKeystore', () => {
it('creates a keystore in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
await createKeystoreAction.runAsync(ctx);
// expect keystore to be created on expo servers
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
});
it('creates a keystore by uploading', async () => {
(askForUserProvidedAsync as jest.Mock).mockImplementation(() => testKeystore);
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
await createKeystoreAction.runAsync(ctx);
// expect keystore to be created on expo servers
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledWith(
appLookupParams.account,
expect.objectContaining(testKeystore)
);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = 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
);
@@ -120,9 +120,8 @@ export async function createAndroidAppBuildCredentialsAsync(
androidKeystoreId: string;
}
): Promise<AndroidAppBuildCredentialsFragment> {
const androidAppCredentials = await createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
appLookupParams
);
const androidAppCredentials =
await createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(appLookupParams);
const buildCredentialsList = androidAppCredentials.androidAppBuildCredentialsList;
const existingDefaultBuildCredentials =
buildCredentialsList.find(buildCredentials => buildCredentials.isDefault) ?? null;
@@ -3,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,
+9 -6
View File
@@ -3,6 +3,7 @@ import chalk from 'chalk';
import pick from 'lodash/pick';
import Log from '../log';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName } from '../project/projectUtils';
import { confirmAsync } from '../prompts';
import { Actor, getActorDisplayName } from '../user/User';
@@ -23,6 +24,7 @@ interface AppleCtxOptions {
interface Options extends AppleCtxOptions {
nonInteractive?: boolean;
exp?: ExpoConfig;
}
export interface Context {
@@ -45,12 +47,13 @@ export async function createCredentialsContextAsync(
): Promise<Context> {
const user = await ensureLoggedInAsync();
let expoConfig: ExpoConfig | undefined;
try {
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
expoConfig = exp;
} catch (error) {
// ignore error, context might be created outside of expo project
let expoConfig: ExpoConfig | undefined = options.exp;
if (!expoConfig) {
try {
expoConfig = getExpoConfig(projectDir);
} catch (error) {
// ignore error, context might be created outside of expo project
}
}
return new CredentialsContext(projectDir, user, expoConfig, options);
@@ -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();
@@ -1,5 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { CredentialsSource, IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import { CredentialsSource, DistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import { CommonIosAppCredentialsFragment } from '../../graphql/generated';
import Log from '../../log';
@@ -18,7 +18,7 @@ import { isAdHocProfile } from './utils/provisioningProfile';
interface Options {
app: App;
targets: Target[];
distribution: IosDistributionType;
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
@@ -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();
});
@@ -67,12 +67,8 @@ export class ConfigureProvisioningProfile {
app: AppLookupParams,
profileFromApple: ProvisioningProfileStoreInfo
): Promise<AppleProvisioningProfileMutationResult> {
const {
developerPortalIdentifier,
certificateP12,
certificatePassword,
serialNumber,
} = this.distributionCertificate;
const { developerPortalIdentifier, certificateP12, certificatePassword, serialNumber } =
this.distributionCertificate;
assert(
certificateP12 && certificatePassword,
'Distribution Certificate P12 and Password is required'
@@ -101,11 +101,12 @@ export class SetupAdhocProvisioningProfile {
const chosenDevices = await chooseDevices(registeredAppleDevices, provisionedDeviceIdentifiers);
// 4. Reuse or create the profile on Apple Developer Portal
const provisioningProfileStoreInfo = await ctx.appStore.createOrReuseAdhocProvisioningProfileAsync(
chosenDevices.map(({ identifier }) => identifier),
this.app.bundleIdentifier,
distCert.serialNumber
);
const provisioningProfileStoreInfo =
await ctx.appStore.createOrReuseAdhocProvisioningProfileAsync(
chosenDevices.map(({ identifier }) => identifier),
this.app.bundleIdentifier,
distCert.serialNumber
);
// 5. Create or update the profile on servers
const appleAppIdentifier = await ctx.ios.createOrGetExistingAppleAppIdentifierAsync(
@@ -1,4 +1,4 @@
import { IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import { DistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
@@ -12,7 +12,7 @@ import { SetupTargetBuildCredentials } from './SetupTargetBuildCredentials';
interface Options {
app: App;
targets: Target[];
distribution: IosDistributionType;
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
@@ -1,4 +1,4 @@
import { IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import { DistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import {
IosDistributionType as GraphQLIosDistributionType,
@@ -14,7 +14,7 @@ import { SetupProvisioningProfile } from './SetupProvisioningProfile';
interface Options {
app: GraphQLAppLookupParams;
distribution: IosDistributionType;
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
@@ -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);
@@ -63,11 +52,12 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction = new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials not to be created or updated on expo servers
@@ -91,11 +81,12 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction = new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
@@ -123,11 +114,12 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction = new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
@@ -156,11 +148,12 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction = new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect no prompts to be called
@@ -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,17 +25,30 @@ const validateBooleanOptions = (options: any): boolean => {
return typeof options === 'boolean';
};
const validatePrefixedStringArrayOptions =
(prefix: string) =>
(options: any): boolean => {
return (
Array.isArray(options) &&
options.every(option => typeof option === 'string' && option.startsWith(prefix))
);
};
const validateStringArrayOptions = (options: any): boolean => {
return Array.isArray(options) && options.every(option => typeof option === 'string');
};
const createValidateStringOptions = (allowed: string[]) => (options: any): boolean => {
return allowed.includes(options);
};
const createValidateStringOptions =
(allowed: string[]) =>
(options: any): boolean => {
return allowed.includes(options);
};
const createValidateStringArrayOptions = (allowed: string[]) => (options: any): boolean => {
return Array.isArray(options) && options.every(option => allowed.includes(option));
};
const createValidateStringArrayOptions =
(allowed: string[]) =>
(options: any): boolean => {
return Array.isArray(options) && options.every(option => allowed.includes(option));
};
const validateDevProdString = createValidateStringOptions(['development', 'production']);
@@ -63,7 +79,9 @@ export async function syncCapabilitiesForEntitlementsAsync(
bundleId: BundleId,
entitlements: JSONObject = {}
): Promise<{ enabled: string[]; disabled: string[] }> {
if (EXPO_NO_CAPABILITY_SYNC) return { enabled: [], disabled: [] };
if (EXPO_NO_CAPABILITY_SYNC) {
return { enabled: [], disabled: [] };
}
const currentCapabilities = await bundleId.getBundleIdCapabilitiesAsync();
if (Log.isDebug) {
Log.log(`Current remote capabilities:\n${JSON.stringify(currentCapabilities, null, 2)}`);
@@ -107,7 +125,7 @@ function getCapabilitiesToEnable(
continue;
}
const existingIndex = currentCapabilities.findIndex(existing =>
const existingIndex = remainingCapabilities.findIndex(existing =>
existing.isType(staticCapabilityInfo.capability)
);
const existing = existingIndex > -1 ? remainingCapabilities[existingIndex] : null;
@@ -120,16 +138,19 @@ function getCapabilitiesToEnable(
remainingCapabilities.splice(existingIndex, 1);
if (Log.isDebug) {
Log.log(`Skipping existing capability: ${key} (${staticCapabilityInfo.name})`);
Log.log(
`Remaining to remove: `,
remainingCapabilities.map(({ id }) => id)
);
}
continue;
}
if (!staticCapabilityInfo.validateOptions(value)) {
throw new Error(`iOS entitlement "${key}" has invalid value "${value}".`);
}
assertValidOptions(staticCapabilityInfo, value);
enabledCapabilityNames.push(staticCapabilityInfo.name);
const option = staticCapabilityInfo.getOptions(value!, entitlements);
const option = staticCapabilityInfo.getOptions(value, entitlements);
request.push({
capabilityType: staticCapabilityInfo.capability,
@@ -140,11 +161,30 @@ function getCapabilitiesToEnable(
return { enabledCapabilityNames, request, remainingCapabilities };
}
export function assertValidOptions(classifier: CapabilityClassifier, value: any): asserts value {
if (!classifier.validateOptions(value)) {
let reason = '';
if (classifier.capabilityIdPrefix) {
// Assert string array matching prefix. ASC will throw if the IDs are invalid, this just saves some time.
reason = ` Expected an array of strings, where each string is prefixed with "${classifier.capabilityIdPrefix}", ex: ["${classifier.capabilityIdPrefix}myapp"]`;
}
throw new Error(
`iOS entitlement "${classifier.entitlement}" has invalid value "${value}".${reason}`
);
}
}
function getCapabilitiesToDisable(
bundleId: BundleId,
currentCapabilities: BundleIdCapability[],
request: { capabilityType: CapabilityType; option: any }[]
) {
if (Log.isDebug) {
Log.log(
`Existing to disable: `,
currentCapabilities.map(({ id }) => id)
);
}
const disabledCapabilityNames: string[] = [];
// Disable any extras that aren't present, this functionality is kinda unreliable because managed apps
@@ -192,17 +232,21 @@ function getCapabilitiesToDisable(
return { disabledCapabilityNames, request };
}
// NOTE(Bacon): From manually toggling values in Xcode and checking the git diff and network requests.
// Last Updated: May 5th, 2021
// https://developer.apple.com/documentation/bundleresources/entitlements
export const CapabilityMapping: {
type CapabilityClassifier = {
name: string;
entitlement: string;
capability: CapabilityType;
validateOptions: (options: any) => boolean;
getOptions: GetOptionsMethod;
capabilityIdModel?: typeof MerchantId;
capabilityIdPrefix?: string;
options?: undefined;
}[] = [
};
// NOTE(Bacon): From manually toggling values in Xcode and checking the git diff and network requests.
// Last Updated: July 22nd, 2021
// https://developer.apple.com/documentation/bundleresources/entitlements
export const CapabilityMapping: CapabilityClassifier[] = [
{
name: 'HomeKit',
entitlement: 'com.apple.developer.homekit',
@@ -287,24 +331,30 @@ export const CapabilityMapping: {
entitlement: 'com.apple.security.application-groups',
capability: CapabilityType.APP_GROUP,
// Ex: ['group.CY-A5149AC2-49FC-11E7-B3F3-0335A16FFB8D.com.cydia.Extender']
validateOptions: validateStringArrayOptions,
validateOptions: validatePrefixedStringArrayOptions('group.'),
getOptions: getDefinedOptions,
capabilityIdModel: AppGroup,
capabilityIdPrefix: 'group.',
},
{
name: 'Apple Pay Payment Processing',
entitlement: 'com.apple.developer.in-app-payments',
capability: CapabilityType.APPLE_PAY,
// Ex: ['merchant.com.example.development']
validateOptions: validateStringArrayOptions,
validateOptions: validatePrefixedStringArrayOptions('merchant.'),
getOptions: getDefinedOptions,
capabilityIdModel: MerchantId,
capabilityIdPrefix: 'merchant.',
},
{
name: 'iCloud',
entitlement: 'com.apple.developer.icloud-container-identifiers',
capability: CapabilityType.ICLOUD,
validateOptions: validateStringArrayOptions,
validateOptions: validatePrefixedStringArrayOptions('iCloud.'),
// Only supports Xcode +6, 5 could be added if needed.
getOptions: getDefinedOptions,
capabilityIdModel: CloudContainer,
capabilityIdPrefix: 'iCloud.',
},
{
name: 'ClassKit',
@@ -313,6 +363,34 @@ export const CapabilityMapping: {
validateOptions: validateDevProdString,
getOptions: getDefinedOptions,
},
{
name: 'Communication Notifications',
entitlement: 'com.apple.developer.usernotifications.communication',
capability: CapabilityType.USER_NOTIFICATIONS_COMMUNICATION,
validateOptions: validateBooleanOptions,
getOptions: getBooleanOptions,
},
{
name: 'Time Sensitive Notifications',
entitlement: 'com.apple.developer.usernotifications.time-sensitive',
capability: CapabilityType.USER_NOTIFICATIONS_TIME_SENSITIVE,
validateOptions: validateBooleanOptions,
getOptions: getBooleanOptions,
},
{
name: 'Group Activities',
entitlement: 'com.apple.developer.group-session',
capability: CapabilityType.GROUP_ACTIVITIES,
validateOptions: validateBooleanOptions,
getOptions: getBooleanOptions,
},
{
name: 'Family Controls',
entitlement: 'com.apple.developer.family-controls',
capability: CapabilityType.FAMILY_CONTROLS,
validateOptions: validateBooleanOptions,
getOptions: getBooleanOptions,
},
{
// https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_default-data-protection
name: 'Data Protection',
@@ -0,0 +1,120 @@
import { BundleId, CapabilityTypeOption } from '@expo/apple-utils';
import { JSONObject } from '@expo/json-file';
import Log from '../../../log';
import {
CapabilityMapping,
EXPO_NO_CAPABILITY_SYNC,
assertValidOptions,
} from './bundleIdCapabilities';
type UpdateCapabilityRequest = Parameters<BundleId['updateBundleIdCapabilityAsync']>[0];
/**
* Sync the capability identifiers with the bundle identifier capabilities.
* If a capability identifier is missing, then attempt to create it.
* Link all of the capability identifiers at the same time after parsing the entitlements file.
*
* @param bundleId Bundle identifier object.
* @param entitlements JSON representation of the iOS entitlements plist
*
* @returns an object specifying the capabilities that were linked, and created.
*/
export async function syncCapabilityIdentifiersForEntitlementsAsync(
bundleId: BundleId,
entitlements: JSONObject = {}
): Promise<{ created: string[]; linked: string[] }> {
if (EXPO_NO_CAPABILITY_SYNC) {
return { created: [], linked: [] };
}
const createdIds: string[] = [];
const linkedIds: string[] = [];
const CapabilityIdMapping = CapabilityMapping.filter(capability => capability.capabilityIdModel);
const updateRequest: UpdateCapabilityRequest = [];
// Iterate through the supported capabilities to build the request.
for (const classifier of CapabilityIdMapping) {
const CapabilityModel = classifier.capabilityIdModel;
// Skip capabilities that don't support capability IDs.
if (!CapabilityModel) {
continue;
}
const validate = (value: any): value is string[] => {
if (!value) {
return false;
}
assertValidOptions(classifier, value);
return true;
};
// Skip capabilities that aren't defined in the entitlements file.
const entitlementValue = entitlements[classifier.entitlement];
if (!validate(entitlementValue)) {
continue;
}
// Remove any duplicates to cut down on network requests
const capabilityIds: string[] = [...new Set(entitlementValue)];
// Get a list of all of the capability IDs that are already created on the server.
const existingIds = await CapabilityModel.getAsync(bundleId.context);
// A list of server IDs for linking.
const capabilityIdOpaqueIds = [];
// Iterate through all the local IDs and see if they exist on the server.
for (const localId of capabilityIds) {
let remoteIdModel = existingIds.find(model => model.attributes.identifier === localId);
// If a remote ID exists, then create it.
if (!remoteIdModel) {
if (Log.isDebug) {
Log.log(`Creating capability ID: ${localId} (${CapabilityModel.type})`);
}
try {
remoteIdModel = await CapabilityModel.createAsync(bundleId.context, {
identifier: localId,
});
} catch (error) {
// Add a more helpful error message.
error.message += `\n\nRemove the value '${localId}' from the array '${classifier.entitlement}' in the iOS project entitlements.\nIf you know that the ID is registered to one of your apps, try again with a different Apple account.`;
throw error;
}
// Create a list of newly created IDs for displaying in the CLI.
createdIds.push(localId);
if (Log.isDebug) {
Log.log(`Created capability ID: ${remoteIdModel.id}`);
}
}
if (Log.isDebug) {
Log.log(`Linking ID to ${CapabilityModel.type}: ${localId} (${remoteIdModel.id})`);
}
// Create a list of linked IDs for displaying in the CLI.
linkedIds.push(remoteIdModel.attributes.identifier);
capabilityIdOpaqueIds.push(remoteIdModel.id);
}
updateRequest.push({
capabilityType: classifier.capability,
option: CapabilityTypeOption.ON,
relationships: {
// One of: `merchantIds`, `appGroups`, `cloudContainers`.
[CapabilityModel.type]: capabilityIdOpaqueIds,
},
});
}
if (updateRequest.length) {
if (Log.isDebug) {
Log.log(`Updating bundle identifier with capability identifiers:`, updateRequest);
}
await bundleId.updateBundleIdCapabilityAsync(updateRequest);
} else if (Log.isDebug) {
Log.log(`No capability identifiers need to be updated`);
}
return { created: createdIds, linked: linkedIds };
}
@@ -6,6 +6,7 @@ import ora from 'ora';
import Log from '../../../log';
import { AuthCtx, getRequestContext } from './authenticate';
import { syncCapabilitiesForEntitlementsAsync } from './bundleIdCapabilities';
import { syncCapabilityIdentifiersForEntitlementsAsync } from './capabilityIdentifiers';
import { assertContractMessagesAsync } from './contractMessages';
export interface IosCapabilitiesOptions {
@@ -95,8 +96,6 @@ export async function syncCapabilities(
bundleId,
entitlements
);
const buildMessage = (title: string, items: string[]) =>
items.length ? `${title}: ${items.join(', ')}` : '';
const results =
[buildMessage('Enabled', enabled), buildMessage('Disabled', disabled)]
.filter(Boolean)
@@ -107,6 +106,43 @@ export async function syncCapabilities(
spinner.fail(`Failed to sync capabilities ${chalk.dim(bundleId.attributes.identifier)}`);
throw err;
}
// Always run this after syncing the capabilities...
await syncCapabilityIdentifiersAsync(bundleId, { entitlements });
}
const buildMessage = (title: string, items: string[]) =>
items.length ? `${title}: ${items.join(', ')}` : '';
export async function syncCapabilityIdentifiersAsync(
bundleId: BundleId,
{ entitlements }: IosCapabilitiesOptions
): Promise<void> {
const spinner = ora(`Syncing capabilities identifiers`).start();
// Stop spinning in debug mode so we can print other information
if (Log.isDebug) {
spinner.stop();
}
try {
const { created, linked } = await syncCapabilityIdentifiersForEntitlementsAsync(
bundleId,
entitlements
);
const results =
[buildMessage('Created', created), buildMessage('Linked', linked)]
.filter(Boolean)
.join(' | ') || 'No updates';
spinner.succeed(`Synced capability identifiers: ` + chalk.dim(results));
} catch (err) {
spinner.fail(
`Failed to sync capability identifiers ${chalk.dim(bundleId.attributes.identifier)}`
);
throw err;
}
}
export async function ensureAppExistsAsync(
@@ -73,9 +73,8 @@ async function findProfileByBundleIdAsync(
// there is an expo managed profile with our desired certificate
// return the profile that will be valid for the longest duration
return {
profile: expoProfilesWithCertificate.sort(sortByExpiration)[
expoProfilesWithCertificate.length - 1
],
profile:
expoProfilesWithCertificate.sort(sortByExpiration)[expoProfilesWithCertificate.length - 1],
didUpdate: false,
};
} else if (expoProfiles) {
@@ -136,10 +135,11 @@ async function manageAdHocProfilesAsync(
if (profileId) {
existingProfile = await findProfileByIdAsync(context, profileId, bundleId);
// Fail if we cannot find the profile that was specifically requested
if (!existingProfile)
if (!existingProfile) {
throw new Error(
`Could not find profile with profile id "${profileId}" for bundle id "${bundleId}"`
);
}
} else {
// If no profile id is passed, try to find a suitable provisioning profile for the App ID.
const results = await findProfileByBundleIdAsync(context, bundleId, certSerialNumber);
@@ -227,16 +227,12 @@ export async function createOrReuseAdhocProvisioningProfileAsync(
const spinner = ora(`Handling Apple ad hoc provisioning profiles`).start();
try {
const context = getRequestContext(authCtx);
const {
didUpdate,
didCreate,
profileName,
...adhocProvisioningProfile
} = await manageAdHocProfilesAsync(context, {
udids,
bundleId: bundleIdentifier,
certSerialNumber: distCertSerialNumber,
});
const { didUpdate, didCreate, profileName, ...adhocProvisioningProfile } =
await manageAdHocProfilesAsync(context, {
udids,
bundleId: bundleIdentifier,
certSerialNumber: distCertSerialNumber,
});
if (didCreate) {
spinner.succeed(`Created new profile: ${profileName}`);
@@ -11,6 +11,7 @@ export interface App {
export interface Target {
targetName: string;
buildConfiguration?: string;
bundleIdentifier: string;
parentBundleIdentifier?: string;
}
@@ -21,12 +21,10 @@ describe('print credentials', () => {
},
projectName: 'test52',
};
const testIosAppCredentialsData = await IosAppCredentialsQuery.withCommonFieldsByAppIdentifierIdAsync(
'@quinlanj/test52',
{
const testIosAppCredentialsData =
await IosAppCredentialsQuery.withCommonFieldsByAppIdentifierIdAsync('@quinlanj/test52', {
appleAppIdentifierId: 'test-id',
}
);
});
const appCredentials = {
test52: nullthrows(testIosAppCredentialsData),
};
@@ -41,7 +41,9 @@ function getService(rootUrl: string) {
},
replacement(content: string, node: any) {
let href = node.getAttribute('href');
if (href.startsWith('/')) href = `${rootUrl}${href}`;
if (href.startsWith('/')) {
href = `${rootUrl}${href}`;
}
if (terminalLink.isSupported) {
return chalk.cyan(terminalLink(content, href));
}
@@ -162,14 +162,8 @@ function displayIosAppBuildCredentials(buildCredentials: IosAppBuildCredentialsF
const maybeProvProf = buildCredentials.provisioningProfile;
Log.log(` Provisioning Profile:`);
if (maybeProvProf) {
const {
expiration,
updatedAt,
status,
developerPortalIdentifier,
appleTeam,
appleDevices,
} = maybeProvProf;
const { expiration, updatedAt, status, developerPortalIdentifier, appleTeam, appleDevices } =
maybeProvProf;
if (developerPortalIdentifier) {
Log.log(` Developer Portal ID: ${developerPortalIdentifier}`);
}
@@ -1,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);
}
}
}
Loaded 100 of 175 files, more files were not shown because too many files have changed in this diff. Show more