Compare commits

...
163 Commits
Author SHA1 Message Date
Quinlan Jung 0df16236a2 v0.18.3 2021-06-28 14:54:56 -07:00
Quinlan Jung ecaf5a4318 changelog stuff 2021-06-28 14:44:00 -07:00
Quinlan Jung 86e5e1a7b1 [eas-cli] integrate push key setup in ios builds (#473)
* [eas-cli] integrate push key setup in build flow

* nits

* fix lint

* pr feedback

* pr feedback
2021-06-28 14:43:15 -07:00
Quinlan Jung ab10094071 [eas-cli] remove nullthrows on keystore keyPassword (#484)
* [eas-cli] remove nullthrows on keystore keyPassword

* changelog
2021-06-28 10:34:45 -07:00
Dominik Sokal 2d536d73fe mock fs-extra so tests don't produce files (#480) 2021-06-28 09:43:43 +02:00
Evan Bacon 80ea8ee50b Support bundle identifiers that start with a dot (#459) 2021-06-25 15:41:48 -07:00
Brent Vatne b49c5f47d1 Update CHANGELOG 2021-06-25 15:16:06 -07:00
Brent Vatne a9da78946b v0.18.2 2021-06-25 14:57:32 -07:00
Brent Vatne b6d9accea4 [eas-cli] Fix inverted conditional so we actually only prompt about commit when index is dirty (#481) 2021-06-25 14:54:26 -07:00
Quinlan Jung 4611a65bce [eas-cli] show projects that depend on a push key (#472) 2021-06-25 13:55:16 -07:00
Quinlan Jung db29b3fc18 [eas-cli] make apple team optional in appropriate cases (#468)
* [eas-cli] make apple team optional in appropriate cases

* pr feedback
2021-06-25 13:53:37 -07:00
Dominik Sokal 2534a46bd3 [ENG-1396] change default ios image for sdk <= 41 (#479)
* change default ios image for sdk <= 41

* update CHANGELOG
2021-06-25 15:23:24 +02:00
Dominik Sokal 7def4b502e remove redundant type 2021-06-25 13:58:38 +02:00
Brent Vatne 4ea26d7ca3 Remove leftover CHANGELOG entry 2021-06-24 17:56:14 -07:00
Brent Vatne c4c8075b1f Update CHANGELOG 2021-06-24 17:55:06 -07:00
Brent Vatne 266545d2a0 v0.18.1 2021-06-24 17:43:58 -07:00
Brent Vatne ddd7046513 Add entry to CHANGELOG 2021-06-24 17:43:14 -07:00
Quinlan Jung 7a44eeb22b [eas-cli] add basic printing support for Apple App Specific Password (#474) 2021-06-24 17:42:00 -07:00
Brent Vatne 5bcf8263cf [eas-cli] Automatically migrate build credentials to EAS (#477) 2021-06-24 17:41:17 -07:00
Dominik Sokal f380ba0106 update CHANGELOG 2021-06-24 13:35:32 +02:00
Dominik Sokal 1a6d81f68e v0.18.0 2021-06-24 12:17:08 +02:00
Dominik Sokal 522818113b support new build status: new (#475)
* support new build status: 'new'

* update CHANGELOG
2021-06-24 12:07:44 +02:00
Hayden Daly f93d347665 [docs] update 'See code' links in README (#446)
Why
Majority of the See code links in the README resolve to nonexistent paths causing 404s. For example, build/commands/account/login.ts should resolve to src/commands/account/login.ts.

How
Simple find and replace with the links and updating relevant text using patch-readme.js script added to package.json.

Test Plan
Clicked through the links to make sure they all resolve to right locations.
2021-06-23 14:52:36 -07:00
Wojciech KozyraandDominik Sokal e96cee1c3b [eas-cli] support different vcs (#462)
* [eas-cli] add abstraction around VCS, implement git and local version

* Apply suggestions from code review

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

* review feedback

* update CHANGELOG

Co-authored-by: Dominik Sokal <[email protected]>
2021-06-23 12:05:31 +02:00
Quinlan Jung 4d46e0a8ac changelog stuff 2021-06-22 13:02:58 -07:00
Quinlan Jung 80e35c7524 [eas-cli] add 'remove push key' action (#470)
* [eas-cli] remove push key action

* todo
2021-06-22 12:23:31 -07:00
Quinlan Jung 186c26bc99 [eas-cli] setup push key action (#469) 2021-06-22 11:49:26 -07:00
jkh be6a7f54f9 [eas-cli][eas-json] add updates.channel to build metadata (#461) 2021-06-21 10:29:31 -07:00
Ville Immonen fff7aac5b6 Add an internal distribution with dev client to build:configure defaults (#465) 2021-06-21 12:51:27 +03:00
Quinlan Jung 0d90fbd3ff [eas-cli] create and assign push key actions (#467) 2021-06-18 10:32:05 -07:00
Quinlan Jung e71779049e [eas-cli] rename android mocks 2021-06-17 21:24:38 -07:00
Quinlan Jung 7da909e3c1 [eas-cli] fix android manager push description 2021-06-17 21:15:13 -07:00
Quinlan Jung e439fd14a9 changelog stuff 2021-06-17 11:19:45 -07:00
Ville Immonen ffed1b446a Record dev client builds created and downloaded to analytics (#454) 2021-06-17 15:28:38 +03:00
Quinlan Jung 7b65412ec6 [eas-cli] swap android manager for the refactored beta one (#460)
* [eas-cli] swap the android manager with the beta one

* [eas-cli] remove unused classes, remove new directory

* [eas-cli] delete credentials manager class

* [eas-cli] replace API newAndroid -> android

* [eas-cli] printCredentialsBeta -> printCredentials
2021-06-16 09:55:55 -07:00
Quinlan Jung f1cbdcc094 changelog stuff 2021-06-15 15:56:11 -07:00
Quinlan Jung ec5573efa6 [eas-cli] add push key graphql types (#456) 2021-06-15 11:14:33 -07:00
Quinlan JungandDominik Sokal 242da62271 [eas-cli] action to upload/download credentials.json to www (#455)
* [eas-cli] action to upload/download credentials.json to www

* Update packages/eas-cli/src/credentials/android/actions/new/SetupBuildCredentialsFromCredentialsJson.ts

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

* Update packages/eas-cli/src/credentials/android/actions/UpdateCredentialsJson.ts

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-06-15 11:14:15 -07:00
Wojciech Kozyra 23d1221ef9 [eas-cli] add support for big sur image for ios builds (#457)
* [eas-cli] add support for big sure image for ios builds

* update CHANGELOG
2021-06-15 16:20:43 +02:00
Quinlan Jung 5402d8c74f changelog stuff 2021-06-14 19:02:47 -07:00
Dominik Sokal 176026645c fix typo: Expo CLI -> EAS CLI 2021-06-14 15:45:30 +02:00
Brent Vatne b0d4ed6c0e [eas-cli] Deprecate --skip-credentials-check (#442) 2021-06-10 20:27:06 -07:00
Quinlan Jung 2f7ac71921 [eas-cli] remove android fcm/keystore actions (#451) 2021-06-10 11:50:08 -07:00
Quinlan Jung b599315a85 [eas-cli] more manager dx improvements (#449)
* [eas-cli] improve ios manager dx

* add go back
2021-06-10 11:49:45 -07:00
Quinlan Jung e86d8aa77a [eas-cli] add additionalTypenames for creds mutation to invalidate urql cache 2021-06-09 19:38:16 -07:00
Quinlan Jung ab4ac87d52 [eas-cli] download and backup keystore action (#447)
* [eas-cli] download and backup keystore action

* pr feedback
2021-06-09 15:42:57 -07:00
Quinlan Jung af98fb1621 [eas-cli] refactor ios manager (#448) 2021-06-09 13:11:04 -07:00
Dominik Sokal 0ce7c1b3b4 clean up legacy stuff (#444)
* drop support for npm token in credentials.json

* remove --allow-experimantal flag

* update CHANGELOG
2021-06-09 14:31:30 +02:00
Dominik Sokal 2bdaa0d428 [ENG-1271] make iOS credentials manager work with multi-target projects (#441)
* make function for selecting Xcode scheme generic

* move XcodeBuildContext resolver

* adjust ios credentials manager to multitarget projects

* update CHANGELOG

* address PR feedback
2021-06-09 13:56:17 +02:00
Quinlan Jung 2486a4de97 [ENG-1242][eas-cli] copy Expo Classic credentials to EAS if user is eligible (#445)
* [eas-cli] copy Expo Classic credentials to EAS if user is eligible

* fix tsc

* fix lint

* pr feedback
2021-06-08 22:17:14 -07:00
kgc00 6137b215e9 [eas-cli] Introduce action stat (#437)
* Introduces a new abstract class for commands to inherit from -- EasCommand
* EasCommand does two things, login and analytics:
  * Reads local credentials or initiates login (depending on command requirements) 
  * Bootstraps analytics, sends an 'action' stat to track cli usage, and flushes events at the end of the commands lifecycle
* Build and Submit now subclass EasCommand
* Tests for EasCommand are included
2021-06-07 10:54:14 -07:00
Quinlan Jung 265ba16594 [eas-cli] refactor credential input types (#443)
* [eas-cli] use generated input types

* pulled cleaned up schema from www

* fix lint
2021-06-07 10:33:56 -07:00
Quinlan Jung a7bc3d0c98 [eas-cli] action to select android build credentials (#438)
* [eas-cli] action to select android build credentials

* oops

* pr feedback
2021-06-07 10:33:36 -07:00
Quinlan Jung ee618880d7 [eas-cli] fcm create and assign graphql actions (#440) 2021-06-04 14:03:57 -07:00
Quinlan Jung 2c95c8c310 [eas-cli] add fcm types for graphql (#439) 2021-06-04 14:02:38 -07:00
Dominik Sokal b90e37d9fc update CHANGELOG 2021-06-02 13:19:35 +02:00
Dominik Sokal 34563070f4 v0.17.0 2021-06-02 12:07:46 +02:00
Dominik Sokal 66b269f4bf update CHANGELOG 2021-06-02 12:06:41 +02:00
Brent Vatne 168cb4d4a0 [eas-cli] Fix git repo root path on Windows (#429)
* Fix git repo root path on Windows

* Update CHANGELOG
2021-06-02 12:03:33 +02:00
Quinlan Jung 91b531da43 [eas-cli] use correct method to determine android application identifier (#431)
* [eas-cli] use correct method to determine android application identifier

* pr feedback
2021-06-02 12:00:51 +02:00
Quinlan Jung 2e0d567361 update changelog 2021-06-01 11:59:51 -07:00
Quinlan Jung 81e6280cff [eas-cli] use new android build credentials provider (#427)
* [eas-cli] use new android build credentials provider

* pr feedback

* fix tsc

* pr feedback:
2021-06-01 11:56:37 -07:00
Quinlan Jung e2fda7b9d3 [eas-cli] refactor application id configuration (#432) 2021-06-01 10:34:08 -07:00
Dominik Sokal ede30807f5 fix bundle identifier getter (#434)
* fix bundle identifier getter

* update CHANGELOG
2021-06-01 14:25:03 +02:00
Brent Vatne 9f354013fa Update CHANGELOG 2021-05-26 15:09:59 -07:00
Brent Vatne 30552f134c v0.16.0 2021-05-26 14:49:42 -07:00
Brent Vatne e4beba9a21 Add env var to disable capability sync and extra logs when EXPO_DEBUG is set (#426) 2021-05-26 13:11:42 -07:00
Dominik Sokal b71ab7ec84 implement managed credentials support for multitarget iOS projects (#414)
* ignore --skip-credentials-check flag for ios builds

* account for parent bundle identifier when creating apple app identifier

* implement managed credentials support for multitarget ios projects

* update CHANGELOG.md

* address PR feedback

* upgrade @expo/config-plugins 1.0.32 -> 1.0.33

* fix lint warning

* address more PR feedback

* address PR feedback
2021-05-26 11:06:29 +02:00
Brent Vatne 596cfc372b Move updating the release on Github to after the release is completed (#418) 2021-05-25 15:56:55 +02:00
Dominik Sokal d5f6c5680c update CHANGELOG 2021-05-25 15:55:23 +02:00
Dominik Sokal 28c508ff62 v0.15.1 2021-05-25 15:44:36 +02:00
Dominik Sokal 97ff6f406d fix sending appBuildVersion to api (#423)
* generate graphql code

* fix sending appBuildVersion to api

* update CHANGELOG
2021-05-25 15:40:22 +02:00
Evan Bacon bd39b296fa Use config introspection to get all entitlements (#419)
* Use config introspection to get all entitlements

* Update CHANGELOG.md

* Updated getAppBuildGradle -> getAppBuildGradleFilePath

* Update RemoveProvisioningProfile.ts

* Update view.ts

* Update applicationId.ts

* Update entitlements.ts
2021-05-24 17:30:13 -07:00
Quinlan Jung bfbe1e930b [eas-cli] add tests for unencrypted/empty password keystores (#396)
* [eas-cli] add tests for unencrypted/empty password keystores

* detect unencrypted pem

* much sad

* much sad 2

* much sad

* much sad
2021-05-24 16:25:07 -07:00
jkh 3fe45176cc [eas-cli] make republish work smoothly with different platforms (#417) 2021-05-21 10:11:57 -07:00
Test User bffa6fbf03 Release 0.15.0 2021-05-20 14:58:23 -07:00
Test User 161808bc37 v0.15.0 2021-05-20 14:30:18 -07:00
jkh ac42b15acd [eas-cli] fix bug when publishing without platform flag (#415) 2021-05-20 07:55:48 -07:00
Dominik Sokal 47d21de4eb add appBuildVersion to build metadata (#413)
* add functions for reading buildNumber / versionCode

* add buildNumber and versionCode to metadata

* read proper app version for generic projects

* bump @expo/eas-build-job to 0.2.35

* address PR feedback

* update CHANGELOG.md
2021-05-20 11:40:17 +02:00
Wojciech Kozyra e12ffa9ebc docs for introducing breaking changes in local builds api (#408)
* docs for introducing breaking changes in local builds api

* review feedback
2021-05-19 14:21:35 +02:00
Dominik Sokal 2944023986 clean up test output 2021-05-19 11:36:30 +02:00
Dominik Sokal 49c1e7670d warn about deprecated android.package/ios.bundleIdentifier only once 2021-05-18 11:28:31 +02:00
Dominik Sokal b157b901e3 simplify app identifier management (#407)
* simplify app identifier management

* remove disableIosBundleIdentifierValidation from eas.json

* update CHANGELOG.md

* add unit tests

* address PR feedback
2021-05-18 09:42:58 +02:00
Brent Vatne 1a1f88f202 [eas-json] Include development-client in valid buildType for internal distribution (#410) 2021-05-17 18:14:07 -07:00
jkh d02d70da91 [eas-cli] put projectId caching behind EAS_ENABLE_PROJECT_ID flag (#412) 2021-05-17 12:48:36 -07:00
Evan Bacon c37f2957bb Bump @expo/[email protected] to support Apple server errors (#411) 2021-05-17 11:27:23 -07:00
Wojciech Kozyra a32f5fe57a [eas-cli] allow empty keystore password and optional key password (#398)
* [eas-cli] allow empty keystore password and optional key password

* update @expo/config and @expo/config-plugins

* update CHNAGELOG
2021-05-17 12:39:49 +02:00
Test User b7f6d36a63 [eas-cli] hide 'eas init' 2021-05-14 18:05:11 -07:00
Mike Hampton 2975a025ac Add expoCli parameter. (#409) 2021-05-14 15:36:05 -04:00
Wojciech Kozyra f4e396dde2 [eas-cli] fix validation for empty password in dist cert (#403)
* [eas-cli] fix validation for empty password in dist cert

* add tests

* change error message

* fix test description

* review feedback
2021-05-14 10:44:48 +02:00
Evan Bacon ef54911323 Dynamically toggle capabilities before build (#384)
* Enable associated-domains

* Update CHANGELOG.md

* Added support for native projects

* Update ensureAppExists-test.ts

* Validate entitlements from JSON

* removed unneeded tests

* Update bundleIdCapabilities.ts

* use Workflow

* Forward props everywhere

* refactor

* Added return types

* Update entitlements.ts

* Update bundleIdCapabilities.ts

* Update bundleIdCapabilities.ts

* Update bundleIdCapabilities.ts

* cleanup

* IosCapabilitiesOptions
2021-05-13 20:25:02 -07:00
Quinlan Jung 18b1074232 [eas-cli] setup android build credentials with graphql (#406)
* [eas-cli] setup android build credentials with graphql

* pr feedback
2021-05-13 16:37:28 -07:00
Jonathan 小林 Hales 8468e0c5ea [eas-cli] make branch name optional for branch:publish 2021-05-12 12:54:43 -07:00
jkh 436d5c9ec9 [eas-cli] query app by ID instead of fullName in listBranchesAsync (#405) 2021-05-12 10:04:05 -07:00
jkh 1f88f7e1a3 [eas-cli] replace ensureProjectExistsAsync with getProjectIdAsync (#404) 2021-05-12 09:34:55 -07:00
jkh 09682754f8 [eas-cli] add 'eas init' command (#402) 2021-05-12 07:49:05 -07:00
Mike HamptonandWojciech Kozyra 8e3d84b1e5 [eas-cli] Support buildConfiguration parameter. (#394)
* [eas-cli] Support buildConfiguration parameter.

* Update CHANGELOG.md

* Address PR comments.

* Update CHANGELOG.md

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

* Update new features in changelog.

Co-authored-by: Wojciech Kozyra <[email protected]>
2021-05-12 10:21:01 -04:00
Dominik Sokal 8bb2631192 Revert "[eas-cli] set default urql requestPolicy to network-only (#397)"
This reverts commit d97ed80867.

The proper fix has been implemented in #399.
2021-05-12 10:10:44 +02:00
Dominik Sokal 38a7fb6266 set empty save-prefix (#400) 2021-05-12 10:06:30 +02:00
Dominik Sokal 0ac2fb77a1 Pin versions in package.json (#399)
* pin versions in package.json

* update CHANGELOG.md
2021-05-12 10:04:09 +02:00
jkh f12774b539 [eas-cli] display the platforms supported by an update group (#391) 2021-05-11 09:03:47 -07:00
jkh d50bdedcec [eas-cli] add --platform flag to branch:publish command (#389) 2021-05-11 08:22:48 -07:00
Dominik Sokal 8d443e5992 update CONTRIBUTING.md 2021-05-11 12:01:52 +02:00
Dominik Sokal 2b5fb6de53 update CHANGELOG.md 2021-05-11 11:57:33 +02:00
Dominik Sokal dc86f27199 v0.14.1 2021-05-11 11:23:22 +02:00
jkh d97ed80867 [eas-cli] set default urql requestPolicy to network-only (#397)
* [eas-cli] set default urql requestPolicy to network-only

* add changelog
2021-05-11 11:14:55 +02:00
Quinlan Jung 3601b448fe [eas-cli] create keystore action (#387)
* [eas-cli] create android keystore action

* 1: some pr feedback
2021-05-10 16:38:17 -07:00
Quinlan Jung 68aab13d51 [eas-cli] Keystore utils and fixtures (#386)
* [eas-cli] Keystore utils and fixtures

validate using pem decryption instead

* 0: some pr feedback

* pr feedback
2021-05-10 16:25:07 -07:00
Brent Vatne 6cef13d756 Update CHANGELOG 2021-05-10 11:36:59 -07:00
Brent Vatne 1d45941f5d v0.14.0 2021-05-10 11:28:46 -07:00
Quinlan Jung 5e5badf11f [eas-cli] credentials manager DX nits (#383) 2021-05-03 19:16:14 -07:00
jkh 840c65946b [eas-cli] remove --force to fix eas publish (#381) 2021-05-03 15:35:52 -07:00
Quinlan Jung fdbd786126 [eas-cli] move from deprecated iosAppCredentialsArray to iosAppCredentialsList (#380) 2021-05-03 13:16:33 -07:00
Quinlan Jung d5365275e0 [eas-cli] beta android credentials manager (#378) 2021-04-29 12:07:20 -07:00
Quinlan Jung 886e48d3ee [eas-cli] android graphql types (#377) 2021-04-29 11:17:18 -07:00
Wojciech Kozyra 342c78a4f6 [eas-cli] disable pending build check (#373)
* [eas-cli] disable pending builds check

* update CHANGELOG.md
2021-04-28 15:13:51 +02:00
Axel Delafosse a483d94e26 [eas-cli] assert (#372) 2021-04-28 11:54:33 +02:00
Axel DelafosseandDominik Sokal b40412a7a1 [eas-cli] add QR code to install internal distribution build on device (#371)
* [website] add qr code to install internal distribution build on device

* [eas-cli] update changelog

* [eas-cli] sort imports

* [eas-cli] sort imports

* [eas-cli] improve getInstallUrl typing

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

* [eas-cli] fix typing

* [eas-cli] clean code

Co-authored-by: Dominik Sokal <[email protected]>
2021-04-28 11:43:21 +02:00
Quinlan Jung d652e04f7b [eas-cli] getAppAsync not used anywhere outside (#369) 2021-04-27 13:09:10 -07:00
Quinlan Jung 85326d1104 [eas-cli] merge beta and main cred utils (#368) 2021-04-27 13:08:51 -07:00
Quinlan Jung 28c087138b [eas-cli] replace printCredentials with printCredentialsBeta (#367) 2021-04-27 13:08:30 -07:00
jkh 8badf14ec3 [eas-cli] create asset keys without an extension (#366)
* Clients must be using expo-asset >= 8.3.2 in order to handle this new update format.
2021-04-27 06:02:12 -07:00
Wojciech Kozyra 4f1db29ef8 [eas-cli] increase build wait timeout (#370)
* [eas-cli] increase build wait timeout

* Update CHANGELOG.md
2021-04-27 14:19:23 +02:00
Quinlan Jung 2354dc1b71 update changelog 2021-04-26 12:16:21 -07:00
Quinlan Jung f106bfa6d7 [eas-cli] replace ctx.ios with ctx.newIos (#365)
remove old ctx.ios

ctx.newIos -> ctx.ios

fix lint

unused fixtures

remove unused fixtures

merge fixtures

fix lint
2021-04-26 12:11:36 -07:00
Quinlan Jung 1a7edbea06 [eas-cli] merge everything from actions/new to actions (#364)
delete unused actions

delete old inner class

remove uneeded dist cert utils

merge dist cert utils

remove unused functions of pprofile utils

pop out new directory
2021-04-26 12:10:27 -07:00
Quinlan Jung 5bfaee9e4d [eas-cli] swap beta credentials manager with current (#363) 2021-04-26 12:07:35 -07:00
Quinlan Jung 0cb2309d7d [eas-cli] add additionalTypenames to delete mutations to clear urql cache 2021-04-23 13:43:50 -07:00
Quinlan Jung 0415a1d97f changelog stuff 2021-04-23 12:47:59 -07:00
Quinlan Jung 249c60f4d1 [eas-cli] add remove provisioning profile to beta manager (#361) 2021-04-23 12:33:48 -07:00
Quinlan Jung c357cc378c [eas-cli] add high level actions to beta manager (#360) 2021-04-23 12:29:14 -07:00
Quinlan Jung f7c0d731b6 changelog stuff" 2021-04-22 22:48:38 -07:00
Quinlan Jung e852e3edac [eas-cli] add CreateDistCert and UseExistingDistCert to beta manager (#357)
* [eas-cli] add create and add existing dist cert options

* nits:

* fix dx
2021-04-22 17:27:17 -07:00
Quinlan Jung 2a283b74a3 [eas-cli][eas-json] add getBuildProfileNamesAsync helper (#351)
* [eas-cli][eas-json] getProfileNames helper

* tweak name

* pr feedback

* pr feedback
2021-04-22 13:47:18 -07:00
Quinlan Jung c749bc2372 [eas-cli] refactor UpdateCredentialsJson (#352)
* [eas-cli] refactor UpdateCredentialsJson

* fix broken tests from switching b64 pprofile test fixture

* fix lint

* add to beta manager
2021-04-22 12:02:45 -07:00
Brent Vatne 9c60f7190e Update CHANGELOG 2021-04-22 10:49:49 -07:00
Brent Vatne 8126b2276e v0.13.0 2021-04-22 10:16:06 -07:00
Dominik Sokal e1de21015f implement SetupInternalProvisioningProfile (#344) 2021-04-22 10:01:35 -07:00
Dominik Sokal 61e505f1f8 fix typo 2021-04-22 16:00:39 +02:00
Wojciech KozyraandDominik Sokal 2e3611161e fix api deprecation error (#359)
* fix api deprecation error

* update CHANGELOG.md

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

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-04-22 14:08:25 +02:00
Dominik Sokal 95f5eaaacf update changelog 2021-04-22 11:40:42 +02:00
Dominik Sokal 87a3ea99e2 v0.12.0 2021-04-22 10:32:28 +02:00
Quinlan Jung ddab65a55d [eas-cli] adhoc opposite name (#358) 2021-04-22 10:29:37 +02:00
Wojciech Kozyra 0cf8f8ce0d [eas-cli] support --clear-cache (#355)
* [eas-cli] support --clear-cache

* update CHANGELOG.md

* fix param description
2021-04-22 10:17:09 +02:00
Dominik Sokal 2b531ec70c update contributing doc (#353) 2021-04-22 10:04:44 +02:00
Dominik Sokal 6cd7966904 send username as build metadata property (#354)
* send username as build metadata property

* update changelog

* upgrade @expo/eas-build-job to 0.2.27
2021-04-21 14:38:25 +02:00
Dominik Sokal ad72ab990f Update CHANGELOG.md 2021-04-21 09:20:08 +02:00
Brent Vatne b53412fc00 Update CHANGELOG 2021-04-20 15:29:06 -07:00
Brent Vatne cbb2de9e11 v0.11.1 2021-04-20 15:00:15 -07:00
Brent Vatne 593c42f601 [eas-cli] Bump @expo/apple-utils to fix initial iOS app capabilities sync 2021-04-20 14:59:27 -07:00
Quinlan Jung 84b10b9dad [eas-cli] fix lint 2021-04-20 14:14:40 -07:00
Quinlan Jung 0f286061e4 [eas-cli] tests for SelectIosDistributionTypeGraphqlFromBuildProfile 2021-04-20 14:13:44 -07:00
Quinlan Jung c8aad01480 [eas-cli] non interactive support, aesthetic nits 2021-04-20 14:12:12 -07:00
Quinlan Jung d70a6bee69 [eas-cli] resolve ios distribution types (#348)
* [eas-cli] resolve ios distribution types

* pr feedback

* pr feedback
2021-04-20 12:46:55 -07:00
Dominik Sokal 67799b5c54 update changelog 2021-04-20 15:36:23 +02:00
Dominik Sokal 05b8eef622 v0.11.0 2021-04-20 15:08:57 +02:00
Dominik Sokal 50e56b5bc4 fix building APKs for Android managed projects (#349)
* fix building APKs for Android managed projects

* update CHANGELOG
2021-04-20 14:39:58 +02:00
Dominik Sokal 30ab4e62ec simplify credentials providers - remove auto option (#345)
* simplify credentials providers - remove auto option

* update changelog

* address PR feedback
2021-04-20 10:39:37 +02:00
Dominik Sokal a1d901294b allow for registering devices when building app for internal distribution (#340)
* allow for registering devices when building app for internal distribution

* update CHANGELOG
2021-04-19 12:39:20 +02:00
Dominik Sokal eed818cfe5 refactor setting up adhoc provisioning profile (#338)
* refactor appstore provisioning profile validation

* refactor SetupAdhocProvisioningProfile

* update CHANGELOG

* address PR feedback
2021-04-19 12:29:37 +02:00
Dominik Sokal db0b6bfcf5 remove redundant finally block 2021-04-16 15:54:44 +02:00
Dominik Sokal da81c1c6df remove incorrect usage example from secret:list command 2021-04-16 14:16:41 +02:00
Dominik Sokal 42d3296f07 update CHANGELOG 2021-04-16 14:13:09 +02:00
273 changed files with 14680 additions and 7751 deletions

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

+1
View File
@@ -3,6 +3,7 @@
# Checklist
- [ ] I've added an entry to [CHANGELOG.md](https://github.com/expo/eas-cli/blob/main/CHANGELOG.md#main) if necessary.
- [ ] I've tagged the changelog entry with `[EAS BUILD API]` if it's a part of a breaking change to EAS Build API (only possible when updating `@expo/eas-build-job` package).
# Why
+1
View File
@@ -0,0 +1 @@
save-prefix ""
+196 -1
View File
@@ -6,6 +6,202 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
### 🎉 New features
- 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
- Change default iOS image for projects with Expo SDK <= 41 (SDK 41 won't build with Xcode 12.5). ([#479](https://github.com/expo/eas-cli/pull/479) by [@dsokal](https://github.com/dsokal))
- Make Apple team optional in appropriate cases. ([#468](https://github.com/expo/eas-cli/pull/468) by [@quinlanj](https://github.com/quinlanj))
- Show projects that depend on a push key. ([#472](https://github.com/expo/eas-cli/pull/472) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Fix inverted conditional so we actually only prompt about commit when index is dirty. ([#481](https://github.com/expo/eas-cli/pull/481) by [@brentvatne](https://github.com/brentvatne))
## [0.18.1](https://github.com/expo/eas-cli/releases/tag/v0.18.1) - 2021-06-24
### 🎉 New features
- Add basic printing support for Apple App Specific Password. ([#474](https://github.com/expo/eas-cli/pull/474) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Fix CLI UI getting blocked on credentials migration, leading to partially migrated state. ([#477](https://github.com/expo/eas-cli/pull/477) by [@brentvatne](https://github.com/brentvatne))
## [0.18.0](https://github.com/expo/eas-cli/releases/tag/v0.18.0) - 2021-06-24
### 🛠 Breaking changes
- Drop support for `experimental.npmToken` in credentials.json, EAS Secrets can be used instead. ([#444](https://github.com/expo/eas-cli/pull/444) by [@dsokal](https://github.com/dsokal))
- Remove `--allow-experimental` flag from `eas build:configure` as it has no effect now. ([#444](https://github.com/expo/eas-cli/pull/444) by [@dsokal](https://github.com/dsokal))
### 🎉 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))
- New version of Android credentials manager. ([#460](https://github.com/expo/eas-cli/pull/460) by [@quinlanj](https://github.com/quinlanj))
- Add an internal distribution with dev client to `build:configure` defaults. ([#465](https://github.com/expo/eas-cli/pull/465) by [@fson](https://github.com/fson))
- Add `updates.channel` to build metadata. ([#461](https://github.com/expo/eas-cli/pull/461) by [@jkhales](https://github.com/jkhales))
- iOS push key setup and management now available in `eas-cli credentials`. ([#469](https://github.com/expo/eas-cli/pull/469) [#470](https://github.com/expo/eas-cli/pull/470) by [@quinlanj](https://github.com/quinlanj))
- Support new build status: `new`. ([#475](https://github.com/expo/eas-cli/pull/475) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Deprecate `--skip-credentials-check` flag because it doesn't do anything and is no longer needed. ([#442](https://github.com/expo/eas-cli/pull/442) by [@brentvatne](https://github.com/brentvatne))
- Move android credentials code to new Graphql API. ([#439](https://github.com/expo/eas-cli/pull/439) [#440](https://github.com/expo/eas-cli/pull/440) [#438](https://github.com/expo/eas-cli/pull/438) [#443](https://github.com/expo/eas-cli/pull/443) [#447](https://github.com/expo/eas-cli/pull/447) [#451](https://github.com/expo/eas-cli/pull/451) [#455](https://github.com/expo/eas-cli/pull/455) by [@quinlanj](https://github.com/quinlanj))
- Prepare Graphql infra to support iOS push keys. ([#456](https://github.com/expo/eas-cli/pull/456) by [@quinlanj](https://github.com/quinlanj))
- Improve credentials DX ([#448](https://github.com/expo/eas-cli/pull/448) [#449](https://github.com/expo/eas-cli/pull/449) by [@quinlanj](https://github.com/quinlanj))
- Add analytics on dev client builds. ([#454](https://github.com/expo/eas-cli/pull/454) by [@fson](https://github.com/fson))
- Support non-git projects. ([#462](https://github.com/expo/eas-cli/pull/462) by [@wkozyra95](https://github.com/wkozyra95))
## [0.17.0](https://github.com/expo/eas-cli/releases/tag/v0.17.0) - 2021-06-02
### 🐛 Bug fixes
- Fix bundle identifier resolution when native target is not provided. ([#434](https://github.com/expo/eas-cli/pull/434) by [@dsokal](https://github.com/dsokal))
- Fix git repo root path getter on Windows. ([#429](https://github.com/expo/eas-cli/pull/429) by [@brentvatne](https://github.com/brentvatne))
- Fix resolving Android application identifier. ([#431](https://github.com/expo/eas-cli/pull/431) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Android credentials setup now on Graphql API. ([#434](https://github.com/expo/eas-cli/pull/427) by [@quinlanj](https://github.com/quinlanj))
## [0.16.0](https://github.com/expo/eas-cli/releases/tag/v0.16.0) - 2021-05-26
### 🎉 New features
- Opt out of capability syncing with `EXPO_NO_CAPABILITY_SYNC=1`. ([#426](https://github.com/expo/eas-cli/pull/426) by [@brentvatne](https://github.com/brentvatne))
- Add more verbose logging around capability syncing to help debug reported issues. ([#426](https://github.com/expo/eas-cli/pull/426) by [@brentvatne](https://github.com/brentvatne))
- Add managed credentials support for multi-target iOS projects. ([#414](https://github.com/expo/eas-cli/pull/414) by [@dsokal](https://github.com/dsokal))
## [0.15.1](https://github.com/expo/eas-cli/releases/tag/v0.15.1) - 2021-05-25
### 🎉 New features
- Support auto capabilities in managed workflow using Expo config plugin introspection. ([#419](https://github.com/expo/eas-cli/pull/419) by [@EvanBacon](https://github.com/EvanBacon))
### 🐛 Bug fixes
- Fix sending `appBuildVersion` as part of the build metadata. ([#423](https://github.com/expo/eas-cli/pull/423) by [@dsokal](https://github.com/dsokal))
## [0.15.0](https://github.com/expo/eas-cli/releases/tag/v0.15.0) - 2021-05-20
### 🛠 Breaking changes
- `[EAS BUILD API]` Remove "Auto" option for `schemeBuildConfiguration` and make the old "Auto" behaviour the default. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
- Remove `experimental.disableIosBundleIdentifierValidation` flag from eas.json. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
- Deprecate `android.package` and `ios.bundleIdentifier` in app config for generic projects. EAS CLI depends on the values in native code now. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
- Remove application id synchronization (`android.package` and `ios.bundleIdentifier`) between app.json and native code when running builds. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Auto sync associated domains capability before building. ([#384](https://github.com/expo/eas-cli/pull/384) by [@EvanBacon](https://github.com/EvanBacon))
- Add eas init command. [#402](https://github.com/expo/eas-cli/pull/402) by [@jkhales](https://github.com/jkhales))
- Allow for arbitrary string values in `schemeBuildConfiguration`. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
- Allow for installing custom `expo-cli` version on EAS Build. ([#409](https://github.com/expo/eas-cli/pull/409) by [@randomhajile](https://github.com/randomhajile))
- Support PKCS kesytores for Android. ([#398](https://github.com/expo/eas-cli/pull/398) by [@wkozyra95](https://github.com/wkozyra95))
- Support empty passwords in Android and iOS keystores. ([#398](https://github.com/expo/eas-cli/pull/398) by [@wkozyra95](https://github.com/wkozyra95))
- Add more build metadata - `appBuildVersion`. ([#413](https://github.com/expo/eas-cli/pull/413) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix failure when publishing without the platform flag. ([#415](https://github.com/expo/eas-cli/pull/415) by [@jkhales](https://github.com/jkhales))
- Pin versions in package.json. ([#399](https://github.com/expo/eas-cli/pull/399) by [@dsokal](https://github.com/dsokal))
- Revert [0ac2f](https://github.com/expo/eas-cli/commit/0ac2fb77a118df609381c4e350aa68609340c3cd) as the root cause of the issue has been fixed in [#399](https://github.com/expo/eas-cli/pull/399).
- Include development-client in valid buildType for internal distribution. ([#410](https://github.com/expo/eas-cli/pull/410) by [@brentvatne](https://github.com/brentvatne))
## [0.14.1](https://github.com/expo/eas-cli/releases/tag/v0.14.1) - 2021-05-11
### 🐛 Bug fixes
- Make the default urql requestPolicy 'network-only'. ([#397](https://github.com/expo/eas-cli/pull/397) by [@jkhales](https://github.com/jkhales))
## [0.14.0](https://github.com/expo/eas-cli/releases/tag/v0.14.0) - 2021-05-10
### 🛠 Breaking changes
- Make eas branch:publish work with expo-cli >= 4.4.3 ([#366](https://github.com/expo/eas-cli/pull/366) by [@jkhales](https://github.com/jkhales))
- Create asset keys without an extension. ([#366](https://github.com/expo/eas-cli/pull/366) by [@jkhales](https://github.com/jkhales))
### 🎉 New features
- Display platforms supported by an update group ([#391](https://github.com/expo/eas-cli/pull/391) by [@jkhales](https://github.com/jkhales))
- Add a --platform flag to branch:publish command ([#389](https://github.com/expo/eas-cli/pull/389) by [@jkhales](https://github.com/jkhales))
- Release new credentials manager. ([#363](https://github.com/expo/eas-cli/pull/363) by [@quinlanj](https://github.com/quinlanj))
- Add QR code to install internal distribution build on device. ([#371](https://github.com/expo/eas-cli/pull/371) by [@axeldelafosse](https://github.com/axeldelafosse))
### 🧹 Chores
- Cleanup unused code. ([#364](https://github.com/expo/eas-cli/pull/364), [#365](https://github.com/expo/eas-cli/pull/365) by [@quinlanj](https://github.com/quinlanj))
- Redesign UX in beta credentials manager. ([#360](https://github.com/expo/eas-cli/pull/360) by [@quinlanj](https://github.com/quinlanj))
- Port more options to the beta credentials manager. ([#352](https://github.com/expo/eas-cli/pull/352), [357](https://github.com/expo/eas-cli/pull/357), [361](https://github.com/expo/eas-cli/pull/361) by [@quinlanj](https://github.com/quinlanj))
- Add getBuildProfileNamesAsync helper to EasJsonReader. ([#351](https://github.com/expo/eas-cli/pull/351) by [@quinlanj](https://github.com/quinlanj))
- Increase build timeout to 1 hour. ([#370](https://github.com/expo/eas-cli/pull/370) by [@wkozyra95](https://github.com/wkozyra95))
- Remove check for pending builds. ([#373](https://github.com/expo/eas-cli/pull/373) by [@wkozyra95](https://github.com/wkozyra95))
## [0.13.0](https://github.com/expo/eas-cli/releases/tag/v0.13.0) - 2021-04-22
### 🎉 New features
- Implement offline distribution certificate validation when running a build in non-interactive mode. ([#344](https://github.com/expo/eas-cli/pull/344) by [@dsokal](https://github.com/dsokal))
- Add support for building internal distribution apps for Apple Enterprise Teams. ([#344](https://github.com/expo/eas-cli/pull/344) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Display descriptive error message when API for EAS Build changes. ([#359](https://github.com/expo/eas-cli/pull/359) by [@wkozyra95](https://github.com/wkozyra95))
## [0.12.0](https://github.com/expo/eas-cli/releases/tag/v0.12.0) - 2021-04-22
### 🎉 New features
- Add the initiating user's username to the build metadata. ([#354](https://github.com/expo/eas-cli/pull/354) by [@dsokal](https://github.com/dsokal))
- Add `--clear-cache` flag for `eas build`. ([#355](https://github.com/expo/eas-cli/pull/355) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Fix the bug where the account name was used as the username. ([#354](https://github.com/expo/eas-cli/pull/354) by [@dsokal](https://github.com/dsokal))
## [0.11.1](https://github.com/expo/eas-cli/releases/tag/v0.11.1) - 2021-04-20
### 🎉 New features
- Resolve ios distribution types. ([#348](https://github.com/expo/eas-cli/pull/348) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Bump version of `@expo/apple-utils` to fix capabilities sync immediately after initial identifier registration. ([593c4](https://github.com/expo/eas-cli/commit/593c42f601396deba1751489343ec07b0a8876da) by [@brentvatne](https://github.com/brentvatne))
## [0.11.0](https://github.com/expo/eas-cli/releases/tag/v0.11.0) - 2021-04-20
### 🛠 Breaking changes
- `"credentialsSource": "auto"` is now deprecated. The option will use `"remote"` as the default value. ([#345](https://github.com/expo/eas-cli/pull/345) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Add validation when setting up ad-hoc provisioning profile in non-interactive mode. ([#338](https://github.com/expo/eas-cli/pull/338) by [@dsokal](https://github.com/dsokal))
- Allow for registering devices when running an ad-hoc build for the first time. ([#340](https://github.com/expo/eas-cli/pull/340) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix the bug where all Android managed builds produced AAB archives. The `buildType` property from `eas.json` is not ignored now. ([#349](https://github.com/expo/eas-cli/pull/349) by [@dsokal](https://github.com/dsokal))
## [0.10.0](https://github.com/expo/eas-cli/releases/tag/v0.10.0) - 2021-04-16
### 🛠 Breaking changes
- `secrets:<COMMAND>` is renamed to `secret:<COMMAND>` ([#315](https://github.com/expo/eas-cli/pull/315) by [@fiberjw](https://github.com/fiberjw))
- `secret:delete` now accepts `id` through a flag rather than an argument ([#315](https://github.com/expo/eas-cli/pull/315) by [@fiberjw](https://github.com/fiberjw))
@@ -44,7 +240,6 @@ This is the log of notable changes to EAS CLI and related packages.
- Fix the issue where the first iOS build fails for a project without `ios.bundleIdentifier` set in `app.json`. ([#319](https://github.com/expo/eas-cli/pull/319) by [@dsokal](https://github.com/dsokal))
- Bump @expo/config-plugins to fix `eas build:configure` on Windows. ([d0f3e](https://github.com/expo/eas-cli/commit/d0f3e387e7f31dbce4c3b93b8d083c7d20d9e30d) by [@brentvatne](https://github.com/brentvatne))
- Replace REST API calls with GraphQL counterparts. ([#333](https://github.com/expo/eas-cli/pull/333) by [@barthap](https://github.com/barthap))
## [0.8.1](https://github.com/expo/eas-cli/releases/tag/v0.8.1) - 2021-04-06
+31 -2
View File
@@ -2,9 +2,38 @@
1. Run `yarn release` in the repository root folder. This will create a new tag like `v1.0.0`.
1. **Wait until the `release` GitHub workflow has finished.**
1. Once the release is ready to go, you can go to https://github.com/expo/eas-cli/releases and mark it as not draft.
- Remember to copy the changelog entries from `CHANGELOG.md`.
![Wait for all three jobs to complete](./.gh-assets/release-workflow.png)
1. Run `yarn lerna publish from-git` in the repository root folder. This will publish the packages to npm.
- This command works only if `HEAD` is on the release commit, e.g. when `git log -1` prints:
```
commit cbb2de9e118bfbcb5a597b261d2dbef0bb5d4b0b (tag: v0.11.1)
Author: Brent Vatne <[email protected]>
Date: Tue Apr 20 15:00:15 2021 -0700
v0.11.1
```
- If you run `yarn lerna publish from-git` on any other commit, it'll print something like:
```
lerna notice from-git No tagged release found
lerna success No changed packages to publish
```
- If you ever run into the above issue, use `git checkout RELEASE_COMMIT_HASH` to checkout to the release commit. Later, when you've published the packages to npm, run `git checkout main` to go back to the latest commit.
1. Update `CHANGELOG.md`.
- Add the new section with the release version in the title.
- Leave the `main` section empty (without any changelog entries).
1. Once the release is ready to go, you can go to https://github.com/expo/eas-cli/releases and mark it as not draft.
- Remember to copy the changelog entries from `CHANGELOG.md`.
- Before hitting the submit button, make sure there are 5 assets available under the release entry.
![There are 5 assets for each release](./.gh-assets/draft-release.png)
1. If any of the changelog entries have been tagged with `[EAS BUILD API]`
- Open `package.json` and note the version of `@expo/eas-build-job` used in the eas-cli.
- Find the `Publish` commit in [expo/eas-build](https://github.com/expo/eas-build/commit/) for that version. In most cases, it will be the latest `Publish` commit.
- Find the `eas-cli-local-build-plugin` version published in that commit.
![example publish commit](./.gh-assets/eas-build-publish-commit.png)
- Run `npm dist-tags add eas-cli-local-build-plugin@VERSION latest` with the version from the previous step.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.10.0"
"version": "0.18.3"
}
+27 -28
View File
@@ -49,7 +49,7 @@ eas --help COMMAND
* [`eas help [COMMAND]`](#eas-help-command)
* [`eas secret:create`](#eas-secretcreate)
* [`eas secret:delete`](#eas-secretdelete)
* [`eas secrets:list`](#eas-secretslist)
* [`eas secret:list`](#eas-secretlist)
* [`eas submit --platform=(android|ios)`](#eas-submit---platformandroidios)
* [`eas webhook:create`](#eas-webhookcreate)
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
@@ -69,7 +69,7 @@ ALIASES
$ eas login
```
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/account/login.ts)_
_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)_
## `eas account:logout`
@@ -83,7 +83,7 @@ ALIASES
$ eas logout
```
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/account/logout.ts)_
_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)_
## `eas account:view`
@@ -97,7 +97,7 @@ ALIASES
$ eas whoami
```
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/account/view.ts)_
_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)_
## `eas analytics [STATUS]`
@@ -108,7 +108,7 @@ USAGE
$ eas analytics [STATUS]
```
_See code: [build/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/analytics.ts)_
## `eas build`
@@ -120,15 +120,15 @@ USAGE
OPTIONS
-p, --platform=(android|ios|all)
--clear-cache Clear cache before the build
--local Run build locally [experimental]
--non-interactive Run command in --non-interactive mode
--profile=profile [default: release] Name of the build profile from eas.json
--skip-credentials-check Skip validation of build credentials
--skip-project-configuration Skip project configuration
--[no-]wait Wait for build(s) to complete
```
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/build/index.ts)_
_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)_
## `eas build:cancel [BUILD_ID]`
@@ -139,7 +139,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [build/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/build/cancel.ts)_
_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)_
## `eas build:configure`
@@ -151,10 +151,9 @@ USAGE
OPTIONS
-p, --platform=(android|ios|all) Platform to configure
--allow-experimental Enable experimental configuration steps.
```
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/build/configure.ts)_
_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)_
## `eas build:list`
@@ -167,10 +166,10 @@ USAGE
OPTIONS
--limit=limit
--platform=(all|android|ios)
--status=(in-queue|in-progress|errored|finished|canceled)
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/build/list.ts)_
_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)_
## `eas build:view [BUILD_ID]`
@@ -181,7 +180,7 @@ USAGE
$ eas build:view [BUILD_ID]
```
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/build/view.ts)_
_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)_
## `eas credentials`
@@ -192,7 +191,7 @@ USAGE
$ eas credentials
```
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -203,7 +202,7 @@ USAGE
$ eas device:create
```
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/device/create.ts)_
_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)_
## `eas device:list`
@@ -217,7 +216,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/device/list.ts)_
_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)_
## `eas device:view [UDID]`
@@ -228,7 +227,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/device/view.ts)_
_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)_
## `eas help [COMMAND]`
@@ -261,7 +260,7 @@ OPTIONS
--value=value Value of the secret
```
_See code: [build/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/secret/create.ts)_
_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)_
## `eas secret:delete`
@@ -278,18 +277,18 @@ DESCRIPTION
Unsure where to find the secret's ID? Run eas secrets:list
```
_See code: [build/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/secret/delete.ts)_
_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)_
## `eas secrets:list`
## `eas secret:list`
Lists environment secrets available for your current app
```
USAGE
$ eas secrets:list
$ eas secret:list
```
_See code: [build/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/secret/list.ts)_
_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)_
## `eas submit --platform=(android|ios)`
@@ -369,7 +368,7 @@ EXAMPLES
and provide its App ID
```
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/submit.ts)_
## `eas webhook:create`
@@ -388,7 +387,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [build/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/webhook/create.ts)_
_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)_
## `eas webhook:delete [ID]`
@@ -402,7 +401,7 @@ ARGUMENTS
ID ID of the webhook to delete
```
_See code: [build/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/webhook/delete.ts)_
_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)_
## `eas webhook:list`
@@ -416,7 +415,7 @@ OPTIONS
--event=(BUILD) Event type that triggers the webhook
```
_See code: [build/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/webhook/list.ts)_
_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)_
## `eas webhook:update`
@@ -436,7 +435,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [build/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/webhook/update.ts)_
_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)_
## `eas webhook:view ID`
@@ -450,5 +449,5 @@ ARGUMENTS
ID ID of the webhook to view
```
_See code: [build/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.10.0/build/commands/webhook/view.ts)_
_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)_
<!-- commandsstop -->
+1
View File
@@ -3,6 +3,7 @@ schema: 'https://staging.exp.host/--/graphql'
documents:
- 'src/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/commands/**/*.ts'
generates:
src/graphql/generated.ts:
File diff suppressed because it is too large. Load diff
+66 -60
View File
@@ -1,69 +1,74 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.10.0",
"version": "0.18.3",
"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.17",
"@expo/config": "~3.3.19",
"@expo/config-plugins": "1.0.24",
"@expo/eas-build-job": "0.2.22",
"@expo/eas-json": "^0.10.0",
"@expo/json-file": "^8.2.24",
"@expo/plist": "^0.0.11",
"@expo/plugin-warn-if-update-available": "^1.7.0",
"@expo/results": "^1.0.0",
"@expo/sdk-runtime-versions": "^1.0.0",
"@expo/spawn-async": "^1.5.0",
"@hapi/joi": "^17.1.1",
"@oclif/command": "^1",
"@oclif/config": "^1",
"@oclif/errors": "^1.3.3",
"@oclif/plugin-help": "^3",
"@urql/core": "^2.0.0",
"@urql/exchange-retry": "^0.2.0",
"chalk": "^4.1.0",
"cli-table3": "^0.6.0",
"dateformat": "^3.0.3",
"env-paths": "^2.2.0",
"fast-glob": "^3.2.4",
"figures": "^3.2.0",
"form-data": "^3.0.0",
"fs-extra": "^9.0.1",
"getenv": "^1.0.0",
"got": "^11.5.2",
"graphql": "^15.3.0",
"graphql-tag": "^2.11.0",
"indent-string": "^4.0.0",
"keychain": "^1.3.0",
"lodash": "^4.17.20",
"mime": "^2.4.7",
"minimatch": "^3.0.4",
"node-fetch": "^2.6.1",
"node-forge": "^0.10.0",
"nullthrows": "^1.1.1",
"ora": "^5.1.0",
"pkg-dir": "^4.2.0",
"progress": "^2.0.3",
"prompts": "^2.3.2",
"qrcode-terminal": "^0.12.0",
"semver": "^7.3.4",
"strip-ansi": "^6.0.0",
"tar": "^6.0.5",
"tempy": "^0.7.0",
"terminal-link": "^2.1.1",
"timeago.js": "^4.0.2",
"tslib": "^1",
"turndown": "^7.0.0",
"untildify": "^4.0.0",
"uuid": "^8.3.0",
"wrap-ansi": "^7.0.0"
"@amplitude/identify": "1.5.0",
"@amplitude/node": "1.5.0",
"@expo/apple-utils": "0.0.0-alpha.20",
"@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/json-file": "8.2.25",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.11",
"@expo/plugin-warn-if-update-available": "1.7.0",
"@expo/results": "1.0.0",
"@expo/sdk-runtime-versions": "1.0.0",
"@expo/spawn-async": "1.5.0",
"@hapi/joi": "17.1.1",
"@oclif/command": "1.8.0",
"@oclif/config": "1.17.0",
"@oclif/errors": "1.3.3",
"@oclif/plugin-help": "3.2.0",
"@urql/core": "2.0.0",
"@urql/exchange-retry": "0.2.0",
"chalk": "4.1.0",
"cli-table3": "0.6.0",
"dateformat": "3.0.3",
"env-paths": "2.2.0",
"fast-glob": "3.2.4",
"figures": "3.2.0",
"form-data": "3.0.0",
"fs-extra": "9.0.1",
"getenv": "1.0.0",
"got": "11.5.2",
"graphql": "15.3.0",
"graphql-tag": "2.11.0",
"ignore": "5.1.8",
"indent-string": "4.0.0",
"jks-js": "1.0.0",
"keychain": "1.3.0",
"lodash": "4.17.20",
"mime": "2.4.7",
"minimatch": "3.0.4",
"nanoid": "3.1.23",
"node-fetch": "2.6.1",
"node-forge": "0.10.0",
"nullthrows": "1.1.1",
"ora": "5.1.0",
"pkg-dir": "4.2.0",
"progress": "2.0.3",
"prompts": "2.3.2",
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
"semver": "7.3.4",
"strip-ansi": "6.0.0",
"tar": "6.0.5",
"tempy": "0.7.0",
"terminal-link": "2.1.1",
"timeago.js": "4.0.2",
"tslib": "1.14.1",
"turndown": "7.0.0",
"untildify": "4.0.0",
"uuid": "8.3.0",
"wrap-ansi": "7.0.0"
},
"devDependencies": {
"@babel/core": "^7.11.6",
@@ -163,13 +168,14 @@
"repository": "expo/eas-cli",
"scripts": {
"postpack": "rm -f oclif.manifest.json",
"prepack": "rm -rf build && yarn build && yarn oclif-dev manifest && yarn oclif-dev readme",
"prepack": "rm -rf build && yarn build && yarn oclif-dev manifest && yarn oclif-dev readme && node patch-readme",
"build": "tsc --project tsconfig.build.json",
"watch": "yarn build --watch --preserveWatchOutput",
"typecheck": "tsc",
"test": "jest",
"version": "yarn oclif-dev readme && git add README.md",
"version": "yarn oclif-dev readme && node patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml"
},
"types": "build/index.d.ts"
"types": "build/index.d.ts",
"gitHead": "161808bc37c255fdea6801d69d23dbc725c056c3"
}
+9
View File
@@ -0,0 +1,9 @@
const fs = require('fs');
// Patch `oclif-dev readme` path and link generation
let readmeContent = fs.readFileSync('README.md', 'utf8');
readmeContent = readmeContent.replace(/\[build\//g, '[src/');
readmeContent = readmeContent.replace(/build\/commands/g, 'packages/eas-cli/src/commands');
fs.writeFileSync('README.md', readmeContent);
console.log('Patched README path generation');
+6 -1
View File
@@ -96,6 +96,11 @@ function getContext() {
os_version: os.release(),
device_brand: platform,
device_model: platform,
app_version: packageJSON?.version,
source_version: packageJSON?.version,
source: 'eas cli',
};
}
export enum AnalyticsEvent {
ACTION = 'action', // generic event type which is used to determine the 'daily active user' stat, include an `action: eas ${subcommand}` property inside of the event properties object
}
@@ -1,294 +0,0 @@
import { Workflow } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import prompts from 'prompts';
import { asMock } from '../../__tests__/utils';
import { CredentialsProvider } from '../../credentials/CredentialsProvider';
import { ensureCredentialsAsync } from '../credentials';
jest.mock('prompts');
function createMockCredentialsProvider({
hasRemote,
hasLocal,
isLocalSynced,
}: any): CredentialsProvider {
return {
platform: 'android',
hasRemoteAsync: jest.fn().mockImplementation(() => hasRemote || false),
hasLocalAsync: jest.fn().mockImplementation(() => hasLocal || false),
isLocalSyncedAsync: jest.fn().mockImplementation(() => isLocalSynced || false),
} as CredentialsProvider;
}
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
beforeEach(() => {
asMock(prompts).mockReset();
asMock(prompts).mockImplementation(() => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
});
describe(ensureCredentialsAsync, () => {
describe('for generic builds', () => {
it("should use credentials.json if it's the only available credentials", async () => {
const provider = createMockCredentialsProvider({
hasRemote: false,
hasLocal: true,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.GENERIC,
CredentialsSource.AUTO,
false
);
expect(src).toBe('local');
});
it("should use credentials from server if it's the only available credentials", async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: false,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.GENERIC,
CredentialsSource.AUTO,
false
);
expect(src).toBe('remote');
});
it('should use credentials.json if credentials are the same in credentials.json and on server', async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: true,
isLocalSynced: true,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.GENERIC,
CredentialsSource.AUTO,
false
);
expect(prompts).toHaveBeenCalledTimes(0);
expect(src).toBe('local');
});
it('should ask which credentials to use when local and remote crednitals have different values', async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: true,
isLocalSynced: false,
});
asMock(prompts).mockImplementationOnce(() => {
return { select: 'local' };
});
const src = await ensureCredentialsAsync(
provider,
Workflow.GENERIC,
CredentialsSource.AUTO,
false
);
expect(prompts).toHaveBeenCalledTimes(1);
expect(src).toBe('local');
});
it('should ask when local and remote are not the same (select remote)', async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: true,
isLocalSynced: false,
});
asMock(prompts).mockImplementationOnce(() => {
return { select: 'remote' };
});
const src = await ensureCredentialsAsync(
provider,
Workflow.GENERIC,
CredentialsSource.AUTO,
false
);
expect(prompts).toHaveBeenCalledTimes(1);
expect(src).toBe('remote');
});
it('should should throw an error when local and remote are not the same (in non interactive mode)', async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: true,
isLocalSynced: false,
});
try {
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, true);
throw new Error('ensureCredentialsAsync should throw an Error');
} catch (e) {
expect(e.message).toMatch(
'Contents of your local credentials.json for Android are not the same as credentials on EAS servers'
);
}
expect(prompts).toHaveBeenCalledTimes(0);
});
it('should ask when no credentials are present and return remote if confirm=true', async () => {
const provider = createMockCredentialsProvider({
hasRemote: false,
hasLocal: false,
isLocalSynced: false,
});
asMock(prompts).mockImplementationOnce(() => {
return { value: true }; //confirm
});
const src = await ensureCredentialsAsync(
provider,
Workflow.GENERIC,
CredentialsSource.AUTO,
false
);
expect(prompts).toHaveBeenCalledTimes(1);
expect(src).toBe('remote');
});
it('should ask when no credentials are present and abort if confimr=false', async () => {
const provider = createMockCredentialsProvider({
hasRemote: false,
hasLocal: false,
isLocalSynced: false,
});
asMock(prompts).mockImplementationOnce(() => {
return { value: false }; //confirm
});
try {
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, false);
throw new Error('ensureCredentialsAsync should throw an Error');
} catch (e) {
expect(e.message).toMatch(
'Aborting build process, credentials are not configured for Android'
);
}
expect(prompts).toHaveBeenCalledTimes(1);
});
it('should throw an error when no credentials are present in non interactive mode', async () => {
const provider = createMockCredentialsProvider({
hasRemote: false,
hasLocal: false,
isLocalSynced: false,
});
asMock(prompts).mockImplementation(() => {
return { value: true }; // confirm
});
try {
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, true);
throw new Error('ensureCredentialsAsync should throw an Error');
} catch (e) {
expect(e.message).toMatch('Credentials for this app are not configured');
}
expect(prompts).toHaveBeenCalledTimes(0);
});
});
describe('for managed builds', () => {
it("should use credentials.json if it's the only available credentials", async () => {
const provider = createMockCredentialsProvider({
hasRemote: false,
hasLocal: true,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.MANAGED,
CredentialsSource.AUTO,
false
);
expect(src).toBe('local');
});
it("should use credentials from server if it's the only available credentials", async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: false,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.MANAGED,
CredentialsSource.AUTO,
false
);
expect(src).toBe('remote');
});
it('should use credentials.json if credentials are the same in credentials.json and on server', async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: true,
isLocalSynced: true,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.MANAGED,
CredentialsSource.AUTO,
false
);
expect(prompts).toHaveBeenCalledTimes(0);
expect(src).toBe('local');
});
it('should use credentials.json even if credentials on server are different', async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: true,
isLocalSynced: false,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.MANAGED,
CredentialsSource.AUTO,
false
);
expect(prompts).toHaveBeenCalledTimes(0);
expect(src).toBe('local');
});
});
describe('generic builds with credentialsSource set to local', () => {
it('should use credentials.json without asking even if credentials on server are different', async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: true,
isLocalSynced: false,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.GENERIC,
CredentialsSource.LOCAL,
false
);
expect(prompts).toHaveBeenCalledTimes(0);
expect(src).toBe('local');
});
});
describe('generic builds with credentialsSource set to remote', () => {
it('should use credentials from server even if credentials.json exists and have different values', async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: true,
isLocalSynced: false,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.GENERIC,
CredentialsSource.REMOTE,
false
);
expect(prompts).toHaveBeenCalledTimes(0);
expect(src).toBe('remote');
});
});
});
@@ -1,5 +1,5 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { AndroidConfig, AndroidManifest } from '@expo/config-plugins';
import fs from 'fs-extra';
import Log from '../../log';
@@ -10,7 +10,7 @@ import { ensureValidVersions } from '../utils/updates';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
@@ -22,12 +22,7 @@ export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig)
}
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
if (!androidManifestPath) {
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
}
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
androidManifestPath
);
const androidManifest = await getAndroidManifestAsync(projectDir);
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, accountName)) {
const result = AndroidConfig.Updates.setUpdatesConfig(exp, androidManifest, accountName);
@@ -52,10 +47,7 @@ export async function syncUpdatesConfigurationAsync(
}
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
if (!androidManifestPath) {
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
}
let androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(androidManifestPath);
let androidManifest = await getAndroidManifestAsync(projectDir);
if (!AndroidConfig.Updates.areVersionsSynced(exp, androidManifest)) {
androidManifest = AndroidConfig.Updates.setVersionsConfig(exp, androidManifest);
@@ -70,7 +62,7 @@ export async function syncUpdatesConfigurationAsync(
}
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
@@ -78,13 +70,7 @@ async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig)
throw new Error(`Missing ${gradleScriptApply} in ${buildGradlePath}`);
}
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
if (!androidManifestPath) {
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
}
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
androidManifestPath
);
const androidManifest = await getAndroidManifestAsync(projectDir);
if (!AndroidConfig.Updates.isMainApplicationMetaDataSet(androidManifest)) {
throw new Error('Missing values in AndroidManifest.xml');
@@ -93,13 +79,7 @@ async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig)
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
if (!androidManifestPath) {
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
}
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
androidManifestPath
);
const androidManifest = await getAndroidManifestAsync(projectDir);
return AndroidConfig.Manifest.getMainApplicationMetaDataValue(
androidManifest,
AndroidConfig.Updates.Config.RELEASE_CHANNEL
@@ -108,3 +88,28 @@ export async function readReleaseChannelSafelyAsync(projectDir: string): Promise
return null;
}
}
export async function readChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
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
);
if (!stringifiedRequestHeaders) {
return null;
}
return JSON.parse(stringifiedRequestHeaders)['expo-channel-name'] ?? null;
} catch (err) {
console.log({ err });
return null;
}
}
async function getAndroidManifestAsync(projectDir: string): Promise<AndroidManifest> {
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
if (!androidManifestPath) {
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
}
return AndroidConfig.Manifest.readAndroidManifestAsync(androidManifestPath);
}
@@ -0,0 +1,108 @@
import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { readChannelSafelyAsync } from '../UpdatesModule';
jest.mock('fs');
afterAll(() => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.removeSync(os.tmpdir());
});
beforeEach(() => {
vol.reset();
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.mkdirpSync(os.tmpdir());
});
const testChannel = 'test-channel';
describe(readChannelSafelyAsync, () => {
it('retrieves the channel when it is defined natively', async () => {
vol.fromJSON(
{
'./android/app/src/main/AndroidManifest.xml': AndroidManifestWithChannel,
},
'/expo-project'
);
const channel = await readChannelSafelyAsync('/expo-project');
expect(channel).toBe(testChannel);
});
it('returns null if the channel is not defined natively', async () => {
vol.fromJSON(
{
'./android/app/src/main/AndroidManifest.xml': AndroidManifestNoChannel,
},
'/expo-project'
);
const channel = await readChannelSafelyAsync('/expo-project');
expect(channel).toBeNull();
});
});
const AndroidManifestWithChannel = `
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.expo.mycoolapp">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="true"
android:theme="@style/AppTheme">
<meta-data android:name="expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY" android:value='{"expo-channel-name":"${testChannel}"}'/>
<activity
android:name=".MainActivity"
android:launchMode="singleTask"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
</manifest>
`;
const AndroidManifestNoChannel = `
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.expo.mycoolapp">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:launchMode="singleTask"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
</manifest>
`;
@@ -0,0 +1,114 @@
import { ExpoConfig } from '@expo/config';
import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { readVersionCode, readVersionName } from '../version';
jest.mock('fs');
afterAll(() => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.removeSync(os.tmpdir());
});
beforeEach(() => {
vol.reset();
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.mkdirpSync(os.tmpdir());
});
describe(readVersionCode, () => {
describe('generic project', () => {
it('reads the version code from native code', () => {
const exp = initGenericProject();
const versionCode = readVersionCode('/repo', exp);
expect(versionCode).toBe(123);
});
});
describe('managed project', () => {
it('reads the version code from expo config', () => {
const exp = initManagedProject();
const versionCode = readVersionCode('/repo', exp);
expect(versionCode).toBe(123);
});
});
});
describe(readVersionName, () => {
describe('generic project', () => {
it('reads the version name from native code', () => {
const exp = initGenericProject();
const versionName = readVersionName('/repo', exp);
expect(versionName).toBe('1.0');
});
});
describe('managed project', () => {
it('reads the version from expo config', () => {
const exp = initManagedProject();
const versionName = readVersionName('/repo', exp);
expect(versionName).toBe('1.0.0');
});
});
});
function initGenericProject(): ExpoConfig {
vol.fromJSON(
{
'./app.json': JSON.stringify({
expo: {
version: '1.0.0',
android: {
versionCode: 1,
},
},
}),
'./android/app/build.gradle': `android {
defaultConfig {
applicationId "com.expo.testapp"
versionCode 123
versionName "1.0"
}
}`,
},
'/repo'
);
const fakeExp: ExpoConfig = {
name: 'myproject',
slug: 'myproject',
version: '1.0.0',
android: {
versionCode: 123,
},
};
return fakeExp;
}
function initManagedProject(): ExpoConfig {
vol.fromJSON(
{
'./app.json': JSON.stringify({
expo: {
version: '1.0.0',
android: {
versionCode: 123,
},
},
}),
},
'/repo'
);
const fakeExp: ExpoConfig = {
name: 'myproject',
slug: 'myproject',
version: '1.0.0',
android: {
versionCode: 123,
},
};
return fakeExp;
}
@@ -1,191 +0,0 @@
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import assert from 'assert';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import Log from '../../log';
import {
ensureAppIdentifierIsDefinedAsync,
getAndroidApplicationIdAsync,
getProjectConfigDescription,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { gitAddAsync } from '../../utils/git';
import { Platform } from '../types';
enum ApplicationIdSource {
AndroidProject,
AppJson,
}
function isApplicationIdValid(applicationId: string): boolean {
return /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(applicationId);
}
export async function ensureApplicationIdIsValidAsync(projectDir: string, exp: ExpoConfig) {
const applicationId = await ensureAppIdentifierIsDefinedAsync({
projectDir,
platform: Platform.ANDROID,
exp,
});
if (!isApplicationIdValid(applicationId)) {
const configDescription = getProjectConfigDescription(projectDir);
Log.error(
`Invalid format of Android applicationId. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`
);
Log.error(`Update "android.package" in ${configDescription} and run this command again.`);
throw new Error('Invalid applicationId');
}
}
export async function configureApplicationIdAsync(
projectDir: string,
exp: ExpoConfig,
allowExperimental: boolean
): Promise<void> {
const configDescription = getProjectConfigDescription(projectDir);
const applicationIdFromConfig = AndroidConfig.Package.getPackage(exp);
const applicationIdFromAndroidProject = await getAndroidApplicationIdAsync(projectDir);
if (applicationIdFromAndroidProject && applicationIdFromConfig) {
if (applicationIdFromConfig !== applicationIdFromAndroidProject) {
Log.newLine();
Log.warn(
`We detected that your Android project is configured with a different application id than the one defined in ${configDescription}.`
);
const hasApplicationIdInStaticConfig = await hasApplicationIdInStaticConfigAsync(
projectDir,
exp
);
if (!hasApplicationIdInStaticConfig) {
Log.log(`If you choose the one defined in ${configDescription} we'll automatically configure your Android project with it.
However, if you choose the one defined in the Android project you'll have to update ${configDescription} on your own.`);
}
Log.newLine();
const { applicationIdSource } = await promptAsync({
type: 'select',
name: 'applicationIdSource',
message: 'Which application id should we use?',
choices: [
{
title: `${chalk.bold(
applicationIdFromAndroidProject
)} - In build.gradle in Android project`,
value: ApplicationIdSource.AndroidProject,
},
{
title: `${chalk.bold(applicationIdFromConfig)} - In your ${configDescription}`,
value: ApplicationIdSource.AppJson,
},
],
});
switch (applicationIdSource) {
case ApplicationIdSource.AppJson: {
await updateApplicationIdInBuildGradleAsync(projectDir, exp);
break;
}
case ApplicationIdSource.AndroidProject: {
if (hasApplicationIdInStaticConfig) {
await updateAppJsonConfigAsync(projectDir, exp, applicationIdFromAndroidProject);
} else {
throw new Error(missingPackageMessage(configDescription));
}
break;
}
}
}
} else if (!applicationIdFromAndroidProject && !applicationIdFromConfig) {
throw new Error(missingPackageMessage(configDescription));
} else if (applicationIdFromAndroidProject && !applicationIdFromConfig) {
if (getConfigFilePaths(projectDir).staticConfigPath) {
await updateAppJsonConfigAsync(projectDir, exp, applicationIdFromAndroidProject);
} else {
throw new Error(missingPackageMessage(configDescription));
}
} else if (!applicationIdFromAndroidProject && applicationIdFromConfig) {
// This should never happen, adding warning just in case
Log.warn(
'applicationId is not specified in your ./android/app/build.gradle file. Make sure your project is configured correctly before building.'
);
return;
}
if (allowExperimental) {
// this step is optional, the Play store is using applicationId value from build.gradle
// to identify the app, so package name does not need to be updated
await updatePackageNameAsync(projectDir, exp);
await gitAddAsync(path.join(projectDir, 'android'), { intentToAdd: true });
}
}
function missingPackageMessage(configDescription: string): string {
return `Please define "android.package" in ${configDescription} and run "eas build:configure" again.`;
}
async function updateAppJsonConfigAsync(
projectDir: string,
exp: ExpoConfig,
newApplicationId: string
): Promise<void> {
const paths = getConfigFilePaths(projectDir);
assert(paths.staticConfigPath, "can't update dynamic configs");
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
rawStaticConfig.expo = {
...rawStaticConfig.expo,
android: { ...rawStaticConfig.expo?.android, package: newApplicationId },
};
await fs.writeJson(paths.staticConfigPath, rawStaticConfig, { spaces: 2 });
exp.android = { ...exp.android, package: newApplicationId };
}
/**
* Check if static config exists and if expo.android.package is defined there.
* It will return false if the value in static config is different than "android.package" in ExpoConfig
*/
async function hasApplicationIdInStaticConfigAsync(
projectDir: string,
exp: ExpoConfig
): Promise<boolean> {
if (!exp.android?.package) {
return false;
}
const paths = getConfigFilePaths(projectDir);
if (!paths.staticConfigPath) {
return false;
}
const rawStaticConfig = await fs.readJson(paths.staticConfigPath);
return rawStaticConfig?.expo?.android?.package === exp.android.package;
}
async function updateApplicationIdInBuildGradleAsync(projectDir: string, exp: ExpoConfig) {
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
const updatedBuildGradleContents = AndroidConfig.Package.setPackageInBuildGradle(
exp,
buildGradleContents
);
await fs.writeFile(buildGradlePath, updatedBuildGradleContents);
}
async function updatePackageNameAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
await AndroidConfig.Package.renamePackageOnDisk(exp, projectDir);
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
if (!androidManifestPath) {
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
}
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
androidManifestPath
);
const updatedAndroidManifest = AndroidConfig.Package.setPackageInAndroidManifest(
exp,
androidManifest
);
await AndroidConfig.Manifest.writeAndroidManifestAsync(
androidManifestPath,
updatedAndroidManifest
);
}
+21 -12
View File
@@ -2,6 +2,7 @@ import { Android, Metadata, Workflow } from '@expo/eas-build-job';
import { EasConfig } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import path from 'path';
import AndroidCredentialsProvider, {
@@ -10,13 +11,17 @@ import AndroidCredentialsProvider, {
import { createCredentialsContextAsync } from '../../credentials/context';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import Log from '../../log';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
getApplicationId,
} 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 { ensureCredentialsAsync } from '../credentials';
import { transformMetadata } from '../graphql';
import { Platform } from '../types';
import { ensureApplicationIdIsValidAsync } from './applicationId';
import { logCredentialsSource } from '../utils/credentials';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { transformGenericJob, transformManagedJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
@@ -63,11 +68,12 @@ This means that it will most likely produce an AAB and you will not be able to i
}
}
await ensureApplicationIdIsValidAsync(commandCtx.projectDir, commandCtx.exp);
if (buildCtx.buildProfile.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(commandCtx.projectDir, commandCtx.exp);
}
return await prepareBuildRequestForPlatformAsync({
ctx: buildCtx,
projectConfiguration: {},
ensureCredentialsAsync: ensureAndroidCredentialsAsync,
ensureProjectConfiguredAsync: async () => {
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
@@ -113,24 +119,27 @@ async function ensureAndroidCredentialsAsync(
if (!shouldProvideCredentials(ctx)) {
return;
}
const androidApplicationIdentifier = getApplicationId(
ctx.commandCtx.projectDir,
ctx.commandCtx.exp
);
const provider = new AndroidCredentialsProvider(
await createCredentialsContextAsync(ctx.commandCtx.projectDir, {
nonInteractive: ctx.commandCtx.nonInteractive,
}),
{
app: {
account: nullthrows(
findAccountByName(ctx.commandCtx.user.accounts, ctx.commandCtx.accountName),
`You do not have access to account: ${ctx.commandCtx.accountName}`
),
projectName: ctx.commandCtx.projectName,
accountName: ctx.commandCtx.accountName,
androidApplicationIdentifier,
},
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
}
);
const credentialsSource = await ensureCredentialsAsync(
provider,
ctx.buildProfile.workflow,
ctx.buildProfile.credentialsSource,
ctx.commandCtx.nonInteractive
);
const { credentialsSource } = ctx.buildProfile;
logCredentialsSource(credentialsSource, Platform.ANDROID);
return {
credentials: await provider.getCredentialsAsync(credentialsSource),
source: credentialsSource,
+11 -16
View File
@@ -2,23 +2,27 @@ import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import Log from '../../log';
import { getAndroidApplicationIdAsync } from '../../project/projectUtils';
import { gitAddAsync } from '../../utils/git';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
warnIfAndroidPackageDefinedInAppConfigForGenericProject,
} from '../../project/android/applicationId';
import vcs from '../../vcs';
import { ConfigureContext } from '../context';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
import { configureApplicationIdAsync, ensureApplicationIdIsValidAsync } from './applicationId';
export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void> {
if (!ctx.hasAndroidNativeProject) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
return;
}
warnIfAndroidPackageDefinedInAppConfigForGenericProject(ctx.projectDir, ctx.exp);
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
await configureApplicationIdAsync(ctx.projectDir, ctx.exp, ctx.allowExperimental);
await ensureApplicationIdIsValidAsync(ctx.projectDir, ctx.exp);
const easGradlePath = AndroidConfig.EasBuild.getEasBuildGradlePath(ctx.projectDir);
await gitAddAsync(easGradlePath, { intentToAdd: true });
await vcs.trackFileAsync(easGradlePath);
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
@@ -30,20 +34,11 @@ export async function validateAndSyncProjectConfigurationAsync(
projectDir: string,
exp: ExpoConfig
): Promise<void> {
const applicationIdFromAndroidProject = await getAndroidApplicationIdAsync(projectDir);
const packageNameFromConfig = AndroidConfig.Package.getPackage(exp);
const isApplicationIdConfigured =
applicationIdFromAndroidProject && applicationIdFromAndroidProject === packageNameFromConfig;
const isEasBuildGradleConfigured = await AndroidConfig.EasBuild.isEasBuildGradleConfiguredAsync(
projectDir
);
if (!isEasBuildGradleConfigured || !isApplicationIdConfigured) {
if (!(await AndroidConfig.EasBuild.isEasBuildGradleConfiguredAsync(projectDir))) {
throw new Error(
'Project is not configured. Please run "eas build:configure" to configure the project.'
);
}
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
}
+18 -1
View File
@@ -1,6 +1,10 @@
import { Android } from '@expo/eas-build-job';
import { AndroidGenericJobInput, AndroidManagedJobInput } from '../../graphql/generated';
import {
AndroidGenericJobInput,
AndroidManagedBuildType,
AndroidManagedJobInput,
} from '../../graphql/generated';
import { transformProjectArchive } from '../graphql';
export function transformGenericJob(job: Android.GenericJob): AndroidGenericJobInput {
@@ -8,6 +12,7 @@ export function transformGenericJob(job: Android.GenericJob): AndroidGenericJobI
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
updates: job.updates,
secrets: job.secrets,
builderEnvironment: job.builderEnvironment,
cache: job.cache,
@@ -21,9 +26,21 @@ export function transformManagedJob(job: Android.ManagedJob): AndroidManagedJobI
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),
};
}
function transformBuildType(buildType: Android.ManagedBuildType): AndroidManagedBuildType {
if (buildType === Android.ManagedBuildType.APK) {
return AndroidManagedBuildType.Apk;
} else if (buildType === Android.ManagedBuildType.APP_BUNDLE) {
return AndroidManagedBuildType.AppBundle;
} else {
return AndroidManagedBuildType.DevelopmentClient;
}
}
@@ -3,9 +3,9 @@ import { AndroidGenericBuildProfile, AndroidManagedBuildProfile } from '@expo/ea
import path from 'path';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { readEnvironmentSecretsAsync } from '../../credentials/credentialsJson/read';
import { getProjectAccountNameAsync } from '../../project/projectUtils';
import { gitRootDirectoryAsync } from '../../utils/git';
import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import vcs from '../../vcs';
import { BuildContext } from '../context';
import { Platform } from '../types';
@@ -38,7 +38,6 @@ interface CommonJobProperties {
buildCredentials?: {
keystore: Android.Keystore;
};
environmentSecrets?: Record<string, string>;
};
}
@@ -46,8 +45,7 @@ async function prepareJobCommonAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Partial<CommonJobProperties>> {
const environmentSecrets = await readEnvironmentSecretsAsync(ctx.commandCtx.projectDir);
const credentials = jobData.credentials;
const { credentials } = jobData;
const buildCredentials = credentials
? {
buildCredentials: {
@@ -69,11 +67,14 @@ async function prepareJobCommonAsync(
node: ctx.buildProfile.node,
yarn: ctx.buildProfile.yarn,
ndk: ctx.buildProfile.ndk,
expoCli: ctx.buildProfile.expoCli,
env: ctx.buildProfile.env,
},
cache: ctx.buildProfile.cache,
cache: {
...ctx.buildProfile.cache,
clear: ctx.commandCtx.clearCache,
},
secrets: {
...(environmentSecrets ? { environmentSecrets } : {}),
...buildCredentials,
},
};
@@ -85,13 +86,14 @@ async function prepareGenericJobAsync(
buildProfile: AndroidGenericBuildProfile
): Promise<Partial<Android.GenericJob>> {
const projectRootDirectory =
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
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,
};
}
@@ -102,14 +104,15 @@ async function prepareManagedJobAsync(
buildProfile: AndroidManagedBuildProfile
): Promise<Partial<Android.ManagedJob>> {
const projectRootDirectory =
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.exp);
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: accountName,
username,
buildType: buildProfile.buildType,
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
projectRootDirectory,
};
}
@@ -0,0 +1,44 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import fs from 'fs-extra';
import { resolveWorkflow } from '../../project/workflow';
export function readVersionCode(projectDir: string, exp: ExpoConfig): number | undefined {
const workflow = resolveWorkflow(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
const buildGradle = readBuildGradle(projectDir);
const matchResult = buildGradle?.match(/versionCode (.*)/);
if (matchResult) {
return Number(matchResult[1]);
} else {
return undefined;
}
} else {
return AndroidConfig.Version.getVersionCode(exp) ?? undefined;
}
}
export function readVersionName(projectDir: string, exp: ExpoConfig): string | undefined {
const workflow = resolveWorkflow(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
const buildGradle = readBuildGradle(projectDir);
const matchResult = buildGradle?.match(/versionName ["'](.*)["']/);
if (matchResult) {
return matchResult[1];
} else {
return undefined;
}
} else {
return exp.version;
}
}
function readBuildGradle(projectDir: string): string | undefined {
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
if (!fs.pathExistsSync(buildGradlePath)) {
return undefined;
}
return fs.readFileSync(buildGradlePath, 'utf8');
}
+32 -35
View File
@@ -9,6 +9,7 @@ import { promptAsync } from '../prompts';
import { uploadAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
import vcs from '../vcs';
import { requestedPlatformDisplayNames } from './constants';
import { BuildContext } from './context';
import { runLocalBuildAsync } from './local';
@@ -16,46 +17,34 @@ import { collectMetadata } from './metadata';
import { Platform, TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
import { printDeprecationWarnings } from './utils/printBuildInfo';
import {
commitChangedFilesAsync,
commitPromptAsync,
isGitStatusCleanAsync,
makeProjectTarballAsync,
showDiffAsync,
} from './utils/repository';
import { commitPromptAsync, makeProjectTarballAsync } from './utils/repository';
export interface CredentialsResult<Credentials> {
source: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
credentials: Credentials;
}
interface Builder<TPlatform extends Platform, Credentials, ProjectConfiguration, TJob extends Job> {
export interface JobData<Credentials> {
credentials?: Credentials;
projectArchive: ArchiveSource;
}
interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
ctx: BuildContext<TPlatform>;
projectConfiguration: ProjectConfiguration;
ensureCredentialsAsync(
ctx: BuildContext<TPlatform>
): Promise<CredentialsResult<Credentials> | undefined>;
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
prepareJobAsync(
ctx: BuildContext<TPlatform>,
jobData: {
credentials?: Credentials;
projectConfiguration?: ProjectConfiguration;
projectArchive: ArchiveSource;
}
): Promise<Job>;
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
}
export async function prepareBuildRequestForPlatformAsync<
TPlatform extends Platform,
Credentials,
ProjectConfiguration,
TJob extends Job
>(
builder: Builder<TPlatform, Credentials, ProjectConfiguration, TJob>
): Promise<() => Promise<string | undefined>> {
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<() => Promise<string | undefined>> {
const credentialsResult = await withAnalyticsAsync(
async () => await builder.ensureCredentialsAsync(builder.ctx),
{
@@ -72,7 +61,7 @@ export async function prepareBuildRequestForPlatformAsync<
});
}
if (!(await isGitStatusCleanAsync())) {
if (await vcs.hasUncommittedChangesAsync()) {
Log.addNewLineIfNone();
await reviewAndCommitChangesAsync(
`[EAS Build] Run EAS Build for ${
@@ -100,7 +89,6 @@ export async function prepareBuildRequestForPlatformAsync<
const job = await builder.prepareJobAsync(builder.ctx, {
projectArchive,
credentials: credentialsResult?.credentials,
projectConfiguration: builder.projectConfiguration,
});
return async () => {
@@ -109,10 +97,24 @@ export async function prepareBuildRequestForPlatformAsync<
return undefined;
} else {
try {
return sendBuildRequestAsync(builder, job, metadata);
return await sendBuildRequestAsync(builder, job, metadata);
} catch (error) {
if (error?.expoApiV2ErrorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli');
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_TOO_MANY_PENDING_BUILDS'
) {
Log.error(
`You have already reached the maximum number of pending ${
requestedPlatformDisplayNames[job.platform]
} builds for your account. Try again later.`
);
throw new Error('Build request failed.');
} else if (error?.graphQLErrors) {
Log.error(
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, please report the issue.'
);
}
throw error;
}
@@ -157,13 +159,8 @@ async function uploadProjectAsync<TPlatform extends Platform>(
}
}
async function sendBuildRequestAsync<
TPlatform extends Platform,
Credentials,
ProjectConfiguration,
TJob extends Job
>(
builder: Builder<TPlatform, Credentials, ProjectConfiguration, TJob>,
async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJob extends Job>(
builder: Builder<TPlatform, Credentials, TJob>,
job: TJob,
metadata: Metadata
): Promise<string> {
@@ -223,7 +220,7 @@ async function reviewAndCommitChangesAsync(
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
): Promise<void> {
if (process.env.EAS_BUILD_AUTOCOMMIT) {
await commitChangedFilesAsync(initialCommitMessage);
await vcs.commitAsync({ commitMessage: initialCommitMessage, commitAllFiles: false });
Log.withTick('Committed changes.');
return;
}
@@ -256,7 +253,7 @@ async function reviewAndCommitChangesAsync(
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes.');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await showDiffAsync();
await vcs.showDiffAsync();
await reviewAndCommitChangesAsync(initialCommitMessage, {
nonInteractive,
askedFirstTime: false,
+40 -25
View File
@@ -6,18 +6,12 @@ import path from 'path';
import Log from '../log';
import { promptAsync } from '../prompts';
import { ensureLoggedInAsync } from '../user/actions';
import { gitAddAsync } from '../utils/git';
import vcs from '../vcs';
import { configureAndroidAsync } from './android/configure';
import { ConfigureContext } from './context';
import { configureIosAsync } from './ios/configure';
import { RequestedPlatform } from './types';
import {
commitPromptAsync,
ensureGitRepoExistsAsync,
isGitStatusCleanAsync,
maybeBailOnGitStatusAsync,
showDiffAsync,
} from './utils/repository';
import { commitPromptAsync, maybeBailOnRepoStatusAsync } from './utils/repository';
const configureCommitMessage = {
[RequestedPlatform.Android]: 'Configure EAS Build for Android',
@@ -28,10 +22,9 @@ const configureCommitMessage = {
export async function configureAsync(options: {
platform: RequestedPlatform;
projectDir: string;
allowExperimental: boolean;
}): Promise<void> {
await ensureGitRepoExistsAsync();
await maybeBailOnGitStatusAsync();
await vcs.ensureRepoExistsAsync();
await maybeBailOnRepoStatusAsync();
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
@@ -39,7 +32,6 @@ export async function configureAsync(options: {
user: await ensureLoggedInAsync(),
projectDir: options.projectDir,
exp,
allowExperimental: options.allowExperimental,
requestedPlatform: options.platform,
shouldConfigureAndroid: [RequestedPlatform.All, RequestedPlatform.Android].includes(
options.platform
@@ -58,7 +50,7 @@ export async function configureAsync(options: {
await configureIosAsync(ctx);
}
if (!(await isGitStatusCleanAsync())) {
if (await vcs.hasUncommittedChangesAsync()) {
Log.newLine();
await reviewAndCommitChangesAsync(configureCommitMessage[options.platform]);
} else {
@@ -67,6 +59,37 @@ export async function configureAsync(options: {
}
}
const MANAGED_DEFAULTS = {
release: {
workflow: 'managed',
},
development: {
workflow: 'managed',
buildType: 'development-client',
distribution: 'internal',
},
};
const ANDROID_GENERIC_DEFAULTS = {
release: {
workflow: 'generic',
},
development: {
workflow: 'generic',
gradleCommand: ':app:assembleDebug',
distribution: 'internal',
},
};
const IOS_GENERIC_DEFAULTS = {
release: {
workflow: 'generic',
},
development: {
workflow: 'generic',
schemeBuildConfiguration: 'Debug',
distribution: 'internal',
},
};
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
const easJsonPath = path.join(ctx.projectDir, 'eas.json');
let existingEasJson;
@@ -101,27 +124,19 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
...existingEasJson,
...(shouldInitAndroid
? {
android: {
release: {
workflow: ctx.hasAndroidNativeProject ? 'generic' : 'managed',
},
},
android: ctx.hasAndroidNativeProject ? ANDROID_GENERIC_DEFAULTS : MANAGED_DEFAULTS,
}
: null),
...(shouldInitIOS
? {
ios: {
release: {
workflow: ctx.hasIosNativeProject ? 'generic' : 'managed',
},
},
ios: ctx.hasIosNativeProject ? IOS_GENERIC_DEFAULTS : MANAGED_DEFAULTS,
}
: null),
},
};
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
await gitAddAsync(easJsonPath, { intentToAdd: true });
await vcs.trackFileAsync(easJsonPath);
Log.withTick(`${existingEasJson ? 'Updated' : 'Generated'} eas.json`);
}
@@ -155,7 +170,7 @@ async function reviewAndCommitChangesAsync(
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await showDiffAsync();
await vcs.showDiffAsync();
await reviewAndCommitChangesAsync(initialCommitMessage, false);
}
}
+42 -5
View File
@@ -1,5 +1,8 @@
import { ExpoConfig } from '@expo/config';
import { Workflow } from '@expo/eas-build-job';
import { AndroidBuildProfile, EasConfig, 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';
@@ -20,7 +23,7 @@ export interface CommandContext {
exp: ExpoConfig;
nonInteractive: boolean;
local: boolean;
skipCredentialsCheck: boolean;
clearCache: boolean;
skipProjectConfiguration: boolean;
waitForBuildEnd: boolean;
}
@@ -33,7 +36,7 @@ export async function createCommandContextAsync({
projectId,
nonInteractive = false,
local,
skipCredentialsCheck = false,
clearCache = false,
skipProjectConfiguration = false,
waitForBuildEnd,
}: {
@@ -44,7 +47,7 @@ export async function createCommandContextAsync({
projectDir: string;
nonInteractive: boolean;
local: boolean;
skipCredentialsCheck: boolean;
clearCache: boolean;
skipProjectConfiguration: boolean;
waitForBuildEnd: boolean;
}): Promise<CommandContext> {
@@ -63,7 +66,7 @@ export async function createCommandContextAsync({
exp,
nonInteractive,
local,
skipCredentialsCheck,
clearCache,
skipProjectConfiguration,
waitForBuildEnd,
};
@@ -73,7 +76,6 @@ export interface ConfigureContext {
user: Actor;
projectDir: string;
exp: ExpoConfig;
allowExperimental: boolean;
requestedPlatform: RequestedPlatform;
shouldConfigureAndroid: boolean;
shouldConfigureIos: boolean;
@@ -107,6 +109,7 @@ export function createBuildContext<T extends Platform>({
}
const accountId = findAccountByName(commandCtx.user.accounts, commandCtx.accountName)?.id;
const devClientProperties = getDevClientEventProperties(platform, commandCtx, buildProfile);
const trackingCtx = {
tracking_id: uuidv4(),
platform,
@@ -114,6 +117,7 @@ export function createBuildContext<T extends Platform>({
account_name: commandCtx.accountName,
project_id: commandCtx.projectId,
project_type: buildProfile.workflow,
...devClientProperties,
};
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
return {
@@ -123,3 +127,36 @@ export function createBuildContext<T extends Platform>({
buildProfile,
};
}
function getDevClientEventProperties(
platform: Platform,
commandCtx: CommandContext,
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) {
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
} else if (platform === Platform.IOS && 'schemeBuildConfiguration' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.schemeBuildConfiguration === 'Debug');
} else {
includesDevClient = false;
}
if (version) {
return { dev_client: includesDevClient, dev_client_version: version };
} else {
return { dev_client: includesDevClient };
}
}
function tryGetDevClientVersion(projectDir: string): string | null {
try {
const pkg = JsonFile.read(resolveFrom(projectDir, 'expo-dev-client/package.json'));
return pkg.version?.toString() ?? null;
} catch {
return null;
}
}
+10 -4
View File
@@ -7,16 +7,17 @@ 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 { ensureGitRepoExistsAsync, ensureGitStatusIsCleanAsync } from './utils/repository';
import { ensureRepoIsCleanAsync } from './utils/repository';
export async function buildAsync(commandCtx: CommandContext): Promise<void> {
await ensureGitRepoExistsAsync();
await ensureGitStatusIsCleanAsync(commandCtx.nonInteractive);
await vcs.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(commandCtx.nonInteractive);
const scheduledBuilds = await startBuildsAsync(commandCtx);
if (commandCtx.local) {
@@ -71,7 +72,7 @@ async function startBuildsAsync(
async function waitForBuildEndAsync(
buildIds: string[],
{ timeoutSec = 1800, intervalSec = 30 } = {}
{ 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();
@@ -93,6 +94,9 @@ async function waitForBuildEndAsync(
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;
@@ -130,6 +134,7 @@ async function waitForBuildEndAsync(
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;
@@ -137,6 +142,7 @@ async function waitForBuildEndAsync(
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}`,
-104
View File
@@ -1,104 +0,0 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import { CredentialsProvider } from '../credentials/CredentialsProvider';
import Log from '../log';
import { confirmAsync, promptAsync } from '../prompts';
import { requestedPlatformDisplayNames } from './constants';
function logCredentials(target: 'local' | 'remote', platform: Platform) {
let message = `Using ${target} ${requestedPlatformDisplayNames[platform]} credentials`;
if (target === 'local') message += ` ${chalk.dim('(credentials.json)')}`;
if (target === 'remote') message += ` ${chalk.dim('(Expo server)')}`;
Log.succeed(message);
}
async function ensureCredentialsAutoAsync(
provider: CredentialsProvider,
workflow: Workflow,
nonInteractive: boolean
): Promise<CredentialsSource.LOCAL | CredentialsSource.REMOTE> {
const platform = requestedPlatformDisplayNames[provider.platform];
switch (workflow) {
case Workflow.MANAGED:
if (await provider.hasLocalAsync()) {
return CredentialsSource.LOCAL;
} else {
return CredentialsSource.REMOTE;
}
case Workflow.GENERIC: {
const hasLocal = await provider.hasLocalAsync();
const hasRemote = await provider.hasRemoteAsync();
if (hasRemote && hasLocal) {
if (!(await provider.isLocalSyncedAsync())) {
if (nonInteractive) {
throw new Error(
`Contents of your local credentials.json for ${platform} are not the same as credentials on EAS servers. To use the desired credentials, set the "builds.${platform}.{profile}.credentialsSource" field in the credentials.json file to one of the following: "local", "remote".`
);
} else {
Log.log(
`Contents of your local credentials.json for ${platform} are not the same as credentials on EAS servers`
);
}
const { select } = await promptAsync({
type: 'select',
name: 'select',
message: 'Which credentials do you want to use for this build?',
choices: [
{ title: 'Local credentials.json', value: CredentialsSource.LOCAL },
{ title: 'Credentials stored on EAS servers.', value: CredentialsSource.REMOTE },
],
});
return select;
} else {
return CredentialsSource.LOCAL;
}
} else if (hasLocal) {
logCredentials('local', provider.platform);
return CredentialsSource.LOCAL;
} else if (hasRemote) {
logCredentials('remote', provider.platform);
return CredentialsSource.REMOTE;
} else {
if (nonInteractive) {
throw new Error(
`Credentials for this app are not configured and there is no entry in credentials.json for ${platform}. Either configure credentials.json, or launch the build without "--non-interactive" flag to get a prompt to generate credentials automatically.`
);
} else if (Log.isDebug) {
Log.warn(
`Credentials for this app are not configured and there is no entry in credentials.json for ${platform}`
);
}
const confirm = await confirmAsync({
message: `${platform} credentials not found, generate now?`,
});
if (confirm) {
return CredentialsSource.REMOTE;
} else {
throw new Error(`Aborting build process, credentials are not configured for ${platform}`);
}
}
}
}
}
export async function ensureCredentialsAsync(
provider: CredentialsProvider,
workflow: Workflow,
src: CredentialsSource,
nonInteractive: boolean
): Promise<CredentialsSource.LOCAL | CredentialsSource.REMOTE> {
switch (src) {
case CredentialsSource.LOCAL:
logCredentials('local', provider.platform);
return CredentialsSource.LOCAL;
case CredentialsSource.REMOTE:
logCredentials('remote', provider.platform);
return CredentialsSource.REMOTE;
case CredentialsSource.AUTO:
return await ensureCredentialsAutoAsync(provider, workflow, nonInteractive);
}
}
+1 -9
View File
@@ -27,18 +27,10 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
export function transformMetadata(metadata: Metadata): BuildMetadataInput {
return {
appIdentifier: metadata.appIdentifier,
appName: metadata.appName,
appVersion: metadata.appVersion,
buildProfile: metadata.buildProfile,
cliVersion: metadata.cliVersion,
...metadata,
credentialsSource:
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
distribution: metadata.distribution && transformDistribution(metadata.distribution),
gitCommitHash: metadata.gitCommitHash,
releaseChannel: metadata.releaseChannel,
sdkVersion: metadata.sdkVersion,
trackingContext: metadata.trackingContext,
workflow: metadata.workflow && transformWorkflow(metadata.workflow),
};
}
@@ -5,7 +5,7 @@ import fs from 'fs-extra';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { gitAddAsync } from '../../utils/git';
import vcs from '../../vcs';
import { ensureValidVersions } from '../utils/updates';
import { readPlistAsync, writePlistAsync } from './plist';
@@ -84,7 +84,7 @@ async function writeExpoPlistAsync(
): Promise<void> {
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
await writePlistAsync(expoPlistPath, expoPlist);
await gitAddAsync(expoPlistPath, { intentToAdd: true });
await vcs.trackFileAsync(expoPlistPath);
}
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
@@ -95,3 +95,13 @@ export async function readReleaseChannelSafelyAsync(projectDir: string): Promise
return null;
}
}
export async function readChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
const expoPlist: any = await readExpoPlistAsync(projectDir); // TODO-JJ remove any once IOSConfig.ExpoPlist is updated to include `EXUpdatesRequestHeaders : Record<string,string>`
const updatesRequestHeaders = expoPlist['EXUpdatesRequestHeaders'] ?? {}; //TODO-JJ 'EXUpdatesRequestHeaders' IosConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY once https://github.com/expo/expo-cli/pull/3571 is published
return updatesRequestHeaders['expo-channel-name'] ?? null;
} catch (err) {
return null;
}
}
@@ -0,0 +1,88 @@
import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { readChannelSafelyAsync } from '../UpdatesModule';
jest.mock('fs');
afterAll(() => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.removeSync(os.tmpdir());
});
beforeEach(() => {
vol.reset();
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.mkdirpSync(os.tmpdir());
});
const testChannel = 'test-channel';
describe(readChannelSafelyAsync, () => {
it('retrieves the channel when it is defined natively', async () => {
vol.fromJSON(
{
'ios/expo-project/Supporting/Expo.plist': ExpoPlistWithChannel,
'ios/expo-project/AppDelegate.m': '',
},
'/expo-project'
);
const channel = await readChannelSafelyAsync('/expo-project');
expect(channel).toBe(testChannel);
});
it('returns null if the channel is not defined natively', async () => {
vol.fromJSON(
{
'ios/expo-project/Supporting/Expo.plist': ExpoPlistWithOutChannel,
'ios/expo-project/AppDelegate.m': '',
},
'/expo-project'
);
const channel = await readChannelSafelyAsync('/expo-project');
expect(channel).toBeNull();
});
});
const ExpoPlistWithChannel = `
<plist version="1.0">
<dict>
<key>EXUpdatesCheckOnLaunch</key>
<string>ALWAYS</string>
<key>EXUpdatesEnabled</key>
<true/>
<key>EXUpdatesLaunchWaitMs</key>
<integer>0</integer>
<key>EXUpdatesReleaseChannel</key>
<string>default</string>
<key>EXUpdatesSDKVersion</key>
<string>40.0.0</string>
<key>EXUpdatesURL</key>
<string>https://exp.host/@user/expo-project</string>
<key>EXUpdatesRequestHeaders</key>
<dict>
<key>expo-channel-name</key>
<string>${testChannel}</string>
</dict>
</dict>
</plist>
`;
const ExpoPlistWithOutChannel = `
<plist version="1.0">
<dict>
<key>EXUpdatesCheckOnLaunch</key>
<string>ALWAYS</string>
<key>EXUpdatesEnabled</key>
<true/>
<key>EXUpdatesLaunchWaitMs</key>
<integer>0</integer>
<key>EXUpdatesReleaseChannel</key>
<string>default</string>
<key>EXUpdatesSDKVersion</key>
<string>40.0.0</string>
<key>EXUpdatesURL</key>
<string>https://exp.host/@user/expo-project</string>
</dict>
</plist>
`;
@@ -5,7 +5,13 @@ import { vol } from 'memfs';
import os from 'os';
import { readPlistAsync } from '../plist';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from '../version';
import {
BumpStrategy,
bumpVersionAsync,
bumpVersionInAppJsonAsync,
readBuildNumberAsync,
readShortVersionAsync,
} from '../version';
jest.mock('fs');
@@ -144,6 +150,42 @@ describe(bumpVersionInAppJsonAsync, () => {
});
});
describe(readBuildNumberAsync, () => {
describe('generic project', () => {
it('reads the build number from native code', async () => {
const exp = initGenericProject();
const buildNumber = await readBuildNumberAsync('/repo', exp);
expect(buildNumber).toBe('1');
});
});
describe('managed project', () => {
it('reads the build number from expo config', async () => {
const exp = initManagedProject();
const buildNumber = await readBuildNumberAsync('/repo', exp);
expect(buildNumber).toBe('1');
});
});
});
describe(readShortVersionAsync, () => {
describe('generic project', () => {
it('reads the short version from native code', async () => {
const exp = initGenericProject();
const shortVersion = await readShortVersionAsync('/repo', exp);
expect(shortVersion).toBe('1.0.0');
});
});
describe('managed project', () => {
it('reads the version from app config', async () => {
const exp = initGenericProject();
const shortVersion = await readShortVersionAsync('/repo', exp);
expect(shortVersion).toBe('1.0.0');
});
});
});
function initGenericProject(): ExpoConfig {
vol.fromJSON(
{
+37 -67
View File
@@ -1,23 +1,22 @@
import { IOSConfig } from '@expo/config-plugins';
import { Ios, Metadata, Workflow } from '@expo/eas-build-job';
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 sortBy from 'lodash/sortBy';
import path from 'path';
import { IosCredentials } from '../../credentials/ios/types';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { prepareBuildRequestForPlatformAsync } from '../build';
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 { transformMetadata } from '../graphql';
import { Platform } from '../types';
import { ensureBundleIdentifierIsValidAsync } from './bundleIdentifer';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { ensureIosCredentialsAsync } from './credentials';
import { transformGenericJob, transformManagedJob } from './graphql';
import { prepareJobAsync, sanitizedTargetName } from './prepareJob';
import { prepareJobAsync } from './prepareJob';
export async function prepareIosBuildAsync(
commandCtx: CommandContext,
@@ -28,9 +27,10 @@ export async function prepareIosBuildAsync(
platform: Platform.IOS,
easConfig,
});
const { buildProfile } = buildCtx;
if (
buildCtx.buildProfile.workflow === Workflow.GENERIC &&
buildProfile.workflow === Workflow.GENERIC &&
!(await fs.pathExists(path.join(commandCtx.projectDir, 'ios')))
) {
throw new Error(
@@ -40,41 +40,47 @@ export async function prepareIosBuildAsync(
);
}
let iosBuildScheme: string | undefined;
let iosApplicationTarget: string | undefined;
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
iosBuildScheme = buildCtx.buildProfile.scheme ?? (await resolveSchemeAsync(buildCtx));
iosApplicationTarget = await IOSConfig.BuildScheme.getApplicationTargetForSchemeAsync(
buildCtx.commandCtx.projectDir,
iosBuildScheme
if (buildProfile.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(
commandCtx.projectDir,
commandCtx.exp
);
} else {
if (!commandCtx.exp.name) {
throw new Error('"expo.name" is required in your app.json');
}
iosApplicationTarget = sanitizedTargetName(commandCtx.exp.name);
if (!iosApplicationTarget) {
throw new Error('"expo.name" needs to contain some alphanumeric characters');
}
}
await ensureBundleIdentifierIsValidAsync(commandCtx.projectDir, commandCtx.exp);
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
projectDir: commandCtx.projectDir,
nonInteractive: commandCtx.nonInteractive,
exp: commandCtx.exp,
},
buildProfile
);
const targets = await resolveTargetsAsync(
{
projectDir: commandCtx.projectDir,
exp: commandCtx.exp,
},
xcodeBuildContext
);
return await prepareBuildRequestForPlatformAsync({
ctx: buildCtx,
projectConfiguration: {
iosBuildScheme,
iosApplicationTarget,
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
return ensureIosCredentialsAsync(ctx, targets);
},
ensureCredentialsAsync: ensureIosCredentialsAsync,
ensureProjectConfiguredAsync: async () => {
await validateAndSyncProjectConfigurationAsync({
projectDir: commandCtx.projectDir,
exp: commandCtx.exp,
buildProfile: buildCtx.buildProfile,
buildProfile,
});
},
prepareJobAsync,
prepareJobAsync: async (
ctx: BuildContext<Platform.IOS>,
jobData: JobData<IosCredentials>
): Promise<Job> => {
return await prepareJobAsync(ctx, { ...jobData, buildScheme: xcodeBuildContext.buildScheme });
},
sendBuildRequestAsync: async (
appId: string,
job: Ios.Job,
@@ -99,39 +105,3 @@ export async function prepareIosBuildAsync(
},
});
}
async function resolveSchemeAsync(ctx: BuildContext<Platform.IOS>): Promise<string> {
const schemes = IOSConfig.BuildScheme.getSchemesFromXcodeproj(ctx.commandCtx.projectDir);
if (schemes.length === 1) {
return schemes[0];
}
const sortedSchemes = sortBy(schemes);
Log.newLine();
Log.log(
`We've found multiple schemes in your Xcode project: ${chalk.bold(sortedSchemes.join(', '))}`
);
Log.log(
`You can specify the scheme you want to build at ${chalk.bold(
`builds.ios.${ctx.commandCtx.profile}.scheme`
)} in eas.json.`
);
if (ctx.commandCtx.nonInteractive) {
const withoutTvOS = sortedSchemes.filter(i => !i.includes('tvOS'));
const scheme = withoutTvOS.length > 0 ? withoutTvOS[0] : sortedSchemes[0];
Log.log(
`You've run Expo CLI in non-interactive mode, choosing the ${chalk.bold(scheme)} scheme.`
);
Log.newLine();
return scheme;
} else {
const { selectedScheme } = await promptAsync({
type: 'select',
name: 'selectedScheme',
message: 'Which scheme would you like to build now?',
choices: sortedSchemes.map(scheme => ({ title: scheme, value: scheme })),
});
Log.newLine();
return selectedScheme as string;
}
}
@@ -1,138 +0,0 @@
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import chalk from 'chalk';
import fs from 'fs-extra';
import Log from '../../log';
import {
ensureAppIdentifierIsDefinedAsync,
getProjectConfigDescription,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { Platform } from '../types';
import { updateAppJsonConfigAsync } from '../utils/appJson';
enum BundleIdentiferSource {
XcodeProject,
AppJson,
}
function isBundleIdentifierValid(bundleIdentifier: string): boolean {
return /^[a-zA-Z][a-zA-Z0-9\-.]+$/.test(bundleIdentifier);
}
export async function ensureBundleIdentifierIsValidAsync(projectDir: string, exp: ExpoConfig) {
const bundleIdentifier = await ensureAppIdentifierIsDefinedAsync({
projectDir,
platform: Platform.IOS,
exp,
});
if (!isBundleIdentifierValid(bundleIdentifier)) {
const configDescription = getProjectConfigDescription(projectDir);
Log.error(
`Invalid format of iOS bundleId. Only alphanumeric characters, '.' and '-' are allowed, and each '.' must be followed by a letter.`
);
Log.error(`Update "ios.bundleIdentifier" in ${configDescription} and run this command again.`);
throw new Error('Invalid bundleIdentifier');
}
}
export async function configureBundleIdentifierAsync(
projectDir: string,
exp: ExpoConfig
): Promise<void> {
const configDescription = getProjectConfigDescription(projectDir);
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
projectDir
);
const bundleIdentifierFromConfig = IOSConfig.BundleIdentifier.getBundleIdentifier(exp);
if (bundleIdentifierFromPbxproj && bundleIdentifierFromConfig) {
if (bundleIdentifierFromPbxproj !== bundleIdentifierFromConfig) {
Log.addNewLineIfNone();
Log.warn(
`We detected that your Xcode project is configured with a different bundle identifier than the one defined in ${configDescription}.`
);
const hasBundleIdentifierInStaticConfig = await hasBundleIdentifierInStaticConfigAsync(
projectDir,
exp
);
if (!hasBundleIdentifierInStaticConfig) {
Log.log(`If you choose the one defined in ${configDescription} we'll automatically configure your Xcode project with it.
However, if you choose the one defined in the Xcode project you'll have to update ${configDescription} on your own.`);
}
Log.newLine();
const { bundleIdentifierSource } = await promptAsync({
type: 'select',
name: 'bundleIdentifierSource',
message: 'Which bundle identifier should we use?',
choices: [
{
title: `${chalk.bold(bundleIdentifierFromPbxproj)} - In Xcode project`,
value: BundleIdentiferSource.XcodeProject,
},
{
title: `${chalk.bold(bundleIdentifierFromConfig)} - In your ${configDescription}`,
value: BundleIdentiferSource.AppJson,
},
],
});
switch (bundleIdentifierSource) {
case BundleIdentiferSource.AppJson: {
IOSConfig.BundleIdentifier.setBundleIdentifierForPbxproj(
projectDir,
bundleIdentifierFromConfig
);
break;
}
case BundleIdentiferSource.XcodeProject: {
if (hasBundleIdentifierInStaticConfig) {
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.ios = { ...config.ios, bundleIdentifier: bundleIdentifierFromPbxproj };
});
} else {
throw new Error(missingBundleIdentifierMessage(configDescription));
}
break;
}
}
}
} else if (!bundleIdentifierFromPbxproj && !bundleIdentifierFromConfig) {
throw new Error(missingBundleIdentifierMessage(configDescription));
} else if (bundleIdentifierFromPbxproj && !bundleIdentifierFromConfig) {
if (getConfigFilePaths(projectDir).staticConfigPath) {
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.ios = { ...config.ios, bundleIdentifier: bundleIdentifierFromPbxproj };
});
} else {
throw new Error(missingBundleIdentifierMessage(configDescription));
}
} else if (!bundleIdentifierFromPbxproj && bundleIdentifierFromConfig) {
IOSConfig.BundleIdentifier.setBundleIdentifierForPbxproj(
projectDir,
bundleIdentifierFromConfig
);
}
}
function missingBundleIdentifierMessage(configDescription: string): string {
return `Please define "ios.bundleIdentifier" in ${configDescription} and run "eas build:configure" again.`;
}
/**
* Check if static config exists and if ios.bundleIdentifier is defined there.
* It will return false if the value in static config is different than "ios.bundleIdentifier" in ExpoConfig
*/
async function hasBundleIdentifierInStaticConfigAsync(
projectDir: string,
exp: ExpoConfig
): Promise<boolean> {
if (!exp.ios?.bundleIdentifier) {
return false;
}
const paths = getConfigFilePaths(projectDir);
if (!paths.staticConfigPath) {
return false;
}
const rawStaticConfig = await fs.readJson(paths.staticConfigPath);
return rawStaticConfig?.expo?.ios?.bundleIdentifier === exp.ios.bundleIdentifier;
}
+7 -32
View File
@@ -1,35 +1,24 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { Workflow } from '@expo/eas-build-job';
import {
EasJsonReader,
IosBuildProfile,
IosGenericBuildProfile,
VersionAutoIncrement,
} from '@expo/eas-json';
import { IosBuildProfile, VersionAutoIncrement } from '@expo/eas-json';
import Log from '../../log';
import {
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
warnIfBundleIdentifierDefinedInAppConfigForGenericProject,
} from '../../project/ios/bundleIdentifier';
import { ConfigureContext } from '../context';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
import {
configureBundleIdentifierAsync,
ensureBundleIdentifierIsValidAsync,
} from './bundleIdentifer';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
if (!ctx.hasIosNativeProject) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
return;
}
const disableIosBundleIdentifierValidation = (
await new EasJsonReader(ctx.projectDir, 'all').readRawAsync()
)?.experimental?.disableIosBundleIdentifierValidation;
if (!disableIosBundleIdentifierValidation) {
await configureBundleIdentifierAsync(ctx.projectDir, ctx.exp);
await ensureBundleIdentifierIsValidAsync(ctx.projectDir, ctx.exp);
}
warnIfBundleIdentifierDefinedInAppConfigForGenericProject(ctx.projectDir, ctx.exp);
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
@@ -50,20 +39,6 @@ export async function validateAndSyncProjectConfigurationAsync({
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement);
if (workflow === Workflow.GENERIC) {
const genericBuildProfile = buildProfile as IosGenericBuildProfile;
if (!genericBuildProfile.disableIosBundleIdentifierValidation) {
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
projectDir
);
if (
!bundleIdentifierFromPbxproj ||
bundleIdentifierFromPbxproj !== exp.ios?.bundleIdentifier
) {
throw new Error(
'Bundle identifier is not configured correctly in your Xcode project. Please run "eas build:configure" to configure it.'
);
}
}
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
}
+27 -54
View File
@@ -1,76 +1,49 @@
import { Workflow } from '@expo/eas-build-job';
import { CredentialsSource, IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import assert from 'assert';
import { createCredentialsContextAsync } from '../../credentials/context';
import IosCredentialsProvider, {
IosCredentials,
} from '../../credentials/ios/IosCredentialsProvider';
import { getAppLookupParamsFromContext } from '../../credentials/ios/actions/new/BuildCredentialsUtils';
import { AppLookupParams } from '../../credentials/ios/credentials';
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
import { resolveEntitlementsJsonAsync } from '../../credentials/ios/appstore/entitlements';
import { IosCredentials, Target } from '../../credentials/ios/types';
import { CredentialsResult } from '../build';
import { BuildContext } from '../context';
import { ensureCredentialsAsync } from '../credentials';
import { Platform } from '../types';
import { logCredentialsSource } from '../utils/credentials';
export async function ensureIosCredentialsAsync(
ctx: BuildContext<Platform.IOS>
buildCtx: BuildContext<Platform.IOS>,
targets: Target[]
): Promise<CredentialsResult<IosCredentials> | undefined> {
if (!shouldProvideCredentials(ctx)) {
if (!shouldProvideCredentials(buildCtx)) {
return;
}
assert(ctx.commandCtx.exp?.ios?.bundleIdentifier, 'ios.bundleIdentifier is required');
return await resolveIosCredentialsAsync(ctx.commandCtx.projectDir, {
app: {
accountName: ctx.commandCtx.accountName,
projectName: ctx.commandCtx.projectName,
bundleIdentifier: ctx.commandCtx.exp.ios.bundleIdentifier,
},
workflow: ctx.buildProfile.workflow,
credentialsSource: ctx.buildProfile.credentialsSource,
distribution: ctx.buildProfile.distribution ?? 'store',
enterpriseProvisioning: ctx.buildProfile.enterpriseProvisioning,
nonInteractive: ctx.commandCtx.nonInteractive,
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
});
}
interface ResolveCredentialsParams {
app: AppLookupParams;
workflow: Workflow;
credentialsSource: CredentialsSource;
distribution: IosDistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
nonInteractive: boolean;
skipCredentialsCheck: boolean;
}
const { projectDir } = buildCtx.commandCtx;
export async function resolveIosCredentialsAsync(
projectDir: string,
params: ResolveCredentialsParams
): Promise<CredentialsResult<IosCredentials>> {
const ctx = await createCredentialsContextAsync(projectDir, {
nonInteractive: params.nonInteractive,
nonInteractive: buildCtx.commandCtx.nonInteractive,
});
const app = getAppLookupParamsFromContext(ctx);
const provider = new IosCredentialsProvider(ctx, {
app,
distribution: params.distribution,
enterpriseProvisioning: params.enterpriseProvisioning,
skipCredentialsCheck: params.skipCredentialsCheck,
app: getAppFromContext(ctx),
targets,
iosCapabilitiesOptions: {
entitlements: await resolveEntitlementsJsonAsync(
buildCtx.commandCtx.projectDir,
buildCtx.buildProfile.workflow
),
},
distribution: buildCtx.buildProfile.distribution ?? 'store',
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
});
const credentialsSource = await ensureCredentialsAsync(
provider,
params.workflow,
params.credentialsSource,
params.nonInteractive
);
const { credentialsSource } = buildCtx.buildProfile;
logCredentialsSource(credentialsSource, Platform.IOS);
return {
credentials: await provider.getCredentialsAsync(credentialsSource),
source: credentialsSource,
};
}
function shouldProvideCredentials(ctx: BuildContext<Platform.IOS>): boolean {
return ctx.buildProfile.distribution !== 'simulator';
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
return buildCtx.buildProfile.distribution !== 'simulator';
}
+3 -14
View File
@@ -7,7 +7,6 @@ import {
IosJobSecretsInput,
IosManagedBuildType,
IosManagedJobInput,
IosSchemeBuildConfiguration,
} from '../../graphql/generated';
import { transformProjectArchive } from '../graphql';
@@ -16,14 +15,13 @@ export function transformGenericJob(job: Ios.GenericJob): IosGenericJobInput {
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,
scheme: job.scheme,
schemeBuildConfiguration:
job.schemeBuildConfiguration &&
transformSchemeBuildConfiguration(job.schemeBuildConfiguration),
buildConfiguration: job.buildConfiguration,
artifactPath: job.artifactPath,
};
}
@@ -33,6 +31,7 @@ export function transformManagedJob(job: Ios.ManagedJob): IosManagedJobInput {
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,
@@ -74,16 +73,6 @@ function transformIosSecrets(secrets: {
};
}
function transformSchemeBuildConfiguration(
schemeBuildConfiguration: Ios.SchemeBuildConfiguration
): IosSchemeBuildConfiguration {
if (schemeBuildConfiguration === Ios.SchemeBuildConfiguration.DEBUG) {
return IosSchemeBuildConfiguration.Debug;
} else {
return IosSchemeBuildConfiguration.Release;
}
}
function transformBuildType(buildType: Ios.ManagedBuildType): IosManagedBuildType {
if (buildType === Ios.ManagedBuildType.DEVELOPMENT_CLIENT) {
return IosManagedBuildType.DevelopmentClient;
+40 -60
View File
@@ -1,26 +1,19 @@
import { ArchiveSource, Cache, Ios, Job, Workflow, sanitizeJob } from '@expo/eas-build-job';
import { IosGenericBuildProfile, IosManagedBuildProfile } from '@expo/eas-json';
import assert from 'assert';
import path from 'path';
import semver from 'semver';
import {
IosCredentials,
IosTargetCredentials,
isCredentialsMap,
readEnvironmentSecretsAsync,
} from '../../credentials/credentialsJson/read';
import { getProjectAccountNameAsync } from '../../project/projectUtils';
import { gitRootDirectoryAsync } from '../../utils/git';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import vcs from '../../vcs';
import { BuildContext } from '../context';
import { Platform } from '../types';
interface JobData {
projectArchive: ArchiveSource;
credentials?: IosCredentials;
projectConfiguration: {
iosBuildScheme?: string;
iosApplicationTarget?: string;
};
buildScheme: string;
}
export async function prepareJobAsync(
@@ -42,7 +35,6 @@ interface CommonJobProperties {
platform: Platform.IOS;
projectArchive: ArchiveSource;
builderEnvironment: Ios.BuilderEnvironment;
releaseChannel?: string;
distribution?: Ios.DistributionType;
cache: Cache;
secrets: {
@@ -53,28 +45,14 @@ interface CommonJobProperties {
async function prepareJobCommonAsync(
ctx: BuildContext<Platform.IOS>,
{
credentials,
targetName,
projectArchive,
}: { credentials?: IosCredentials; targetName?: string; projectArchive: ArchiveSource }
{ credentials, projectArchive }: { credentials?: IosCredentials; projectArchive: ArchiveSource }
): Promise<Partial<CommonJobProperties>> {
const environmentSecrets = await readEnvironmentSecretsAsync(ctx.commandCtx.projectDir);
let buildCredentials: CommonJobProperties['secrets']['buildCredentials'] = {};
if (credentials && isCredentialsMap(credentials)) {
const targets = Object.keys(credentials);
for (const target of targets) {
buildCredentials[target] = prepareTargetCredentials(credentials[target]);
const buildCredentials: CommonJobProperties['secrets']['buildCredentials'] = {};
if (credentials) {
const targetNames = Object.keys(credentials);
for (const targetName of targetNames) {
buildCredentials[targetName] = prepareTargetCredentials(credentials[targetName]);
}
} else if (credentials) {
// targetName
// - for managed projects: sanitized .name from the app config
// - for generic projects: name of the application target
assert(targetName, 'target name should be defined');
buildCredentials = {
[targetName]: prepareTargetCredentials(credentials),
};
}
return {
@@ -82,28 +60,42 @@ async function prepareJobCommonAsync(
projectArchive,
distribution: ctx.buildProfile.distribution,
builderEnvironment: {
image: ctx.buildProfile.image,
image: resolveImage(ctx),
node: ctx.buildProfile.node,
yarn: ctx.buildProfile.yarn,
bundler: ctx.buildProfile.bundler,
cocoapods: ctx.buildProfile.cocoapods,
fastlane: ctx.buildProfile.fastlane,
expoCli: ctx.buildProfile.expoCli,
env: ctx.buildProfile.env,
},
cache: ctx.buildProfile.cache,
cache: {
...ctx.buildProfile.cache,
clear: ctx.commandCtx.clearCache,
},
secrets: {
...(environmentSecrets ? { environmentSecrets } : {}),
buildCredentials,
},
};
}
function prepareTargetCredentials(targetCredentials: IosTargetCredentials): Ios.TargetCredentials {
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 (majorSdkVersion <= 41) {
return 'macos-catalina-10.15-xcode-12.4';
}
}
return ctx.buildProfile.image ?? 'default';
}
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
return {
provisioningProfileBase64: targetCredentials.provisioningProfile,
distributionCertificate: {
dataBase64: targetCredentials.distributionCertificate.certP12,
password: targetCredentials.distributionCertificate.certPassword,
dataBase64: targetCredentials.distributionCertificate.certificateP12,
password: targetCredentials.distributionCertificate.certificatePassword,
},
};
}
@@ -114,21 +106,18 @@ async function prepareGenericJobAsync(
buildProfile: IosGenericBuildProfile
): Promise<Partial<Ios.GenericJob>> {
const projectRootDirectory =
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
return {
...(await prepareJobCommonAsync(ctx, {
credentials: jobData.credentials,
targetName: jobData.projectConfiguration.iosApplicationTarget,
projectArchive: jobData.projectArchive,
})),
type: Workflow.GENERIC,
scheme: jobData.projectConfiguration.iosBuildScheme,
schemeBuildConfiguration:
buildProfile.schemeBuildConfiguration === 'Auto'
? undefined
: buildProfile.schemeBuildConfiguration,
scheme: jobData.buildScheme,
buildConfiguration: buildProfile.schemeBuildConfiguration,
artifactPath: buildProfile.artifactPath,
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
projectRootDirectory,
};
}
@@ -139,27 +128,18 @@ async function prepareManagedJobAsync(
buildProfile: IosManagedBuildProfile
): Promise<Partial<Ios.ManagedJob>> {
const projectRootDirectory =
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.exp);
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
return {
...(await prepareJobCommonAsync(ctx, {
credentials: jobData.credentials,
targetName: jobData.projectConfiguration.iosApplicationTarget,
projectArchive: jobData.projectArchive,
})),
type: Workflow.MANAGED,
buildType: buildProfile.buildType,
username: accountName,
username,
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
projectRootDirectory,
};
}
// copy-pasted from expo-cli/packages/xdl/src/Exp.ts
// it's used in eject
export function sanitizedTargetName(name: string) {
return name
.replace(/[\W_]+/g, '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '');
}
+31 -3
View File
@@ -1,10 +1,12 @@
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import semver from 'semver';
import Log from '../../log';
import { resolveWorkflow } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { readPlistAsync, writePlistAsync } from './plist';
@@ -100,9 +102,30 @@ export async function bumpVersionInAppJsonAsync({
}
}
async function readInfoPlistAsync(projectDir: string): Promise<IOSConfig.InfoPlist> {
const infoPlistPath = IOSConfig.Paths.getInfoPlistPath(projectDir);
return await readPlistAsync(infoPlistPath);
export async function readShortVersionAsync(
projectDir: string,
exp: ExpoConfig
): Promise<string | undefined> {
const workflow = resolveWorkflow(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir);
return infoPlist.CFBundleShortVersionString;
} else {
return exp.version;
}
}
export async function readBuildNumberAsync(
projectDir: string,
exp: ExpoConfig
): Promise<string | undefined> {
const workflow = resolveWorkflow(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir);
return infoPlist.CFBundleVersion;
} else {
return IOSConfig.Version.getBuildNumber(exp);
}
}
async function writeVersionsToInfoPlistAsync({
@@ -120,6 +143,11 @@ async function writeVersionsToInfoPlistAsync({
return updatedInfoPlist;
}
async function readInfoPlistAsync(projectDir: string): Promise<IOSConfig.InfoPlist> {
const infoPlistPath = IOSConfig.Paths.getInfoPlistPath(projectDir);
return (await readPlistAsync(infoPlistPath)) as IOSConfig.InfoPlist;
}
async function writeInfoPlistAsync({
projectDir,
infoPlist,
+98 -27
View File
@@ -1,11 +1,22 @@
import { Metadata } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import { getAppIdentifierAsync } from '../project/projectUtils';
import { gitCommitHashAsync } from '../utils/git';
import { readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync } from './android/UpdatesModule';
import { getApplicationId } from '../project/android/applicationId';
import { getBundleIdentifier } from '../project/ios/bundleIdentifier';
import { getUsername } from '../project/projectUtils';
import { ensureLoggedInAsync } from '../user/actions';
import vcs from '../vcs';
import {
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
} from './android/UpdatesModule';
import { readVersionCode, readVersionName } from './android/version';
import { BuildContext } from './context';
import { readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync } from './ios/UpdatesModule';
import {
readChannelSafelyAsync as readIosChannelSafelyAsync,
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
} from './ios/UpdatesModule';
import { readBuildNumberAsync, readShortVersionAsync } from './ios/version';
import { Platform } from './types';
import { isExpoUpdatesInstalled } from './utils/updates';
@@ -24,43 +35,103 @@ export async function collectMetadata<T extends Platform>(
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
}
): Promise<Metadata> {
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
return {
// TODO: type error fixed in https://github.com/expo/eas-build/pull/34
// remove @ts-expect-error after upgrading @expo/eas-build-job
// @ts-expect-error error TS2322: not assignable to type 'Record<string, string | number>'
trackingContext: ctx.trackingCtx,
appVersion: ctx.commandCtx.exp.version!,
appVersion: await resolveAppVersionAsync(ctx),
appBuildVersion: await resolveAppBuildVersionAsync(ctx),
cliVersion: packageJSON.version,
workflow: ctx.buildProfile.workflow,
credentialsSource,
sdkVersion: ctx.commandCtx.exp.sdkVersion,
releaseChannel: await resolveReleaseChannel(ctx),
...channelOrReleaseChannel,
distribution: ctx.buildProfile.distribution ?? 'store',
appName: ctx.commandCtx.exp.name,
appIdentifier:
(await getAppIdentifierAsync({
projectDir: ctx.commandCtx.projectDir,
platform: ctx.platform,
exp: ctx.commandCtx.exp,
})) ?? undefined,
appIdentifier: resolveAppIdentifier(ctx),
buildProfile: ctx.commandCtx.profile,
gitCommitHash: await gitCommitHashAsync(),
gitCommitHash: await vcs.getCommitHashAsync(),
username: getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync()),
};
}
async function resolveReleaseChannel<T extends Platform>(
async function resolveAppVersionAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<string | undefined> {
if (!isExpoUpdatesInstalled(ctx.commandCtx.projectDir)) {
return undefined;
}
if (ctx.buildProfile.releaseChannel) {
return ctx.buildProfile.releaseChannel;
}
let maybeReleaseChannel: string | null;
if (ctx.platform === Platform.ANDROID) {
maybeReleaseChannel = await readAndroidReleaseChannelSafelyAsync(ctx.commandCtx.projectDir);
if (ctx.platform === Platform.IOS) {
return await readShortVersionAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
} else {
maybeReleaseChannel = await readIosReleaseChannelSafelyAsync(ctx.commandCtx.projectDir);
return readVersionName(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
}
return maybeReleaseChannel ?? 'default';
}
async function resolveAppBuildVersionAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<string | undefined> {
if (ctx.platform === Platform.IOS) {
return await readBuildNumberAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
} else {
const versionCode = readVersionCode(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return versionCode !== undefined ? String(versionCode) : undefined;
}
}
function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string {
if (ctx.platform === Platform.IOS) {
return getBundleIdentifier(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
} else {
return getApplicationId(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
}
}
async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<{ channel: string } | { releaseChannel: string } | null> {
if (!isExpoUpdatesInstalled(ctx.commandCtx.projectDir)) {
return null;
}
if (ctx.buildProfile.channel) {
return { channel: ctx.buildProfile.channel };
}
if (ctx.buildProfile.releaseChannel) {
return { releaseChannel: ctx.buildProfile.releaseChannel };
}
const channel = await getNativeChannelAsync(ctx);
if (channel) {
return { channel };
}
const releaseChannel = await getNativeReleaseChannelAsync(ctx);
return { releaseChannel };
}
async function getNativeReleaseChannelAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<string> {
switch (ctx.platform) {
case Platform.ANDROID: {
return (await readAndroidReleaseChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? 'default';
}
case Platform.IOS: {
return (await readIosReleaseChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? 'default';
}
default:
return 'default';
}
}
async function getNativeChannelAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<string | undefined> {
switch (ctx.platform) {
case Platform.ANDROID: {
return (await readAndroidChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? undefined;
}
case Platform.IOS: {
return (await readIosChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? undefined;
}
}
return undefined;
}
+2 -1
View File
@@ -7,6 +7,7 @@ export enum RequestedPlatform {
}
export enum BuildStatus {
NEW = 'new',
IN_QUEUE = 'in-queue',
IN_PROGRESS = 'in-progress',
ERRORED = 'errored',
@@ -14,4 +15,4 @@ export enum BuildStatus {
CANCELED = 'canceled',
}
export type TrackingContext = Record<string, string | number>;
export type TrackingContext = Record<string, string | number | boolean>;
@@ -1,4 +1,3 @@
// import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import * as ExpoConfig from '@expo/config';
import assert from 'assert';
import fs from 'fs-extra';
@@ -0,0 +1,15 @@
import { Platform } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import Log from '../../log';
import { requestedPlatformDisplayNames } from '../constants';
export function logCredentialsSource(credentialsSource: CredentialsSource, platform: Platform) {
let message = `Using ${credentialsSource} ${requestedPlatformDisplayNames[platform]} credentials`;
if (credentialsSource === CredentialsSource.LOCAL)
message += ` ${chalk.dim('(credentials.json)')}`;
else if (credentialsSource === CredentialsSource.REMOTE)
message += ` ${chalk.dim('(Expo server)')}`;
Log.succeed(message);
}
+10 -10
View File
@@ -18,6 +18,8 @@ export function formatGraphQLBuild(build: BuildFragment) {
label: 'Status',
get value() {
switch (build.status) {
case GraphQLBuildStatus.New:
return chalk.blue('new');
case GraphQLBuildStatus.InQueue:
return chalk.blue('in queue');
case GraphQLBuildStatus.InProgress:
@@ -48,12 +50,8 @@ export function formatGraphQLBuild(build: BuildFragment) {
{
label: 'Artifact',
get value() {
if (
build.status === GraphQLBuildStatus.InQueue ||
build.status === GraphQLBuildStatus.InProgress
) {
}
switch (build.status) {
case GraphQLBuildStatus.New:
case GraphQLBuildStatus.InQueue:
case GraphQLBuildStatus.InProgress:
return '<in progress>';
@@ -72,11 +70,13 @@ export function formatGraphQLBuild(build: BuildFragment) {
{ label: 'Started at', value: new Date(build.createdAt).toLocaleString() },
{
label: 'Finished at',
value:
build.status === GraphQLBuildStatus.InQueue ||
build.status === GraphQLBuildStatus.InProgress
? '<in progress>'
: new Date(build.updatedAt).toLocaleString(),
value: [
GraphQLBuildStatus.New,
GraphQLBuildStatus.InQueue,
GraphQLBuildStatus.InProgress,
].includes(build.status)
? '<in progress>'
: new Date(build.updatedAt).toLocaleString(),
},
{ label: 'Started by', value: actor ?? 'unknown' },
];
@@ -1,5 +1,7 @@
import assert from 'assert';
import chalk from 'chalk';
import indentString from 'indent-string';
import qrcodeTerminal from 'qrcode-terminal';
import {
BuildError,
@@ -15,7 +17,7 @@ import {
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../constants';
import { getBuildLogsUrl } from './url';
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
export function printLogsUrls(
accountName: string,
@@ -82,10 +84,12 @@ function printBuildResult(accountName: string, build: BuildFragment): void {
buildId: build.id,
account: accountName,
});
const installUrl = getInternalDistributionInstallUrl(build);
qrcodeTerminal.generate(installUrl, code => console.log(`${indentString(code, 2)}\n`));
Log.log(
`${appPlatformEmojis[build.platform]} Open this link on your ${
appPlatformDisplayNames[build.platform]
} devices to install the app:`
} devices (or scan the QR code) to install the app:`
);
Log.log(`${chalk.underline(logsUrl)}`);
} else {
+26 -97
View File
@@ -1,4 +1,3 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import fs from 'fs-extra';
import ora from 'ora';
@@ -9,55 +8,16 @@ import { v4 as uuidv4 } from 'uuid';
import Log from '../../log';
import { confirmAsync, promptAsync } from '../../prompts';
import { formatBytes } from '../../utils/files';
import {
doesGitRepoExistAsync,
getGitDiffOutputAsync,
gitDiffAsync,
gitRootDirectoryAsync,
gitStatusAsync,
isGitInstalledAsync,
} from '../../utils/git';
import { getTmpDirectory } from '../../utils/paths';
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
import vcs from '../../vcs';
async function ensureGitRepoExistsAsync(): Promise<void> {
if (!(await isGitInstalledAsync())) {
throw new Error('git command not found, install it before proceeding');
}
if (await doesGitRepoExistAsync()) {
return;
}
Log.warn("It looks like you haven't initialized the git repository yet.");
Log.warn('EAS Build requires you to use a git repository for your project.');
const confirmInit = await confirmAsync({
message: `Would you like to run 'git init' in the current directory?`,
});
if (!confirmInit) {
throw new Error(
'A git repository is required for building your project. Initialize it and run this command again.'
);
}
await spawnAsync('git', ['init']);
Log.log("We're going to make an initial commit for you repository.");
await commitPromptAsync({ initialCommitMessage: 'Initial commit', commitAllFiles: true });
}
async function isGitStatusCleanAsync(): Promise<boolean> {
const changes = await gitStatusAsync({ showUntracked: true });
return changes.length === 0;
}
async function maybeBailOnGitStatusAsync(): Promise<void> {
if (await isGitStatusCleanAsync()) {
export async function maybeBailOnRepoStatusAsync(): Promise<void> {
if (!(await vcs.hasUncommittedChangesAsync())) {
return;
}
Log.addNewLineIfNone();
Log.warn(`${chalk.bold('Warning!')} Your git working tree is dirty.`);
Log.warn(`${chalk.bold('Warning!')} Your repository working tree is dirty.`);
Log.log(
`It's recommended to ${chalk.bold(
'commit all your changes before proceeding'
@@ -72,12 +32,12 @@ async function maybeBailOnGitStatusAsync(): Promise<void> {
}
}
async function ensureGitStatusIsCleanAsync(nonInteractive = false): Promise<void> {
if (await isGitStatusCleanAsync()) {
export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<void> {
if (!(await vcs.hasUncommittedChangesAsync())) {
return;
}
Log.addNewLineIfNone();
Log.warn(`${chalk.bold('Warning!')} Your git working tree is dirty.`);
Log.warn(`${chalk.bold('Warning!')} Your repository working tree is dirty.`);
Log.log(
`This operation needs to be run on a clean working tree, please ${chalk.bold(
'commit all your changes before proceeding'
@@ -96,7 +56,24 @@ async function ensureGitStatusIsCleanAsync(nonInteractive = false): Promise<void
}
}
async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
export async function commitPromptAsync({
initialCommitMessage,
commitAllFiles,
}: {
initialCommitMessage?: string;
commitAllFiles?: boolean;
} = {}): Promise<void> {
const { message } = await promptAsync({
type: 'text',
name: 'message',
message: 'Commit message:',
initial: initialCommitMessage,
validate: (input: string) => input !== '',
});
await vcs.commitAsync({ commitAllFiles, commitMessage: message });
}
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
const spinner = ora('Compressing project files');
await fs.mkdirp(getTmpDirectory());
@@ -118,14 +95,7 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
startTimer(compressTimerLabel);
try {
await spawnAsync('git', [
'clone',
'--no-hardlinks',
'--depth',
'1',
`file://${await gitRootDirectoryAsync()}`,
shallowClonePath,
]);
await vcs.makeShallowCopyAsync(shallowClonePath);
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
'.',
]);
@@ -151,44 +121,3 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
return { size, path: tarPath };
}
async function showDiffAsync() {
const outputTooLarge = (await getGitDiffOutputAsync()).split(/\r\n|\r|\n/).length > 100;
await gitDiffAsync({ withPager: outputTooLarge });
}
async function commitPromptAsync({
initialCommitMessage,
commitAllFiles,
}: {
initialCommitMessage?: string;
commitAllFiles?: boolean;
} = {}): Promise<void> {
const { message } = await promptAsync({
type: 'text',
name: 'message',
message: 'Commit message:',
initial: initialCommitMessage,
validate: (input: string) => input !== '',
});
if (commitAllFiles) {
await spawnAsync('git', ['add', '-A']);
}
await commitChangedFilesAsync(message);
}
async function commitChangedFilesAsync(message: string): Promise<void> {
await spawnAsync('git', ['add', '-u']);
await spawnAsync('git', ['commit', '-m', message]);
}
export {
isGitStatusCleanAsync,
showDiffAsync,
commitPromptAsync,
commitChangedFilesAsync,
ensureGitRepoExistsAsync,
ensureGitStatusIsCleanAsync,
maybeBailOnGitStatusAsync,
makeProjectTarballAsync,
};
+16 -1
View File
@@ -1,4 +1,7 @@
import { getExpoWebsiteBaseUrl } from '../../api';
import assert from 'assert';
import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api';
import { AppPlatform, BuildFragment } from '../../graphql/generated';
export function getBuildLogsUrl({
buildId,
@@ -13,3 +16,15 @@ export function getBuildLogsUrl({
export function getArtifactUrl(artifactId: string): string {
return `${getExpoWebsiteBaseUrl()}/artifacts/${artifactId}`;
}
export function getInternalDistributionInstallUrl(build: BuildFragment): string {
if (build.platform === AppPlatform.Ios) {
return `itms-services://?action=download-manifest;url=${getExpoApiBaseUrl()}/--/api/v2/projects/${
build.project.id
}/builds/${build.id}/manifest.plist`;
}
assert(build.artifacts?.buildUrl, 'buildUrl is missing');
return build.artifacts.buildUrl;
}
@@ -0,0 +1,38 @@
import { Command } from '@oclif/command';
import {
AnalyticsEvent,
flushAsync as flushAnalyticsAsync,
initAsync as initAnalyticsAsync,
logEvent,
} from '../analytics';
import { getUserAsync } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
export default abstract class EasCommand extends Command {
/**
* When user data is unavailable locally, determines if the command will
* force the user to log in
*/
protected requiresAuthentication = true;
async init() {
await initAnalyticsAsync();
if (this.requiresAuthentication) {
await ensureLoggedInAsync();
} else {
await getUserAsync();
}
logEvent(AnalyticsEvent.ACTION, {
// id is assigned by oclif in constructor based on the filepath:
// commands/submit === submit, commands/build/list === build:list
action: `eas ${this.id}`,
});
}
async finally(err: Error) {
await flushAnalyticsAsync();
return super.finally(err);
}
}
@@ -0,0 +1,78 @@
import { flushAsync, initAsync, logEvent } from '../../analytics';
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
import { getUserAsync } from '../../user/User';
import { ensureLoggedInAsync } from '../../user/actions';
import EasCommand from '../EasCommand';
import TestEasCommand from './TestEasCommand';
describe(EasCommand.name, () => {
beforeAll(() => {
TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
jest.mock('../../user/User', () => ({ getUserAsync: jest.fn(() => mockJester) }));
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
jest.mock('../../analytics', () => {
const { AnalyticsEvent } = jest.requireActual('../../analytics');
return {
AnalyticsEvent,
logEvent: jest.fn(),
initAsync: jest.fn(),
flushAsync: jest.fn(),
};
});
});
afterEach(() => {
(ensureLoggedInAsync as jest.Mock).mockClear();
(initAsync as jest.Mock).mockClear();
(flushAsync as jest.Mock).mockClear();
(logEvent as jest.Mock).mockClear();
});
describe('without exceptions', () => {
it('ensures the user is logged in', async () => {
await TestEasCommand.run();
expect(ensureLoggedInAsync).toHaveReturnedWith(mockJester);
});
it('ensures the user data is read from cache', async () => {
(TestEasCommand.prototype.authValue as jest.Mock).mockImplementationOnce(() => false);
await TestEasCommand.run();
expect(getUserAsync).toHaveReturnedWith(mockJester);
});
it('initializes analytics', async () => {
await TestEasCommand.run();
expect(initAsync).toHaveBeenCalled();
});
it('flushes analytics', async () => {
await TestEasCommand.run();
expect(flushAsync).toHaveBeenCalled();
});
it('logs events', async () => {
await TestEasCommand.run();
expect(logEvent).toHaveBeenCalledWith('action', {
action: `eas ${TestEasCommand.id}`,
});
});
});
describe('after exceptions', () => {
it('flushes analytics', async () => {
try {
await TestEasCommand.run().then(() => {
throw new Error('foo');
});
} catch (error) {}
expect(flushAsync).toHaveBeenCalled();
});
});
});
@@ -0,0 +1,13 @@
import EasCommand from '../EasCommand';
export default class TestEasCommand extends EasCommand {
requiresAuthentication = this.authValue();
authValue() {
return false;
}
async run() {}
}
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
+10 -13
View File
@@ -10,11 +10,13 @@ import {
UpdateBranch,
} from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import { findProjectRootAsync, getProjectAccountName } from '../../project/projectUtils';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions';
import { getBranchNameAsync } from '../../utils/git';
import vcs from '../../vcs';
export async function createUpdateBranchOnAppAsync({
appId,
@@ -78,11 +80,8 @@ export default class BranchCreate extends Command {
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const projectId = await ensureProjectExistsAsync({
accountName,
projectName: exp.slug,
});
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!name) {
const validationMessage = 'Branch name may not be empty.';
@@ -94,7 +93,7 @@ export default class BranchCreate extends Command {
name: 'name',
message: 'Please name the branch:',
initial:
(await getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`,
(await vcs.getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`,
validate: value => (value ? true : validationMessage),
}));
}
@@ -107,9 +106,7 @@ export default class BranchCreate extends Command {
}
Log.withTick(
`️Created a new branch: ${chalk.bold(newBranch.name)} on project ${chalk.bold(
`@${accountName}/${exp.slug}`
)}.`
`️Created a new branch: ${chalk.bold(newBranch.name)} on project ${chalk.bold(fullName)}.`
);
}
}
+8 -11
View File
@@ -12,10 +12,12 @@ import {
GetBranchInfoQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import { findProjectRootAsync, getProjectAccountName } from '../../project/projectUtils';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions';
async function getBranchInfoAsync({
appId,
@@ -104,11 +106,8 @@ export default class BranchDelete extends Command {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const projectId = await ensureProjectExistsAsync({
accountName,
projectName: exp.slug,
});
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!name) {
const validationMessage = 'branch name may not be empty.';
@@ -148,9 +147,7 @@ export default class BranchDelete extends Command {
}
Log.withTick(
`️Deleted branch "${name}" and all of its updates on project ${chalk.bold(
`@${accountName}/${exp.slug}`
)}.`
`️Deleted branch "${name}" and all of its updates on project ${chalk.bold(fullName)}.`
);
}
}
+17 -35
View File
@@ -3,42 +3,28 @@ import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import CliTable from 'cli-table3';
import gql from 'graphql-tag';
import { format } from 'timeago.js';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
BranchesByAppQuery,
BranchesByAppQueryVariables,
Maybe,
Robot,
Update,
User,
} from '../../graphql/generated';
import { BranchesByAppQuery, BranchesByAppQueryVariables } from '../../graphql/generated';
import Log from '../../log';
import { findProjectRootAsync, getProjectFullNameAsync } from '../../project/projectUtils';
import { getActorDisplayName } from '../../user/User';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
export type FormatUpdateParameter = Pick<Update, 'id' | 'createdAt' | 'message'> & {
actor?: Maybe<Pick<User, 'username' | 'id'> | Pick<Robot, 'firstName' | 'id'>>;
};
export const UPDATE_COLUMNS = ['update description', 'update runtime version', 'update group ID'];
const BRANCHES_LIMIT = 10_000;
export async function listBranchesAsync({ fullName }: { fullName: string }) {
export async function listBranchesAsync({ projectId }: { projectId: string }) {
const data = await withErrorHandlingAsync(
graphqlClient
.query<BranchesByAppQuery, BranchesByAppQueryVariables>(
gql`
query BranchesByAppQuery($fullName: String!, $limit: Int!) {
query BranchesByAppQuery($appId: String!, $limit: Int!) {
app {
byFullName(fullName: $fullName) {
byId(appId: $appId) {
id
fullName
updateBranches(offset: 0, limit: $limit) {
id
name
updates(offset: 0, limit: 1) {
updates(offset: 0, limit: 10) {
id
actor {
__typename
@@ -54,6 +40,7 @@ export async function listBranchesAsync({ fullName }: { fullName: string }) {
message
runtimeVersion
group
platform
}
}
}
@@ -61,14 +48,14 @@ export async function listBranchesAsync({ fullName }: { fullName: string }) {
}
`,
{
fullName,
appId: projectId,
limit: BRANCHES_LIMIT,
}
)
.toPromise()
);
return data?.app?.byFullName.updateBranches ?? [];
return data?.app?.byId.updateBranches ?? [];
}
export default class BranchList extends Command {
@@ -91,8 +78,8 @@ export default class BranchList 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 branches = await listBranchesAsync({ fullName });
const projectId = await getProjectIdAsync(exp);
const branches = await listBranchesAsync({ projectId });
if (flags.json) {
Log.log(JSON.stringify(branches, null, 2));
} else {
@@ -100,12 +87,17 @@ export default class BranchList extends Command {
head: ['branch', ...UPDATE_COLUMNS],
wordWrap: true,
});
table.push(
...branches.map(branch => [
branch.name,
formatUpdate(branch.updates[0]),
branch.updates[0]?.runtimeVersion ?? 'N/A',
branch.updates[0]?.group ?? 'N/A',
getPlatformsForGroup({
updates: branch.updates,
group: branch.updates[0]?.group,
}),
])
);
Log.log(chalk.bold('Branches with their most recent update group:'));
@@ -116,13 +108,3 @@ export default class BranchList extends Command {
}
}
}
export function formatUpdate(update: FormatUpdateParameter): string {
if (!update) {
return 'N/A';
}
const message = update.message ? `"${update.message}" ` : '';
return `${message}(${format(update.createdAt, 'en_US')} by ${getActorDisplayName(
update.actor as any
)})`;
}
+75 -26
View File
@@ -19,20 +19,26 @@ import {
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import {
PublishPlatform,
buildBundlesAsync,
buildUpdateInfoGroupAsync,
collectAssets,
uploadAssetsAsync,
} from '../../project/publish';
import { promptAsync, selectAsync } from '../../prompts';
import { getLastCommitMessageAsync } from '../../utils/git';
import { formatUpdate, listBranchesAsync } from './list';
import { formatUpdate } from '../../update/utils';
import vcs from '../../vcs';
import { listBranchesAsync } from './list';
import { viewUpdateBranchAsync } from './view';
type PublishPlatforms = 'android' | 'ios';
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
type PlatformFlag = PublishPlatform | 'all';
async function getUpdateGroupAsync({
group,
@@ -68,7 +74,6 @@ export default class BranchPublish extends Command {
static args = [
{
name: 'name',
required: true,
description: 'Name of the branch to publish on',
},
];
@@ -95,6 +100,13 @@ export default class BranchPublish extends Command {
description: `skip running Expo CLI to bundle the app before publishing`,
default: false,
}),
platform: flags.enum({
char: 'p',
description: `Only publish to a single platform`,
options: [...defaultPublishPlatforms, 'all'],
default: 'all',
required: false,
}),
json: flags.boolean({
description: `return a json with the new update group.`,
default: false,
@@ -113,6 +125,7 @@ export default class BranchPublish extends Command {
'skip-bundler': skipBundler,
},
} = this.parse(BranchPublish);
const platformFlag = this.parse(BranchPublish).flags.platform as PlatformFlag;
// If a group was specified, that means we are republishing it.
republish = group ? true : republish;
@@ -139,10 +152,7 @@ export default class BranchPublish extends Command {
"Couldn't find 'runtimeVersion'. Please specify it under the 'expo' key in 'app.json'"
);
}
const projectId = await ensureProjectExistsAsync({
accountName,
projectName: slug,
});
const projectId = await getProjectIdAsync(exp);
if (!name) {
const validationMessage = 'branch name may not be empty.';
@@ -150,7 +160,7 @@ export default class BranchPublish extends Command {
throw new Error(validationMessage);
}
const branches = await listBranchesAsync({ fullName: `@${accountName}/${slug}` });
const branches = await listBranchesAsync({ projectId });
name = await selectAsync<string>(
'which branch would you like to publish on?',
branches.map(branch => {
@@ -182,30 +192,68 @@ export default class BranchPublish extends Command {
if (group) {
updatesToRepublish = await getUpdateGroupAsync({ group });
} else {
const updateGroups = uniqBy(updates, u => u.group).map(update => ({
title: formatUpdateTitle(update),
value: update.group,
}));
// Drop into interactive mode if the user has not specified an update group to republish.
if (jsonFlag) {
throw new Error('You must specify the update group to republish.');
}
const updateGroups = uniqBy(updates, u => u.group)
.filter(update => {
// Only show groups that have updates on the specified platform(s).
return platformFlag === 'all' || update.platform === platformFlag;
})
.map(update => ({
title: formatUpdateTitle(update),
value: update.group,
}));
if (updateGroups.length === 0) {
throw new Error(
`There are no updates on branch "${name}". Did you mean to do a regular publish?`
`There are no updates on branch "${name}" published on the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
);
}
const updateGroup = await selectAsync<string>(
const selectedUpdateGroup = await selectAsync<string>(
'which update would you like to republish?',
updateGroups
);
updatesToRepublish = updates.filter(update => update.group === updateGroup);
updatesToRepublish = updates.filter(update => update.group === selectedUpdateGroup);
}
const updatesToRepublishFilteredByPlatform = updatesToRepublish.filter(
// Only republish to the specified platforms
update => platformFlag === 'all' || update.platform === platformFlag
);
if (updatesToRepublishFilteredByPlatform.length === 0) {
throw new Error(
`There are no updates on branch "${name}" published on the platform(s) "${platformFlag}" with group ID "${
group ? group : updatesToRepublish[0].group
}". Did you mean to publish a new update instead?`
);
}
for (const update of updatesToRepublish) {
const { platform, manifestFragment } = update;
updateInfoGroup[platform as PublishPlatforms] = JSON.parse(manifestFragment);
let publicationPlatformMessage: string;
if (platformFlag === 'all') {
if (updatesToRepublishFilteredByPlatform.length !== defaultPublishPlatforms.length) {
Log.warn(`You are republishing an update that wasn't published for all platforms.`);
}
publicationPlatformMessage = `The republished update will appear on the same plaforms it was originally published on: ${updatesToRepublishFilteredByPlatform
.map(update => update.platform)
.join(',')}`;
} else {
publicationPlatformMessage = `The republished update will appear only on: ${platformFlag}`;
}
Log.withTick(publicationPlatformMessage);
for (const update of updatesToRepublishFilteredByPlatform) {
const { manifestFragment } = update;
const platform = update.platform as PublishPlatform;
updateInfoGroup[platform] = JSON.parse(manifestFragment);
}
// These are the same for each member of an update group
group = updatesToRepublish[0].group;
oldMessage = updatesToRepublish[0].message ?? '';
oldRuntimeVersion = updatesToRepublish[0].runtimeVersion;
group = updatesToRepublishFilteredByPlatform[0].group;
oldMessage = updatesToRepublishFilteredByPlatform[0].message ?? '';
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
} else {
// build bundle and upload assets for a new publish
if (!skipBundler) {
@@ -214,7 +262,8 @@ export default class BranchPublish extends Command {
const assetSpinner = ora('Uploading assets...').start();
try {
const assets = collectAssets(inputDir!);
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
const assets = collectAssets({ inputDir: inputDir!, platforms });
await uploadAssetsAsync(assets);
updateInfoGroup = await buildUpdateInfoGroupAsync(assets);
assetSpinner.succeed('Uploaded assets!');
@@ -235,7 +284,7 @@ export default class BranchPublish extends Command {
message: `Please enter a publication message.`,
initial: republish
? `Republish "${oldMessage!}" - group: ${group}`
: (await getLastCommitMessageAsync())?.trim(),
: (await vcs.getLastCommitMessageAsync())?.trim(),
validate: value => (value ? true : validationMessage),
}));
}
+8 -10
View File
@@ -11,10 +11,12 @@ import {
UpdateBranch,
} from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import { findProjectRootAsync, getProjectAccountName } from '../../project/projectUtils';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions';
async function renameUpdateBranchOnAppAsync({
appId,
@@ -76,12 +78,8 @@ export default class BranchRename extends Command {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const projectId = await ensureProjectExistsAsync({
accountName,
projectName: exp.slug,
});
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!currentName) {
const validationMessage = 'current name may not be empty.';
@@ -123,7 +121,7 @@ export default class BranchRename extends Command {
Log.withTick(
`️Renamed branch from ${currentName} to ${chalk.bold(
editedBranch.name
)} on project ${chalk.bold(`@${accountName}/${exp.slug}`)}.`
)} on project ${chalk.bold(fullName)}.`
);
}
}
+18 -11
View File
@@ -16,10 +16,13 @@ import {
ViewBranchQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { UPDATE_COLUMNS, formatUpdate } from './list';
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
const PAGE_LIMIT = 10_000;
@@ -118,12 +121,8 @@ export default class BranchView extends Command {
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = await getProjectAccountNameAsync(exp);
const { slug } = exp;
const projectId = await ensureProjectExistsAsync({
accountName,
projectName: slug,
});
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!name) {
const validationMessage = 'Branch name may not be empty.';
@@ -158,12 +157,20 @@ export default class BranchView extends Command {
});
for (const update of updates) {
groupTable.push([formatUpdate(update), update.runtimeVersion, update.group]);
groupTable.push([
formatUpdate(update),
update.runtimeVersion,
update.group,
getPlatformsForGroup({
updates: UpdateBranch.updates,
group: update.group,
}),
]);
}
Log.withTick(
`️Branch: ${chalk.bold(UpdateBranch.name)} on project ${chalk.bold(
`@${accountName}/${slug}`
fullName
)}. Branch ID: ${chalk.bold(UpdateBranch.id)}`
);
Log.log(chalk.bold('Recent update groups published on this branch:'));
+12 -3
View File
@@ -47,7 +47,15 @@ function formatUnfinishedBuild(
): string {
const platform = appPlatformEmojis[build.platform];
const startTime = new Date(build.createdAt).toLocaleString();
const status = chalk.blue(build.status === BuildStatus.InQueue ? 'in-queue' : 'in-progress');
let statusText: string;
if (build.status === BuildStatus.New) {
statusText = 'new';
} else if (build.status === BuildStatus.InQueue) {
statusText = 'in queue';
} else {
statusText = 'in progress';
}
const status = chalk.blue(statusText);
return `${platform} Started at: ${startTime}, Status: ${status}, Id: ${build.id}`;
}
@@ -58,12 +66,13 @@ async function selectBuildToCancelAsync(
const spinner = ora().start('Fetching the uncompleted builds…');
let builds;
try {
const [inQueueBuilds, inProgressBuilds] = await Promise.all([
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 }),
]);
spinner.stop();
builds = [...inQueueBuilds, ...inProgressBuilds];
builds = [...newBuilds, ...inQueueBuilds, ...inProgressBuilds];
} catch (error) {
spinner.fail(
`Something went wrong and we couldn't fetch the builds for the project ${projectFullName}.`
@@ -17,10 +17,6 @@ export default class BuildConfigure extends Command {
char: 'p',
options: ['android', 'ios', 'all'],
}),
'allow-experimental': flags.boolean({
description: 'Enable experimental configuration steps.',
default: false,
}),
};
async run() {
@@ -32,20 +28,10 @@ export default class BuildConfigure extends Command {
const platform =
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
const allowExperimental = flags['allow-experimental'];
if (allowExperimental) {
Log.warn(
`Project configuration will execute some additional steps that might fail if structure of your native project is significantly different from ${chalk.bold(
'expo eject'
)} or ${chalk.bold('expo init')}`
);
}
await ensureLoggedInAsync();
await configureAsync({
platform,
allowExperimental,
projectDir: (await findProjectRootAsync()) ?? process.cwd(),
});
+53 -125
View File
@@ -1,44 +1,31 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { Command, flags } from '@oclif/command';
import { flags } from '@oclif/command';
import chalk from 'chalk';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import path from 'path';
import {
flushAsync as flushAnalyticsAsync,
initAsync as initAnalyticsAsync,
} from '../../analytics';
import { configureAsync } from '../../build/configure';
import { createCommandContextAsync } from '../../build/context';
import { buildAsync } from '../../build/create';
import { RequestedPlatform } from '../../build/types';
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
import { isGitStatusCleanAsync } from '../../build/utils/repository';
import { AppPlatform } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import EasCommand from '../../commandUtils/EasCommand';
import Log, { learnMore } from '../../log';
import {
isEasEnabledForProjectAsync,
warnEasUnavailable,
} from '../../project/isEasEnabledForProject';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { confirmAsync, promptAsync } from '../../prompts';
import { Actor } from '../../user/User';
import { ensureLoggedInAsync } from '../../user/actions';
import vcs from '../../vcs';
export default class Build extends Command {
export default class Build extends EasCommand {
static description = 'Start a build';
static flags = {
platform: flags.enum({ char: 'p', options: ['android', 'ios', 'all'] }),
'skip-credentials-check': flags.boolean({
default: false,
description: 'Skip validation of build credentials',
hidden: true,
}),
'skip-project-configuration': flags.boolean({
default: false,
@@ -61,109 +48,61 @@ export default class Build extends Command {
allowNo: true,
description: 'Wait for build(s) to complete',
}),
'clear-cache': flags.boolean({
default: false,
description: 'Clear cache before the build',
}),
};
async run(): Promise<void> {
const { flags } = this.parse(Build);
await initAnalyticsAsync(); // TODO: run in oclif hook for every command
try {
const user = await ensureLoggedInAsync();
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 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;
}
const accountName = await getProjectAccountNameAsync(exp);
const accountHasPendingBuilds = await ensureNoPendingBuildsExistAsync({
accountName,
platform,
projectId,
user,
});
if (accountHasPendingBuilds) {
return;
}
exp = (await ensureProjectConfiguredAsync(projectDir)) ?? exp;
const commandCtx = await createCommandContextAsync({
requestedPlatform: platform,
profile: flags.profile,
exp,
projectDir,
projectId,
nonInteractive,
skipCredentialsCheck: flags['skip-credentials-check'],
local: flags.local,
skipProjectConfiguration: flags['skip-project-configuration'],
waitForBuildEnd: flags.wait,
});
await buildAsync(commandCtx);
} finally {
await flushAnalyticsAsync(); // TODO: run in oclif hook for every command
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());
async function ensureNoPendingBuildsExistAsync({
user,
platform,
accountName,
projectId,
}: {
user: Actor;
platform: RequestedPlatform;
accountName: string;
projectId: string;
}): Promise<boolean> {
// allow expo admins to run as many builds as they wish
if (user.isExpoAdmin) {
return false;
}
const appPlatforms = toAppPlatforms(platform);
const maybePendingBuilds = await Promise.all(
appPlatforms.map(appPlatform => BuildQuery.getPendingBuildIdAsync(accountName, appPlatform))
);
const pendingBuilds = maybePendingBuilds.filter(i => i !== null);
if (pendingBuilds.length > 0) {
Log.newLine();
Log.error(
'Your other builds are still pending. Wait for them to complete before running this command again.'
);
Log.newLine();
const results = await Promise.all(
pendingBuilds.map(pendingBuild => {
return BuildQuery.byIdAsync(nullthrows(pendingBuild).id);
})
);
for (const result of results) {
Log.log(formatGraphQLBuild(result));
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.'
);
Log.newLine();
}
process.exitCode = 1;
return true;
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,
skipProjectConfiguration: flags['skip-project-configuration'],
waitForBuildEnd: flags.wait,
});
await buildAsync(commandCtx);
}
return false;
}
function verifyOptionsForLocalBuilds(platform: RequestedPlatform): boolean {
@@ -178,16 +117,6 @@ function verifyOptionsForLocalBuilds(platform: RequestedPlatform): boolean {
}
}
function toAppPlatforms(requestedPlatform: RequestedPlatform): AppPlatform[] {
if (requestedPlatform === RequestedPlatform.All) {
return [AppPlatform.Android, AppPlatform.Ios];
} else if (requestedPlatform === RequestedPlatform.Android) {
return [AppPlatform.Android];
} else {
return [AppPlatform.Ios];
}
}
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
const { platform } = await promptAsync({
type: 'select',
@@ -222,11 +151,10 @@ async function ensureProjectConfiguredAsync(projectDir: string): Promise<ExpoCon
await configureAsync({
projectDir,
platform: RequestedPlatform.All,
allowExperimental: false,
});
if (!(await isGitStatusCleanAsync())) {
if (await vcs.hasUncommittedChangesAsync()) {
throw new Error(
'Build process requires clean git working tree, please commit all your changes and run `eas build` again'
'Build process requires clean working tree, please commit all your changes and run `eas build` again'
);
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -23,6 +23,7 @@ export default class BuildList extends Command {
}),
status: flags.enum({
options: [
BuildStatus.NEW,
BuildStatus.IN_QUEUE,
BuildStatus.IN_PROGRESS,
BuildStatus.ERRORED,
@@ -92,6 +93,8 @@ const toAppPlatform = (requestedPlatform?: RequestedPlatform): AppPlatform | und
const toGraphQLBuildStatus = (buildStatus: BuildStatus): GraphQLBuildStatus | undefined => {
if (!buildStatus) {
return undefined;
} else if (buildStatus === BuildStatus.NEW) {
return GraphQLBuildStatus.New;
} else if (buildStatus === BuildStatus.IN_QUEUE) {
return GraphQLBuildStatus.InQueue;
} else if (buildStatus === BuildStatus.IN_PROGRESS) {
@@ -9,11 +9,11 @@ import {
CreateUpdateChannelOnAppMutationVariables,
} from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import {
findProjectRootAsync,
getBranchByNameAsync,
getProjectAccountNameAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { createUpdateBranchOnAppAsync } from '../branch/create';
@@ -87,12 +87,8 @@ export default class ChannelCreate extends Command {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = await getProjectAccountNameAsync(exp);
const { slug } = exp;
const projectId = await ensureProjectExistsAsync({
accountName,
projectName: slug,
});
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!channelName) {
const validationMessage = 'Channel name may not be empty.';
@@ -146,7 +142,7 @@ export default class ChannelCreate extends Command {
Log.withTick(
`️Created a new channel ${chalk.bold(newChannel.name)} on project ${chalk.bold(
`@${accountName}/${slug}`
fullName
)}. ${branchMessage} and have pointed the channel at it. You can now update your app by publishing!`
);
}
@@ -13,11 +13,10 @@ import {
UpdateChannelBranchMappingMutationVariables,
} from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import {
findProjectRootAsync,
getBranchByNameAsync,
getProjectAccountNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
@@ -126,12 +125,7 @@ export default class ChannelEdit extends Command {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = await getProjectAccountNameAsync(exp);
const { slug } = exp;
const projectId = await ensureProjectExistsAsync({
accountName,
projectName: slug,
});
const projectId = await getProjectIdAsync(exp);
if (!channelName) {
const validationMessage = 'A channel name is required to edit a specific channel.';
@@ -9,8 +9,7 @@ import {
GetAllChannelsForAppQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { logChannelDetails } from './view';
const CHANNEL_LIMIT = 10_000;
@@ -35,12 +34,13 @@ async function getAllUpdateChannelForAppAsync({
updateBranches(offset: 0, limit: $limit) {
id
name
updates(offset: 0, limit: 1) {
updates(offset: 0, limit: 10) {
id
group
message
runtimeVersion
createdAt
platform
actor {
id
... on User {
@@ -84,12 +84,7 @@ export default class ChannelList extends Command {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = await getProjectAccountNameAsync(exp);
const { slug } = exp;
const projectId = await ensureProjectExistsAsync({
accountName,
projectName: slug,
});
const projectId = await getProjectIdAsync(exp);
const getAllUpdateChannelForAppResult = await getAllUpdateChannelForAppAsync({
appId: projectId,
@@ -4,11 +4,11 @@ import chalk from 'chalk';
import { GetChannelByNameForAppQuery, UpdateBranch } from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import {
findProjectRootAsync,
getBranchByNameAsync,
getProjectAccountNameAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync, selectAsync } from '../../prompts';
import { updateChannelBranchMappingAsync } from './edit';
@@ -349,12 +349,8 @@ export default class ChannelRollout extends Command {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = await getProjectAccountNameAsync(exp);
const { slug } = exp;
const projectId = await ensureProjectExistsAsync({
accountName,
projectName: slug,
});
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
const getUpdateChannelByNameForAppResult = await getUpdateChannelByNameForAppAsync({
appId: projectId,
@@ -399,7 +395,7 @@ export default class ChannelRollout extends Command {
percent,
jsonFlag,
projectId,
fullName: `@${accountName}/${slug}`,
fullName,
currentBranchMapping,
getUpdateChannelByNameForAppResult,
});
+26 -13
View File
@@ -1,8 +1,8 @@
import { getConfig } from '@expo/config';
import { Command, flags } from '@oclif/command';
import assert from 'assert';
import chalk from 'chalk';
import Table from 'cli-table3';
import { assert } from 'console';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
@@ -11,10 +11,18 @@ import {
GetChannelByNameForAppQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { FormatUpdateParameter, UPDATE_COLUMNS, formatUpdate } from '../branch/list';
import {
FormatUpdateParameter,
UPDATE_COLUMNS,
formatUpdate,
getPlatformsForGroup,
} from '../../update/utils';
export type BranchMapping = {
version: number;
@@ -102,12 +110,13 @@ export async function getUpdateChannelByNameForAppAsync({
updateBranches(offset: 0, limit: 1000) {
id
name
updates(offset: 0, limit: 1) {
updates(offset: 0, limit: 10) {
id
group
message
runtimeVersion
createdAt
platform
actor {
id
... on User {
@@ -133,7 +142,11 @@ export async function getUpdateChannelByNameForAppAsync({
export function logChannelDetails(channel: {
branchMapping: string;
updateBranches: {
updates: (FormatUpdateParameter & { runtimeVersion?: string; group?: string })[];
updates: (FormatUpdateParameter & {
runtimeVersion: string;
group: string;
platform: string;
})[];
name: string;
id: string;
}[];
@@ -168,6 +181,10 @@ export function logChannelDetails(channel: {
formatUpdate(update),
update?.runtimeVersion ?? 'N/A',
update?.group ?? 'N/A,',
getPlatformsForGroup({
updates: branch.updates,
group: branch.updates[0]?.group,
}),
]);
}
Log.log(table.toString());
@@ -203,12 +220,8 @@ export default class ChannelView extends Command {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = await getProjectAccountNameAsync(exp);
const { slug } = exp;
const projectId = await ensureProjectExistsAsync({
accountName,
projectName: slug,
});
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!channelName) {
const validationMessage = 'A channel name is required to view a specific channel.';
@@ -238,7 +251,7 @@ export default class ChannelView extends Command {
}
Log.withTick(
chalk`Channel: {bold ${channel.name}} on project {bold ${accountName}/${slug}}. Channel ID: {bold ${channel.id}}`
chalk`Channel: {bold ${channel.name}} on project {bold ${fullName}}. Channel ID: {bold ${channel.id}}`
);
Log.log(
chalk`{bold Branches, pointed at by this channel, and their most recent update group:}`
+1 -2
View File
@@ -1,6 +1,5 @@
import { Command } from '@oclif/command';
import { CredentialsManager } from '../credentials/CredentialsManager';
import { createCredentialsContextAsync } from '../credentials/context';
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
@@ -9,6 +8,6 @@ export default class Credentials extends Command {
async run() {
const ctx = await createCredentialsContextAsync(process.cwd(), {});
new CredentialsManager(ctx).runActionAsync(new SelectPlatform());
await new SelectPlatform().runAsync(ctx);
}
}
+28
View File
@@ -0,0 +1,28 @@
import { getConfig } from '@expo/config';
import { Command } from '@oclif/command';
import chalk from 'chalk';
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';
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 });
if (exp.extra?.eas?.projectId) {
Log.error(
`app.json is already linked to project with ID: ${chalk.bold(exp.extra?.eas?.projectId)}`
);
return;
}
await setProjectIdAsync(projectDir);
}
}
@@ -4,12 +4,15 @@ import chalk from 'chalk';
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import {
isEasEnabledForProjectAsync,
warnEasUnavailable,
} from '../../project/isEasEnabledForProject';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { findAccountByName } from '../../user/Account';
import { getActorDisplayName } from '../../user/User';
@@ -47,10 +50,7 @@ export default class EnvironmentSecretCreate extends Command {
const accountName = await getProjectAccountNameAsync(exp);
const { slug } = exp;
const projectId = await ensureProjectExistsAsync({
accountName,
projectName: slug,
});
const projectId = await getProjectIdAsync(exp);
if (!(await isEasEnabledForProjectAsync(projectId))) {
warnEasUnavailable();
@@ -20,7 +20,6 @@ import { ensureLoggedInAsync } from '../../user/actions';
export default class EnvironmentSecretList extends Command {
static description = 'Lists environment secrets available for your current app';
static usage = 'secrets:list';
async run(): Promise<void> {
await ensureLoggedInAsync();
+3 -5
View File
@@ -1,6 +1,7 @@
import { getConfig } from '@expo/config';
import { Command, flags } from '@oclif/command';
import { flags } from '@oclif/command';
import EasCommand from '../commandUtils/EasCommand';
import { AppPlatform } from '../graphql/generated';
import { learnMore } from '../log';
import { isEasEnabledForProjectAsync, warnEasUnavailable } from '../project/isEasEnabledForProject';
@@ -9,13 +10,12 @@ import { promptAsync } from '../prompts';
import AndroidSubmitCommand from '../submissions/android/AndroidSubmitCommand';
import IosSubmitCommand from '../submissions/ios/IosSubmitCommand';
import { AndroidSubmitCommandFlags, IosSubmitCommandFlags } from '../submissions/types';
import { ensureLoggedInAsync } from '../user/actions';
const COMMON_FLAGS = '';
const ANDROID_FLAGS = 'Android specific options';
const IOS_FLAGS = 'iOS specific options';
export default class BuildSubmit extends Command {
export default class BuildSubmit extends EasCommand {
static description = 'Submits build artifact to app store';
static usage = 'submit --platform=(android|ios)';
static aliases = ['build:submit'];
@@ -160,8 +160,6 @@ export default class BuildSubmit extends Command {
},
} = this.parse(BuildSubmit);
await ensureLoggedInAsync();
const platform =
(flags.platform?.toUpperCase() as AppPlatform | undefined) ??
(await promptForPlatformAsync());
+6 -5
View File
@@ -5,7 +5,7 @@ import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { UpdatesByGroupQuery, UpdatesByGroupQueryVariables } from '../../graphql/generated';
import Log from '../../log';
import { UPDATE_COLUMNS, formatUpdate } from '../branch/list';
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
export async function viewUpdateAsync({
groupId,
@@ -80,18 +80,19 @@ export default class UpdateView extends Command {
}
const groupTable = new Table({
head: [...UPDATE_COLUMNS, 'platforms'],
head: [...UPDATE_COLUMNS],
wordWrap: true,
});
// The relevant info of the updates are same except for their platform
const platforms = updatesByGroup.map(update => update.platform);
const representativeUpdate = updatesByGroup[0];
groupTable.push([
formatUpdate(representativeUpdate),
representativeUpdate.runtimeVersion,
representativeUpdate.group,
platforms.join(','),
getPlatformsForGroup({
updates: updatesByGroup,
group: updatesByGroup[0]?.group,
}),
]);
Log.log(groupTable.toString());
@@ -1,13 +0,0 @@
import { Context } from './context';
export interface Action<T = void> {
runAsync(manager: CredentialsManager, ctx: Context): Promise<T>;
}
export class CredentialsManager {
constructor(private ctx: Context) {}
public async runActionAsync<T>(action: Action<T>): Promise<T> {
return await action.runAsync(this, this.ctx);
}
}
@@ -1,8 +0,0 @@
import { Platform } from '@expo/eas-build-job';
export interface CredentialsProvider {
platform: Platform;
hasRemoteAsync(): Promise<boolean>;
hasLocalAsync(): Promise<boolean>;
isLocalSyncedAsync(): Promise<boolean>;
}
@@ -1,7 +0,0 @@
import { getAndroidApiMock } from '../__tests__/fixtures-android';
import { createCtxMock } from '../__tests__/fixtures-context';
export const createCredentialsContextAsync = () =>
createCtxMock({
android: getAndroidApiMock(),
});
@@ -1,70 +1,134 @@
import { AndroidCredentials } from '../android/credentials';
import { testKeystore2Base64, testKeystoreBase64 } from './fixtures-base64-data';
import { testExperienceName, testJester2ExperienceName } from './fixtures-constants';
import {
AndroidAppBuildCredentialsFragment,
AndroidFcmFragment,
AndroidFcmVersion,
AndroidKeystoreFragment,
AndroidKeystoreType,
AppFragment,
CommonAndroidAppCredentialsFragment,
} from '../../graphql/generated';
import { Keystore } from '../android/credentials';
import {
testKeystore2Base64,
testKeystoreBase64,
testPKCS12KeystoreBase64,
testPKCS12KeystoreEmptyPasswordBase64,
} from './fixtures-base64-data';
export const testKeystore = {
export const testKeystore: Keystore = {
keystore: testKeystoreBase64,
keystorePassword: 'ae6777e9444a436dbe533d2be46c83ba',
keyAlias: 'QHdrb3p5cmEvY3JlZGVudGlhbHMtdGVzdA==',
keyPassword: '43f760fe7ecd4e6a925779eb45bc787b',
};
export const testKeystore2 = {
export const testKeystore2: Keystore = {
keystore: testKeystore2Base64,
keystorePassword: '6faeed2326b94effadbeb731510c2378',
keyAlias: 'QHdrb3p5cmEvY3JlZGVudGlhbHMtdGVzdA==',
keyPassword: 'e4829b38057042d78f25053f390478f9',
};
export const testPushCredentials = {
fcmApiKey: 'examplefcmapikey',
// Generated by: keytool -genkey -alias test-alias -keystore keystore.p12 -storetype PKCS12 -keyalg RSA -storepass password -validity 730 -keysize 2048
// A Keystore in a PKCS#12 file, where all entries are under aliases
// This particular PKCS#12 has one key and one certificate under 'test-alias'
export const testPKCS12Keystore: Keystore = {
keystore: testPKCS12KeystoreBase64,
keystorePassword: 'password',
keyAlias: 'test-alias',
};
export const testAppCredentials = {
experienceName: testExperienceName,
keystore: testKeystore,
pushCredentials: testPushCredentials,
// openssl req -new -newkey rsa:4096 -nodes -keyout test.key -out test.csr
// openssl x509 -req -sha256 -days 365 -in test.csr -signkey test.key -out test.pem
// openssl pkcs12 -export -in test.pem -inkey test.key -name "test-alias" -passout pass: -out emptyPassword.p12
// cat emptyPassword.p12 | base64
export const testPKCS12EmptyPasswordKeystore: Keystore = {
keystore: testPKCS12KeystoreEmptyPasswordBase64,
keystorePassword: '',
keyAlias: 'test-alias',
};
export const testJester2AppCredentials = {
experienceName: testJester2ExperienceName,
keystore: testKeystore2,
pushCredentials: testPushCredentials,
export const testAppFragment: AppFragment = {
id: 'test-app-id',
fullName: '@testuser/testapp',
slug: 'testapp',
};
export const testAllCredentials: { [key: string]: AndroidCredentials } = {
[testExperienceName]: testAppCredentials,
export const testLegacyAndroidFcmFragment: AndroidFcmFragment = {
id: 'test-id',
snippet: {
firstFourCharacters: 'abcd',
lastFourCharacters: 'efgh',
},
credential: 'abcdxxxxxxefgh',
version: AndroidFcmVersion.Legacy,
createdAt: new Date(),
updatedAt: new Date(),
};
export function getApiClientWrapperMock() {
// by default all method throw exceptions to make sure that we only call what is expected
const getUnexpectedCallMock = () =>
jest.fn(() => {
throw new Error('unexpected call');
});
export const testJksAndroidKeystoreFragment: AndroidKeystoreFragment = {
id: 'test-id',
type: AndroidKeystoreType.Jks,
keystore: testKeystoreBase64,
keystorePassword: testKeystore.keystorePassword,
keyAlias: testKeystore.keyAlias,
keyPassword: testKeystore.keyPassword,
md5CertificateFingerprint: 'test-md5',
sha1CertificateFingerprint: 'test-sha1',
sha256CertificateFingerprint: 'test-sha256',
createdAt: new Date(),
updatedAt: new Date(),
};
export const testLegacyAndroidBuildCredentialsFragment: AndroidAppBuildCredentialsFragment = {
id: 'test-id',
isDefault: true,
isLegacy: true,
name: 'legacy',
androidKeystore: testJksAndroidKeystoreFragment,
};
export const testLegacyAndroidAppCredentialsFragment: CommonAndroidAppCredentialsFragment = {
id: 'test-id',
applicationIdentifier: null,
isLegacy: true,
app: testAppFragment,
androidFcm: testLegacyAndroidFcmFragment,
androidAppBuildCredentialsList: [testLegacyAndroidBuildCredentialsFragment],
};
export const testAndroidBuildCredentialsFragment: AndroidAppBuildCredentialsFragment = {
id: 'test-id',
isDefault: true,
isLegacy: false,
name: 'Google App Store Build Credentials',
androidKeystore: testJksAndroidKeystoreFragment,
};
export const testAndroidAppCredentialsFragment: CommonAndroidAppCredentialsFragment = {
id: 'test-id',
applicationIdentifier: null,
isLegacy: false,
app: testAppFragment,
androidFcm: testLegacyAndroidFcmFragment,
androidAppBuildCredentialsList: [testLegacyAndroidBuildCredentialsFragment],
};
export function getNewAndroidApiMock() {
return {
getAllCredentialsApi: getUnexpectedCallMock(),
getAllCredentialsForAppApi: getUnexpectedCallMock(),
updateKeystoreApi: getUnexpectedCallMock(),
updateFcmKeyApi: getUnexpectedCallMock(),
removeKeystoreApi: getUnexpectedCallMock(),
removeFcmKeyApi: getUnexpectedCallMock(),
};
}
export function getAndroidApiMock() {
return {
fetchAllAsync: jest.fn(() => testAllCredentials),
fetchKeystoreAsync: jest.fn(() => testKeystore),
updateKeystoreAsync: jest.fn(),
removeKeystoreAsync: jest.fn(),
};
}
export function getAndroidApiMockWithoutCredentials() {
return {
fetchAllAsync: jest.fn(() => []),
fetchKeystoreAsync: jest.fn(() => null),
updateKeystoreAsync: jest.fn(),
removeKeystoreAsync: jest.fn(),
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(),
getAndroidAppBuildCredentialsListAsync: jest.fn(() => []),
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(),
getLegacyAndroidAppBuildCredentialsAsync: jest.fn(),
createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync: jest.fn(),
updateAndroidAppCredentialsAsync: jest.fn(),
updateAndroidAppBuildCredentialsAsync: jest.fn(),
createAndroidAppBuildCredentialsAsync: jest.fn(),
getDefaultAndroidAppBuildCredentialsAsync: jest.fn(),
getAndroidAppBuildCredentialsByNameAsync: jest.fn(),
createOrUpdateAndroidAppBuildCredentialsByNameAsync: jest.fn(),
createKeystoreAsync: jest.fn(),
createFcmAsync: jest.fn(),
deleteKeystoreAsync: jest.fn(),
deleteFcmAsync: jest.fn(),
};
}
@@ -4,4 +4,15 @@ export const testKeystoreBase64 =
export const testKeystore2Base64 =
'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';
export const testPKCS12KeystoreBase64 =
'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';
export const testPKCS12KeystoreEmptyPasswordBase64 =
'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 Line truncated
export const testProvisioningProfileBase64 = `MIIdDQYJKoZIhvcNAQcCoIIc/jCCHPoCAQExCzAJBgUrDgMCGgUAMIINoAYJKoZIhvcNAQcBoIINkQSCDY08P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCI/Pgo8IURPQ1RZUEUgcGxpc3QgUFVCTElDICItLy9BcHBsZS8vRFREIFBMSVNUIDEuMC8vRU4iICJodHRwOi8vd3d3LmFwcGxlLmNvbS9EVERzL1Byb3BlcnR5TGlzdC0xLjAuZHRkIj4KPHBsaXN0IHZlcnNpb249IjEuMCI+CjxkaWN0PgoJPGtleT5BcHBJRE5hbWU8L2tleT4KCTxzdHJpbmc+dGVzdGFwcCA8L3N0cmluZz4KCTxrZXk+QXBwbGljYXRpb25JZGVudGlmaWVyUHJlZml4PC9rZXk+Cgk8YXJyYXk+Cgk8c3RyaW5nPlFMNzZYWUg3M1A8L3N0cmluZz4KCTwvYXJyYXk+Cgk8a2V5PkNyZWF0aW9uRGF0ZTwva2V5PgoJPGRhdGU+MjAyMC0wNy0xM1QxMjozNzowM1o8L2RhdGU+Cgk8a2V5PlBsYXRmb3JtPC9rZXk+Cgk8YXJyYXk+CgkJPHN0cmluZz5pT1M8L3N0cmluZz4KCTwvYXJyYXk+Cgk8a2V5PklzWGNvZGVNYW5hZ2VkPC9rZXk+Cgk8ZmFsc2UvPgoJPGtleT5EZXZlbG9wZXJDZXJ0aWZpY2F0ZXM8L2tleT4KCTxhcnJheT4KCQk8ZGF0YT5NSUlGb1RDQ0JJbWdBd0lCQWdJSU9UWW9sUmhEaTlrd0RRWUpLb1pJaHZjTkFRRUxCUUF3Z1pZeEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVN3d0tnWURWUVFMRENOQmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3pGRU1FSUdBMVVFQXd3N1FYQndiR1VnVjI5eWJHUjNhV1JsSUVSbGRtVnNiM0JsY2lCU1pXeGhkR2x2Ym5NZ1EyVnlkR2xtYVdOaGRHbHZiaUJCZFhSb2IzSnBkSGt3SGhjTk1qQXdOekV6TVRJeU5qRTJXaGNOTWpFd056RXpNVEl5TmpFMldqQ0JsREVhTUJnR0NnbVNKb21UOGl4a0FRRU1DbEZNTnpaWVdVZzNNMUF4T2pBNEJnTlZCQU1NTVdsUWFHOXVaU0JFYVhOMGNtbGlkWFJwYjI0NklFRnNhV05xWVNCWFlYSmphR0hGZ2lBb1VVdzNObGhaU0RjelVDa3hFekFSQmdOVkJBc01DbEZNTnpaWVdVZzNNMUF4R0RBV0JnTlZCQW9NRDBGc2FXTnFZU0JYWVhKamFHSEZnakVMTUFrR0ExVUVCaE1DVlZNd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUNuTUNPT2VqWjEvUjBBTTJ3Sis0cjBDc2F3N0x3N2xFenBTREQ5OUd4TjA2ZkZ3cW1HRnBWdFhsRzhWYUN0cW1sZzl5MzJiMVltd1pJcXJRR3FteVY1cUk4RDJvTzh2QjVvRDJVT3IrUHgzOTQ0YnQyZFFZaUlHQTNlZi81SktZa1E3VDhXWXFQczg3Q1NORnZ5MHhJa1dHbFBpa0dhd0xXcmtzZmJUNFdkMU90dVhiYUtoTEZGNmp5alU0ZCtYN2NoNGtQOFFvdHlkWk5vWmltQTVTYWNtVmRidUM0YTlvQStPL3dscUliaXdiTUtGQlE2Q29tRTFKelpQaWtXMDFoZ1V2T2kvSEtKb0Nsdnp5SmZMLzhZbm43dDRSdUs5bmxHaHNIanJ5b3hFRDdiTGZpcUlKRTllcWlvNTBTK2cycFllWS9tNUh6SW04V1lOZ0RXcjFsTkFnTUJBQUdqZ2dIeE1JSUI3VEFNQmdOVkhSTUJBZjhFQWpBQU1COEdBMVVkSXdRWU1CYUFGSWduRndtcHRoaGdpK3pydXZaSFdjVlNWS08zTUQ4R0NDc0dBUVVGQndFQkJETXdNVEF2QmdnckJnRUZCUWN3QVlZamFIUjBjRG92TDI5amMzQXVZWEJ3YkdVdVkyOXRMMjlqYzNBd015MTNkMlJ5TVRFd2dnRWRCZ05WSFNBRWdnRVVNSUlCRURDQ0FRd0dDU3FHU0liM1kyUUZBVENCL2pDQnd3WUlLd1lCQlFVSEFnSXdnYllNZ2JOU1pXeHBZVzVqWlNCdmJpQjBhR2x6SUdObGNuUnBabWxqWVhSbElHSjVJR0Z1ZVNCd1lYSjBlU0JoYzNOMWJXVnpJR0ZqWTJWd2RHRnVZMlVnYjJZZ2RHaGxJSFJvWlc0Z1lYQndiR2xqWVdKc1pTQnpkR0Z1WkdGeVpDQjBaWEp0Y3lCaGJtUWdZMjl1WkdsMGFXOXVjeUJ2WmlCMWMyVXNJR05sY25ScFptbGpZWFJsSUhCdmJHbGplU0JoYm1RZ1kyVnlkR2xtYVdOaGRHbHZiaUJ3Y21GamRHbGpaU0J6ZEdGMFpXMWxiblJ6TGpBMkJnZ3JCZ0VGQlFjQ0FSWXFhSFIwY0RvdkwzZDNkeTVoY0hCc1pTNWpiMjB2WTJWeWRHbG1hV05oZEdWaGRYUm9iM0pwZEhrdk1CWUdBMVVkSlFFQi93UU1NQW9HQ0NzR0FRVUZCd01ETUIwR0ExVWREZ1FXQkJUTGhSdGRhRVRjRmx4Q0grZnRGQ3d4U3UvZmlUQU9CZ05WSFE4QkFmOEVCQU1DQjRBd0V3WUtLb1pJaHZkalpBWUJCQUVCL3dRQ0JRQXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBSEJxL2VSeitIYTRtemZ0cHZ0SGQ4TTFNb2d0c01UaVNNVDFZK0VGbUQ5UWZYdXdCM3F1ckhQbHFiMjFvV01CWU9TaWcwRDlFZG9Ta29STXAwdU5kaUltVnMycVc0bEgvQ25zL0w1QkhqU2NQc3kwbXUrSitlQjZXZzNXTDduNm1pM3BlOCtxZ2dxak41RVU2YW1Ca04wMmU3Z0pMZnlkcHRSRlBOaklOWFh6bDkxOVNDeUo2VXl3OGFUblBrZFc5eTI1UHgrUHJBSmNDVm1hbFRlYkJvNWxnbUNKejlKUkU3Q3pQd2xmaVBRbUZRMlJVTEpONTBrMmRGSmtESk1jUHd1OFdvUmNBMFE3M21NU256OVNiS21UcHhzeUNEUUo2WHJ5Sm1vKy9aQmEvM0k0dE15dkNBUVQ1alNLWjFrUXJ4c2xFdGJHZE5Bb0xUaUJ6dHY3Wjk0PTwvZGF0YT4KCTwvYXJyYXk+CgoJCQkJCQkJCQkJCgk8a2V5PkVudGl0bGVtZW50czwva2V5PgoJPGRpY3Q+CgkJPGtleT5iZXRhLXJlcG9ydHMtYWN0aXZlPC9rZXk+CgkJPHRydWUvPgoJCQkJCgkJCQk8a2V5PmFwcGxpY2F0aW9uLWlkZW50aWZpZXI8L2tleT4KCQk8c3RyaW5nPlFMNzZYWUg3M1Aub3JnLnJlYWN0anMubmF0aXZlLmV4YW1wbGUudGVzdGFwcC50dXJ0bGV2Mjwvc3RyaW5nPgoJCQkJCgkJCQk8a2V5PmtleWNoYWluLWFjY2Vzcy1ncm91cHM8L2tleT4KCQk8YXJyYXk+CgkJCQk8c3RyaW5nPlFMNzZYWUg3M1AuKjwvc3RyaW5nPgoJCQkJPHN0cmluZz5jb20uYXBwbGUudG9rZW48L3N0cmluZz4KCQk8L2FycmF5PgoJCQkJCgkJCQk8a2V5PmdldC10YXNrLWFsbG93PC9rZXk+CgkJPGZhbHNlLz4KCQkJCQoJCQkJPGtleT5jb20uYXBwbGUuZGV2ZWxvcGVyLnRlYW0taWRlbnRpZmllcjwva2V5PgoJCTxzdHJpbmc+UUw3NlhZSDczUDwvc3RyaW5nPgoJCQkJCgkJCQk8a2V5PmFwcy1lbnZpcm9ubWVudDwva2V5PgoJCTxzdHJpbmc+cHJvZHVjdGlvbjwvc3RyaW5nPgoKCTwvZGljdD4KCTxrZXk+RXhwaXJhdGlvbkRhdGU8L2tleT4KCTxkYXRlPjIwMjEtMDctMTNUMTI6MjY6MTZaPC9kYXRlPgoJPGtleT5OYW1lPC9rZXk+Cgk8c3RyaW5nPm9yZy5yZWFjdGpzLm5hdGl2ZS5leGFtcGxlLnRlc3RhcHAudHVydGxldjIgcHJvZmlsPC9zdHJpbmc+Cgk8a2V5PlRlYW1JZGVudGlmaWVyPC9rZXk+Cgk8YXJyYXk+CgkJPHN0cmluZz5RTDc2WFlINzNQPC9zdHJpbmc+Cgk8L2FycmF5PgoJPGtleT5UZWFtTmFtZTwva2V5PgoJPHN0cmluZz5BbGljamEgV2FyY2hhxYI8L3N0cmluZz4KCTxrZXk+VGltZVRvTGl2ZTwva2V5PgoJPGludGVnZXI+MzY0PC9pbnRlZ2VyPgoJPGtleT5VVUlEPC9rZXk+Cgk8c3RyaW5nPjQ1NjBiNTU1LWFhODMtNDRiNy1iYjMxLTAwMGIwMDZjNDM2ZDwvc3RyaW5nPgoJPGtleT5WZXJzaW9uPC9rZXk+Cgk8aW50ZWdlcj4xPC9pbnRlZ2VyPgo8L2RpY3Q+CjwvcGxpc3Q+oIIMsjCCA/MwggLboAMCAQICARcwDQYJKoZIhvcNAQEFBQAwYjELMAkGA1UEBhMCVVMxEzARBgNVBAoTCkFwcGxlIEluYy4xJjAkBgNVBAsTHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDEw1BcHBsZSBSb290IENBMB4XDTA3MDQxMjE3NDMyOFoXDTIyMDQxMjE3NDMyOFoweTELMAkGA1UEBhMCVVMxE Line truncated
// Generated using instructions from https://support.magplus.com/hc/en-us/articles/203808748-iOS-Creating-a-Distribution-Certificate-and-p12-File
// When exporting from kechain password was left blank
export const testDistributionCertificateEmptyPasswordBase64 =
'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';
@@ -34,6 +34,7 @@ export const testAppJson = {
slug: testSlug,
sdkVersion: '38.0.0',
ios: { bundleIdentifier: testBundleIdentifier },
android: { package: testPackageName },
};
export const testAppJsonWithDifferentOwner = {
@@ -1,19 +1,17 @@
import merge from 'lodash/merge';
import { CredentialsManager } from '../CredentialsManager';
import { Context } from '../context';
import { getAndroidApiMockWithoutCredentials } from './fixtures-android';
import { getNewAndroidApiMock } from './fixtures-android';
import { getAppstoreMock } from './fixtures-appstore';
import { testAppJson, testUsername } from './fixtures-constants';
import { getIosApiMockWithoutCredentials } from './fixtures-ios';
import { getNewIosApiMockWithoutCredentials } from './fixtures-new-ios';
import { getNewIosApiMock } from './fixtures-ios';
export function createCtxMock(mockOverride: Record<string, any> = {}): Context {
const defaultMock = {
ios: getIosApiMockWithoutCredentials(),
newIos: getNewIosApiMockWithoutCredentials(),
android: getAndroidApiMockWithoutCredentials(),
ios: getNewIosApiMock(),
android: getNewAndroidApiMock(),
appStore: getAppstoreMock(),
bestEffortAppStoreAuthenticateAsync: jest.fn(),
ensureAppleCtx: jest.fn(),
ensureProjectContext: jest.fn(),
user: {
@@ -28,14 +26,3 @@ export function createCtxMock(mockOverride: Record<string, any> = {}): Context {
};
return merge(defaultMock, mockOverride) as any;
}
export function createManagerMock(mockOverride: Record<string, any> = {}): CredentialsManager {
return merge(
{
runActionAsync: jest.fn(() => {
throw new Error('unexpected call'); // should be implemented in test directly
}),
},
mockOverride
) as any;
}
@@ -3,46 +3,21 @@ import {
AppleAppIdentifierFragment,
AppleDistributionCertificateFragment,
AppleProvisioningProfileFragment,
ApplePushKeyFragment,
AppleTeamFragment,
CommonIosAppCredentialsFragment,
IosAppBuildCredentialsFragment,
IosDistributionType,
} from '../../graphql/generated';
import { IosAppCredentialsWithBuildCredentialsQueryResult } from '../ios/api/graphql/queries/IosAppCredentialsQuery';
import {
DistributionCertificate,
DistributionCertificateStoreInfo,
ProvisioningProfile,
ProvisioningProfileStoreInfo,
PushKey,
PushKeyStoreInfo,
} from '../ios/appstore/Credentials.types';
import { IosDistCredentials, IosPushCredentials } from '../ios/credentials';
import { DistributionCertificate, ProvisioningProfile } from '../ios/appstore/Credentials.types';
import { Target } from '../ios/types';
import { testProvisioningProfileBase64 } from './fixtures-base64-data';
import { testBundleIdentifier, testExperienceName } from './fixtures-constants';
const today = new Date();
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
export const testAppleTeam = {
id: 'test-team-id',
};
export const testProvisioningProfile: ProvisioningProfile = {
provisioningProfileId: 'test-id',
provisioningProfile: testProvisioningProfileBase64,
teamId: 'id',
};
export const testProvisioningProfiles = [testProvisioningProfile];
export const testProvisioningProfileFromApple: ProvisioningProfileStoreInfo = {
name: 'test-name',
status: 'Active',
expires: tomorrow.getTime(),
distributionMethod: 'test',
certificates: [],
provisioningProfileId: testProvisioningProfile.provisioningProfileId,
provisioningProfile: testProvisioningProfile.provisioningProfile,
teamId: 'id',
};
export const testProvisioningProfilesFromApple = [testProvisioningProfileFromApple];
export const testAppleTeamFragment: AppleTeamFragment = {
id: 'test-apple-team-id-1',
@@ -60,11 +35,21 @@ export const testAppFragment: AppFragment = {
slug: 'testapp',
};
export const testPushKey: ApplePushKeyFragment = {
id: 'test-push-key-id',
keyIdentifier: 'test-key-identifier',
appleTeam: { ...testAppleTeamFragment },
updatedAt: new Date(),
iosAppCredentialsList: [],
};
export const testTargets: Target[] = [{ targetName: 'testapp', bundleIdentifier: 'foo.bar.com' }];
export const testProvisioningProfileFragment: AppleProvisioningProfileFragment = {
id: 'test-prov-prof-id-1',
expiration: new Date(),
developerPortalIdentifier: 'test-developer-identifier',
provisioningProfile: 'test-provisioning-profile',
provisioningProfile: testProvisioningProfileBase64,
updatedAt: new Date(),
status: 'Active',
appleTeam: { ...testAppleTeamFragment },
@@ -117,127 +102,46 @@ export const testIosAppBuildCredentialsFragment: IosAppBuildCredentialsFragment
provisioningProfile: testProvisioningProfileFragment,
};
export const testIosAppCredentialsWithBuildCredentialsQueryResult: IosAppCredentialsWithBuildCredentialsQueryResult = {
id: 'test-app-credential-id',
iosAppBuildCredentialsArray: [testIosAppBuildCredentialsFragment],
export const testCommonIosAppCredentialsFragment: CommonIosAppCredentialsFragment = {
id: 'test-common-ios-app-credentials-id',
app: testAppFragment,
appleTeam: testAppleTeamFragment,
appleAppIdentifier: testAppleAppIdentifierFragment,
pushKey: testPushKey,
iosAppBuildCredentialsList: [testIosAppBuildCredentialsFragment],
};
export const testAppleTeam = {
id: 'test-team-id',
};
export function getNewIosApiMock() {
return {
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(),
createOrGetIosAppCredentialsWithCommonFieldsAsync: jest.fn(),
updateIosAppCredentialsAsync: jest.fn(),
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
createOrGetExistingAppleTeamAsync: () => testAppleTeam,
createOrGetExistingAppleAppIdentifierAsync: jest.fn(),
getDevicesForAppleTeamAsync: jest.fn(),
createProvisioningProfileAsync: jest.fn(),
getProvisioningProfileAsync: jest.fn(),
updateProvisioningProfileAsync: jest.fn(),
deleteProvisioningProfilesAsync: jest.fn(),
getDistributionCertificateForAppAsync: jest.fn(),
getDistributionCertificatesForAccountAsync: jest.fn(),
createDistributionCertificateAsync: jest.fn(),
deleteDistributionCertificateAsync: jest.fn(),
createPushKeyAsync: jest.fn(),
getPushKeyForAppAsync: jest.fn(),
deletePushKeyAsync: jest.fn(),
};
}
export const testDistCert: DistributionCertificate = {
certP12: 'Y2VydHAxMg==',
certPassword: 'test-password',
distCertSerialNumber: 'test-serial',
teamId: 'test-team-id',
};
export const testIosDistCredential: IosDistCredentials = {
id: 1,
type: 'dist-cert',
...testDistCert,
};
export const testIosDistCredentials = [testIosDistCredential];
export const testDistCertFromApple: DistributionCertificateStoreInfo = {
id: 'test-id',
name: 'test-name',
status: 'Active',
created: today.getTime(),
expires: tomorrow.getTime(),
ownerName: 'test-owner',
ownerId: 'test-id',
serialNumber: testIosDistCredential.distCertSerialNumber as string,
};
export const testDistCertsFromApple = [testDistCertFromApple];
export const testPushKey: PushKey = {
apnsKeyP8: 'test-p8',
apnsKeyId: 'test-key-id',
teamId: 'test-team-id',
};
export const testIosPushCredential: IosPushCredentials = {
id: 2,
type: 'push-key',
...testPushKey,
};
export const testIosPushCredentials = [testIosPushCredential];
export const testPushKeyFromApple: PushKeyStoreInfo = {
id: testIosPushCredential.apnsKeyId,
name: 'test-name',
};
export const testPushKeysFromApple = [testPushKeyFromApple];
export const testLegacyPushCert = {
pushId: 'test-push-id',
pushP12: 'test-push-p12',
pushPassword: 'test-push-password',
};
export const testAppCredential = {
experienceName: testExperienceName,
bundleIdentifier: testBundleIdentifier,
distCredentialsId: testIosDistCredential.id,
pushCredentialsId: testIosPushCredential.id,
credentials: {
...testProvisioningProfile,
},
};
export const testAllCredentialsForApp = {
...testAppCredential,
pushCredentials: testPushKey,
distCredentials: testDistCert,
};
export const testAppCredentials = [testAppCredential];
export const testAllCredentials = {
userCredentials: [...testIosDistCredentials, ...testIosPushCredentials],
appCredentials: testAppCredentials,
};
export function getIosApiMock() {
return {
getAllCredentials: jest.fn(() => testAllCredentials),
getDistributionCertificateAsync: jest.fn(() => testIosDistCredential),
getDistributionCertificateByIdAsync: jest.fn(id =>
id === testIosDistCredential.id ? testIosDistCredential : null
),
createDistributionCertificateAsync: jest.fn((owner, cert) => ({
id: 123,
type: 'dist-cert',
...cert,
})),
updateDistributionCertificateAsync: jest.fn(),
deleteDistributionCertificateAsync: jest.fn(),
useDistributionCertificateAsync: jest.fn(),
getPushKey: jest.fn(() => testIosPushCredential),
createPushKeyAsync: jest.fn((owner, key) => ({ id: 124, type: 'push-key', ...key })),
updatePushKeyAsync: jest.fn(),
deletePushKeyAsync: jest.fn(),
usePushKeyAsync: jest.fn(),
getAppCredentialsAsync: jest.fn(() => testAppCredentials),
getProvisioningProfileAsync: jest.fn(() => testProvisioningProfile),
updateProvisioningProfileAsync: jest.fn(),
deleteProvisioningProfileAsync: jest.fn(),
};
}
export function getIosApiMockWithoutCredentials() {
return {
getAllCredentials: jest.fn(() => ({ appCredentials: [], userCredentials: [] })),
getDistributionCertificateAsync: jest.fn(() => null),
getDistributionCertificateByIdAsync: jest.fn(() => {
throw new Error('not found');
}),
createDistributionCertificateAsync: jest.fn(),
updateDistributionCertificateAsync: jest.fn(),
deleteDistributionCertificateAsync: jest.fn(),
useDistributionCertificateAsync: jest.fn(),
getPushKeyAsync: jest.fn(() => null),
createPushKeyAsync: jest.fn(),
updatePushKeyAsync: jest.fn(),
deletePushKeyAsync: jest.fn(),
usePushKeyAsync: jest.fn(),
getAppCredentialsAsync: jest.fn(app => ({
experienceName: `${app.accountName}/${app.projectName}`,
bundleIdentifier: app.bundleIdentifier,
})),
getProvisioningProfileAsync: jest.fn(() => null),
updateProvisioningProfileAsync: jest.fn(),
deleteProvisioningProfileAsync: jest.fn(),
};
}
@@ -1,141 +0,0 @@
import {
DistributionCertificate,
DistributionCertificateStoreInfo,
ProvisioningProfile,
ProvisioningProfileStoreInfo,
PushKey,
PushKeyStoreInfo,
} from '../ios/appstore/Credentials.types';
import { IosDistCredentials, IosPushCredentials } from '../ios/credentials';
import { testProvisioningProfileBase64 } from './fixtures-base64-data';
import { testBundleIdentifier, testExperienceName } from './fixtures-constants';
const today = new Date();
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
export const testAppleTeam = {
id: 'test-team-id',
};
export const testProvisioningProfile: ProvisioningProfile = {
provisioningProfileId: 'test-id',
provisioningProfile: testProvisioningProfileBase64,
teamId: 'id',
};
export const testProvisioningProfiles = [testProvisioningProfile];
export const testProvisioningProfileFromApple: ProvisioningProfileStoreInfo = {
name: 'test-name',
status: 'Active',
expires: tomorrow.getTime(),
distributionMethod: 'test',
certificates: [],
provisioningProfileId: testProvisioningProfile.provisioningProfileId,
provisioningProfile: testProvisioningProfile.provisioningProfile,
teamId: 'id',
};
export const testProvisioningProfilesFromApple = [testProvisioningProfileFromApple];
export const testDistCert: DistributionCertificate = {
certP12: 'Y2VydHAxMg==',
certPassword: 'test-password',
distCertSerialNumber: 'test-serial',
teamId: 'test-team-id',
};
export const testIosDistCredential: IosDistCredentials = {
id: 1,
type: 'dist-cert',
...testDistCert,
};
export const testIosDistCredentials = [testIosDistCredential];
export const testDistCertFromApple: DistributionCertificateStoreInfo = {
id: 'test-id',
name: 'test-name',
status: 'Active',
created: today.getTime(),
expires: tomorrow.getTime(),
ownerName: 'test-owner',
ownerId: 'test-id',
serialNumber: testIosDistCredential.distCertSerialNumber as string,
};
export const testDistCertsFromApple = [testDistCertFromApple];
export const testPushKey: PushKey = {
apnsKeyP8: 'test-p8',
apnsKeyId: 'test-key-id',
teamId: 'test-team-id',
};
export const testIosPushCredential: IosPushCredentials = {
id: 2,
type: 'push-key',
...testPushKey,
};
export const testIosPushCredentials = [testIosPushCredential];
export const testPushKeyFromApple: PushKeyStoreInfo = {
id: testIosPushCredential.apnsKeyId,
name: 'test-name',
};
export const testPushKeysFromApple = [testPushKeyFromApple];
export const testLegacyPushCert = {
pushId: 'test-push-id',
pushP12: 'test-push-p12',
pushPassword: 'test-push-password',
};
export const testAppCredential = {
experienceName: testExperienceName,
bundleIdentifier: testBundleIdentifier,
distCredentialsId: testIosDistCredential.id,
pushCredentialsId: testIosPushCredential.id,
credentials: {
...testProvisioningProfile,
},
};
export const testAllCredentialsForApp = {
...testAppCredential,
pushCredentials: testPushKey,
distCredentials: testDistCert,
};
export const testAppCredentials = [testAppCredential];
export const testAllCredentials = {
userCredentials: [...testIosDistCredentials, ...testIosPushCredentials],
appCredentials: testAppCredentials,
};
export function getNewIosApiMockWithoutCredentials() {
return {
getAppAsync: jest.fn(),
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
createOrGetExistingAppleTeamAsync: () => testAppleTeam,
createOrGetExistingAppleAppIdentifierAsync: jest.fn(),
getDevicesForAppleTeamAsync: jest.fn(),
createProvisioningProfileAsync: jest.fn(),
getProvisioningProfileAsync: jest.fn(),
updateProvisioningProfileAsync: jest.fn(),
deleteProvisioningProfilesAsync: jest.fn(),
getDistributionCertificateForAppAsync: jest.fn(),
getDistributionCertificatesForAccountAsync: jest.fn(),
createDistributionCertificateAsync: jest.fn(),
deleteDistributionCertificateAsync: jest.fn(),
};
}
export function getNewIosApiMockWithCredentials() {
return {
getAppAsync: jest.fn(),
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
createOrGetExistingAppleTeamAsync: jest.fn(),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(),
getDevicesForAppleTeamAsync: jest.fn(),
createProvisioningProfileAsync: jest.fn(),
getProvisioningProfileAsync: jest.fn(),
updateProvisioningProfileAsync: jest.fn(),
deleteProvisioningProfilesAsync: jest.fn(),
getDistributionCertificateForAppAsync: jest.fn(),
getDistributionCertificatesForAccountAsync: jest.fn(),
createDistributionCertificateAsync: jest.fn(),
deleteDistributionCertificateAsync: jest.fn(),
};
}
@@ -1,77 +1,27 @@
import { Platform } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import nullthrows from 'nullthrows';
import Log from '../../log';
import { CredentialsManager } from '../CredentialsManager';
import { CredentialsProvider } from '../CredentialsProvider';
import { AndroidAppBuildCredentialsFragment } from '../../graphql/generated';
import { Context } from '../context';
import * as credentialsJsonReader from '../credentialsJson/read';
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
import { AppLookupParams } from './api/GraphqlClient';
import { Keystore } from './credentials';
export interface AndroidCredentials {
keystore: Keystore;
}
interface AppLookupParams {
projectName: string;
accountName: string;
}
interface Options {
app: AppLookupParams;
skipCredentialsCheck?: boolean;
}
export default class AndroidCredentialsProvider implements CredentialsProvider {
export default class AndroidCredentialsProvider {
public readonly platform = Platform.ANDROID;
constructor(private ctx: Context, private options: Options) {}
private get projectFullName(): string {
const { projectName, accountName } = this.options.app;
return `@${accountName}/${projectName}`;
}
public async hasRemoteAsync(): Promise<boolean> {
const keystore = await this.ctx.android.fetchKeystoreAsync(this.projectFullName);
return !!keystore;
}
public async hasLocalAsync(): Promise<boolean> {
if (!(await credentialsJsonReader.fileExistsAsync(this.ctx.projectDir))) {
return false;
}
try {
const rawCredentialsJson = await credentialsJsonReader.readRawAsync(this.ctx.projectDir);
return !!rawCredentialsJson?.android;
} catch (err) {
Log.error(err); // malformed json
return false;
}
}
public async isLocalSyncedAsync(): Promise<boolean> {
try {
const [remote, local] = await Promise.all([
this.ctx.android.fetchKeystoreAsync(this.projectFullName),
credentialsJsonReader.readAndroidCredentialsAsync(this.ctx.projectDir),
]);
const r = remote;
const l = local?.keystore;
return !!(
r &&
l &&
r.keystore === l.keystore &&
r.keystorePassword === l.keystorePassword &&
r.keyAlias === l.keyAlias &&
r.keyPassword === l.keyPassword
);
} catch (_) {
return false;
}
}
public async getCredentialsAsync(
src: CredentialsSource.LOCAL | CredentialsSource.REMOTE
): Promise<AndroidCredentials> {
@@ -81,18 +31,25 @@ export default class AndroidCredentialsProvider implements CredentialsProvider {
case CredentialsSource.REMOTE:
return await this.getRemoteAsync();
}
throw new Error('Unknown');
}
private async getRemoteAsync(): Promise<AndroidCredentials> {
await new CredentialsManager(this.ctx).runActionAsync(
new SetupBuildCredentials(this.projectFullName)
);
const keystore = await this.ctx.android.fetchKeystoreAsync(this.projectFullName);
if (!keystore) {
throw new Error('Unable to set up credentials, failed to fetch keystore from EAS servers');
}
return { keystore };
const setupBuildCredentialsAction = await new SetupBuildCredentials({ app: this.options.app });
const buildCredentials = await setupBuildCredentialsAction.runAsync(this.ctx);
return this.toAndroidCredentials(buildCredentials);
}
private toAndroidCredentials(
androidBuildCredentials: AndroidAppBuildCredentialsFragment
): AndroidCredentials {
return {
keystore: {
keystore: nullthrows(androidBuildCredentials.androidKeystore?.keystore),
keystorePassword: nullthrows(androidBuildCredentials.androidKeystore?.keystorePassword),
keyAlias: nullthrows(androidBuildCredentials.androidKeystore?.keyAlias),
keyPassword: androidBuildCredentials.androidKeystore?.keyPassword ?? undefined,
},
};
}
private async getLocalAsync(): Promise<AndroidCredentials> {
@@ -0,0 +1,28 @@
import {
AndroidFcmFragment,
CommonAndroidAppCredentialsFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
import { Context } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
export class AssignFcm {
constructor(private app: AppLookupParams) {}
public async runAsync(
ctx: Context,
fcm: AndroidFcmFragment
): Promise<CommonAndroidAppCredentialsFragment> {
const appCredentials = await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
this.app
);
const updatedAppCredentials = await ctx.android.updateAndroidAppCredentialsAsync(
appCredentials,
{
androidFcmId: fcm.id,
}
);
Log.succeed(`FCM API Key assigned to ${this.app.androidApplicationIdentifier}`);
return updatedAppCredentials;
}
}
@@ -0,0 +1,169 @@
import assert from 'assert';
import { nanoid } from 'nanoid';
import ora from 'ora';
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
import { getApplicationId } from '../../../project/android/applicationId';
import { getProjectAccountName, getProjectConfigDescription } from '../../../project/projectUtils';
import { promptAsync } from '../../../prompts';
import { findAccountByName } from '../../../user/Account';
import { Context } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
/**
* Legacy credentials can be copied over to EAS if the user does not have
* EAS credentials set up yet
*/
export async function canCopyLegacyCredentialsAsync(
ctx: Context,
app: AppLookupParams
): Promise<boolean> {
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(app);
if (appCredentials) {
return false; // modern credentials already exist
}
const legacyAppCredentials = await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(
app
);
return !!legacyAppCredentials; // user has some legacy credentials
}
export async function promptUserAndCopyLegacyCredentialsAsync(
ctx: Context,
app: AppLookupParams
): Promise<void> {
assert(
await canCopyLegacyCredentialsAsync(ctx, app),
'User not eligible to copy classic build credentials to EAS'
);
const spinner = ora('Classic credentials detected, copying to EAS...').start();
try {
const legacyAppCredentials = await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(
app
);
if (!legacyAppCredentials) {
return;
}
const appCredentials = await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
app
);
const legacyFcm = legacyAppCredentials.androidFcm;
if (legacyFcm) {
const clonedFcm = await ctx.android.createFcmAsync(
app.account,
legacyFcm.credential,
legacyFcm.version
);
await ctx.android.updateAndroidAppCredentialsAsync(appCredentials, {
androidFcmId: clonedFcm.id,
});
}
const legacyBuildCredentials = await ctx.android.getLegacyAndroidAppBuildCredentialsAsync(app);
const legacyKeystore = legacyBuildCredentials?.androidKeystore ?? null;
if (legacyKeystore) {
const clonedKeystore = await ctx.android.createKeystoreAsync(app.account, {
keystore: legacyKeystore.keystore,
keystorePassword: legacyKeystore.keystorePassword,
keyAlias: legacyKeystore.keyAlias,
keyPassword: legacyKeystore.keyPassword ?? undefined,
type: legacyKeystore.type,
});
await createOrUpdateDefaultAndroidAppBuildCredentialsAsync(ctx, app, {
androidKeystoreId: clonedKeystore.id,
});
}
} catch (e) {
spinner.fail(`Unable to migrate credentials to EAS.`);
throw e;
}
spinner.succeed('Credentials copied to EAS.');
}
export function getAppLookupParamsFromContext(ctx: Context): AppLookupParams {
ctx.ensureProjectContext();
const projectName = ctx.exp.slug;
const accountName = getProjectAccountName(ctx.exp, ctx.user);
const account = findAccountByName(ctx.user.accounts, accountName);
if (!account) {
throw new Error(`You do not have access to account: ${accountName}`);
}
const androidApplicationIdentifier = getApplicationId(ctx.projectDir, ctx.exp);
if (!androidApplicationIdentifier) {
throw new Error(
`android.package needs to be defined in your ${getProjectConfigDescription(
ctx.projectDir
)} file`
);
}
return { account, projectName, androidApplicationIdentifier };
}
export async function createOrUpdateDefaultAndroidAppBuildCredentialsAsync(
ctx: Context,
appLookupParams: AppLookupParams,
{
androidKeystoreId,
}: {
androidKeystoreId: string;
}
): Promise<AndroidAppBuildCredentialsFragment> {
assert(
!ctx.nonInteractive,
'createOrUpdateDefaultAndroidAppBuildCredentialsAsync must be run in interactive mode'
);
const existingDefaultBuildCredentials = await ctx.android.getDefaultAndroidAppBuildCredentialsAsync(
appLookupParams
);
if (existingDefaultBuildCredentials) {
return await ctx.android.updateAndroidAppBuildCredentialsAsync(
existingDefaultBuildCredentials,
{ androidKeystoreId }
);
}
return await ctx.android.createAndroidAppBuildCredentialsAsync(appLookupParams, {
name: generateRandomName(),
isDefault: true,
androidKeystoreId,
});
}
export async function promptForNameAsync(): Promise<string> {
const { providedName } = await promptAsync({
type: 'text',
name: 'providedName',
message: 'Assign a name to your build credentials:',
initial: generateRandomName(),
validate: (input: string) => input !== '',
});
return providedName;
}
/**
* sort a build credentials array in descending order of preference
* prefer default credentials, then prefer names that come first lexicographically
*/
export function sortBuildCredentials(
androidAppBuildCredentialsList: AndroidAppBuildCredentialsFragment[]
): AndroidAppBuildCredentialsFragment[] {
return androidAppBuildCredentialsList.sort((buildCredentialsA, buildCredentialsB) => {
if (buildCredentialsA.isDefault) {
return -1;
} else if (buildCredentialsB.isDefault) {
return 1;
}
return buildCredentialsA.name.localeCompare(buildCredentialsB.name);
});
}
function generateRandomName(): string {
return `Build Credentials ${nanoid(10)}`;
}
@@ -0,0 +1,30 @@
import { AndroidFcmFragment, AndroidFcmVersion } from '../../../graphql/generated';
import Log from '../../../log';
import { promptAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { Context } from '../../context';
export class CreateFcm {
constructor(private account: Account) {}
public async runAsync(ctx: Context): Promise<AndroidFcmFragment> {
if (ctx.nonInteractive) {
throw new Error(`FCM API Key cannot be uploaded in non-interactive mode.`);
}
const { fcmApiKey } = await promptAsync([
{
type: 'text',
name: 'fcmApiKey',
message: 'FCM API Key',
validate: (value: string) => value.length > 0 || "FCM API Key can't be empty",
},
]);
const fcmFragment = await ctx.android.createFcmAsync(
this.account,
fcmApiKey,
AndroidFcmVersion.Legacy
);
Log.succeed('Uploaded FCM API Key');
return fcmFragment;
}
}
@@ -0,0 +1,31 @@
import { AndroidKeystoreFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { Account } from '../../../user/Account';
import { Context } from '../../context';
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
import { KeystoreWithType, keystoreSchema } from '../credentials';
import { generateRandomKeystoreAsync } from '../utils/keystore';
import { getKeystoreWithType, validateKeystore } from '../utils/keystoreNew';
export class CreateKeystore {
constructor(private account: Account) {}
public async runAsync(ctx: Context): Promise<AndroidKeystoreFragment> {
if (ctx.nonInteractive) {
throw new Error(`New keystore cannot be created in non-interactive mode.`);
}
const keystore = await this.provideOrGenerateAsync();
const keystoreFragment = await ctx.android.createKeystoreAsync(this.account, keystore);
Log.succeed('Created keystore');
return keystoreFragment;
}
private async provideOrGenerateAsync(): Promise<KeystoreWithType> {
const providedKeystore = await askForUserProvidedAsync(keystoreSchema);
if (providedKeystore) {
const providedKeystoreWithType = getKeystoreWithType(providedKeystore);
validateKeystore(providedKeystoreWithType);
}
return await generateRandomKeystoreAsync();
}
}
@@ -1,66 +1,82 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { maybeRenameExistingFileAsync } from '../../../utils/files';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
import { AppLookupParams, formatProjectFullName } from '../api/GraphqlClient';
import { displayAndroidKeystore } from '../utils/printCredentials';
interface DownloadKeystoreOptions {
displayCredentials?: boolean;
app: AppLookupParams;
displaySensitiveInformation?: boolean;
outputPath?: string;
}
export class DownloadKeystore implements Action {
constructor(private projectFullName: string, private options?: DownloadKeystoreOptions) {}
export class DownloadKeystore {
constructor(private options: DownloadKeystoreOptions) {}
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
const keystore = await ctx.android.fetchKeystoreAsync(this.projectFullName);
public async runAsync(
ctx: Context,
buildCredentials: AndroidAppBuildCredentialsFragment
): Promise<void> {
const keystore = buildCredentials.androidKeystore;
if (!keystore) {
Log.warn('There is no valid Keystore defined for this app');
Log.warn(
`There is no valid Keystore defined for build credentials: ${buildCredentials.name}`
);
return;
}
let displayCredentials = this.options?.displayCredentials;
if (displayCredentials === undefined && !ctx.nonInteractive) {
displayCredentials = await confirmAsync({
message: 'Do you want to display the Android Keystore credentials?',
let displaySensitiveInformation = this.options?.displaySensitiveInformation;
if (displaySensitiveInformation === undefined && !ctx.nonInteractive) {
displaySensitiveInformation = await confirmAsync({
message: 'Do you want to display the sensitive information of the Android Keystore?',
});
}
const keystorePath =
this.options?.outputPath ?? `${this.projectFullName.replace('/', '__')}.jks`;
const projectFullName = formatProjectFullName(this.options.app);
const keystorePath = this.options?.outputPath ?? `${projectFullName.replace('/', '__')}.jks`;
await maybeRenameExistingFileAsync(ctx.projectDir, keystorePath);
Log.log(`Saving Keystore to ${keystorePath}`);
await fs.writeFile(keystorePath, keystore.keystore, 'base64');
if (displayCredentials) {
Log.log(`Keystore credentials
Keystore password: ${chalk.bold(keystore.keystorePassword)}
Key alias: ${chalk.bold(keystore.keyAlias)}
Key password: ${chalk.bold(keystore.keyPassword)}
// non-sensitive information
displayAndroidKeystore(keystore);
Path to Keystore: ${keystorePath}
if (displaySensitiveInformation) {
Log.newLine();
Log.log(`Sensitive Keystore information:
Keystore password: ${chalk.bold(keystore.keystorePassword)}
Key alias: ${chalk.bold(keystore.keyAlias)}
Key password: ${chalk.bold(keystore.keyPassword)}
Path to Keystore: ${keystorePath}
`);
}
Log.newLine();
}
}
export class BackupKeystore implements Action {
constructor(private projectFullName: string) {}
export class BackupKeystore {
constructor(private app: AppLookupParams) {}
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
if (await ctx.android.fetchKeystoreAsync(this.projectFullName)) {
Log.warn('Backing up your old Android Keystore now...');
await manager.runActionAsync(
new DownloadKeystore(this.projectFullName, {
displayCredentials: true,
outputPath: `${this.projectFullName}.bak.jks`.replace('/', '__'),
})
);
public async runAsync(
ctx: Context,
buildCredentials: AndroidAppBuildCredentialsFragment
): Promise<void> {
if (!buildCredentials.androidKeystore) {
return;
}
const projectFullName = formatProjectFullName(this.app);
Log.warn('Backing up your old Android Keystore now...');
await new DownloadKeystore({
app: this.app,
outputPath: `${projectFullName}.bak.jks`.replace('/', '__'),
displaySensitiveInformation: true,
}).runAsync(ctx, buildCredentials);
}
}
@@ -1,22 +0,0 @@
import Log from '../../../log';
import { promptAsync } from '../../../prompts';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
export class UpdateFcmKey implements Action {
constructor(private experienceName: string) {}
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
const { fcmApiKey } = await promptAsync([
{
type: 'text',
name: 'fcmApiKey',
message: 'FCM API Key',
validate: (value: string) => value.length > 0 || "FCM API Key can't be empty",
},
]);
await ctx.android.updateFcmKeyAsync(this.experienceName, fcmApiKey);
Log.succeed('Updated successfully');
}
}
@@ -0,0 +1,39 @@
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { Context } from '../../context';
import { AppLookupParams, formatProjectFullName } from '../api/GraphqlClient';
export class RemoveFcm {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: Context): Promise<void> {
if (ctx.nonInteractive) {
throw new Error(
"Deleting an FCM Api Key is a destructive operation. Start the CLI without the '--non-interactive' flag to delete the credentials."
);
}
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(
this.app
);
const fcm = appCredentials?.androidFcm;
if (!fcm) {
Log.warn(
`There is no valid FCM Api Key defined for ${formatProjectFullName(this.app)}, ${
this.app.androidApplicationIdentifier
}`
);
return;
}
const confirm = await confirmAsync({
message: 'Permanently delete the FCM Api Key from Expo servers?',
initial: false,
});
if (!confirm) {
return;
}
await ctx.android.deleteFcmAsync(fcm);
Log.succeed('FCM Api Key removed');
}
}
@@ -1,38 +1,44 @@
import chalk from 'chalk';
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
import { BackupKeystore } from './DownloadKeystore';
export class RemoveKeystore implements Action {
constructor(private projectFullName: string) {}
export class RemoveKeystore {
constructor(private app: AppLookupParams) {}
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
if (!(await ctx.android.fetchKeystoreAsync(this.projectFullName))) {
Log.warn('There is no Keystore defined for this app.');
async runAsync(
ctx: Context,
buildCredentials: AndroidAppBuildCredentialsFragment
): Promise<void> {
if (ctx.nonInteractive) {
throw new Error(
"Deleting a keystore is a destructive operation. Start the CLI without the '--non-interactive' flag to delete the credentials."
);
}
const keystore = buildCredentials.androidKeystore;
if (!keystore) {
Log.warn(
`There is no valid Keystore defined for build credentials: ${buildCredentials.name}`
);
return;
}
this.displayWarning();
if (ctx.nonInteractive) {
throw new Error(
"Deleting build credentials is a destructive operation. Start the CLI without the '--non-interactive' flag to delete the credentials."
);
}
const confirm = await confirmAsync({
message: 'Permanently delete the Android build credentials from our servers?',
message: 'Permanently delete the Android Keystore?',
initial: false,
});
if (confirm) {
await manager.runActionAsync(new BackupKeystore(this.projectFullName));
await ctx.android.removeKeystoreAsync(this.projectFullName);
Log.succeed('Keystore removed');
if (!confirm) {
return;
}
await new BackupKeystore(this.app).runAsync(ctx, buildCredentials);
await ctx.android.deleteKeystoreAsync(keystore);
Log.succeed('Keystore removed');
}
displayWarning() {
@@ -1,45 +1,136 @@
import nullthrows from 'nullthrows';
import ora from 'ora';
import {
AndroidAppBuildCredentialsFragment,
AndroidKeystoreFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
import { readAndroidCredentialsAsync } from '../../credentialsJson/read';
import { validateKeystoreAsync } from '../utils/keystore';
import { UpdateKeystore } from './UpdateKeystore';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import { AppLookupParams } from '../api/GraphqlClient';
import {
canCopyLegacyCredentialsAsync,
createOrUpdateDefaultAndroidAppBuildCredentialsAsync,
promptUserAndCopyLegacyCredentialsAsync,
} from './BuildCredentialsUtils';
import { CreateKeystore } from './CreateKeystore';
export class SetupBuildCredentials implements Action {
constructor(private projectFullName: string) {}
interface Options {
app: AppLookupParams;
name?: string;
}
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
if (await ctx.android.fetchKeystoreAsync(this.projectFullName)) {
return;
/**
* Sets up Build Credentials for Android
* @name: sets up build credentials for the specified configuration. If no name is specified, the default configuration is setup
*/
export class SetupBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: Context): Promise<AndroidAppBuildCredentialsFragment> {
const { app, name: maybeName } = this.options;
if (!ctx.nonInteractive) {
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, app);
if (canCopyLegacyCredentials) {
await promptUserAndCopyLegacyCredentialsAsync(ctx, app);
}
}
const alreadySetupBuildCredentials = await this.getFullySetupBuildCredentialsAsync({
ctx,
app,
name: maybeName,
});
if (alreadySetupBuildCredentials) {
return alreadySetupBuildCredentials;
}
if (ctx.nonInteractive) {
throw new Error('Generating a new Keystore is not supported in --non-interactive mode');
}
await manager.runActionAsync(new UpdateKeystore(this.projectFullName));
}
}
export class SetupBuildCredentialsFromCredentialsJson implements Action {
constructor(
private projectFullName: string,
private options: { skipKeystoreValidation: boolean }
) {}
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
let localCredentials;
try {
localCredentials = await readAndroidCredentialsAsync(ctx.projectDir);
} catch (error) {
Log.error(
'Reading credentials from credentials.json failed. Make sure this file is correct and all credentials are present there.'
throw new MissingCredentialsNonInteractiveError(
'Generating a new Keystore is not supported in --non-interactive mode'
);
throw error;
}
if (!this.options.skipKeystoreValidation) {
await validateKeystoreAsync(localCredentials.keystore);
const keystore = await new CreateKeystore(app.account).runAsync(ctx);
return await this.assignBuildCredentialsAsync({ ctx, app, name: maybeName, keystore });
}
async assignBuildCredentialsAsync({
ctx,
app,
name,
keystore,
}: {
ctx: Context;
app: AppLookupParams;
name?: string;
keystore: AndroidKeystoreFragment;
}): Promise<AndroidAppBuildCredentialsFragment> {
if (name) {
return await ctx.android.createOrUpdateAndroidAppBuildCredentialsByNameAsync(app, name, {
androidKeystoreId: keystore.id,
});
}
await ctx.android.updateKeystoreAsync(this.projectFullName, localCredentials.keystore);
Log.succeed('Keystore updated');
return await createOrUpdateDefaultAndroidAppBuildCredentialsAsync(ctx, app, {
androidKeystoreId: keystore.id,
});
}
async getFullySetupBuildCredentialsAsync({
ctx,
app,
name,
}: {
ctx: Context;
app: AppLookupParams;
name?: string;
}): Promise<AndroidAppBuildCredentialsFragment | null> {
if (name) {
return await this.getFullySetupBuildCredentialsByNameAsync({ ctx, app, name });
}
const defaultBuildCredentials = await ctx.android.getDefaultAndroidAppBuildCredentialsAsync(
app
);
const defaultKeystore = defaultBuildCredentials?.androidKeystore ?? null;
if (defaultKeystore) {
ora(
`Using Keystore from configuration: ${nullthrows(defaultBuildCredentials).name} (default)`
).succeed();
return defaultBuildCredentials;
}
return null;
}
async getFullySetupBuildCredentialsByNameAsync({
ctx,
app,
name,
}: {
ctx: Context;
app: AppLookupParams;
name: string;
}): Promise<AndroidAppBuildCredentialsFragment | null> {
const maybeBuildCredentials = await ctx.android.getAndroidAppBuildCredentialsByNameAsync(
app,
name
);
const keystore = maybeBuildCredentials?.androidKeystore ?? null;
if (keystore) {
const buildCredentials = nullthrows(maybeBuildCredentials);
ora(
`Using Keystore from configuration: ${buildCredentials.name}${
buildCredentials.isDefault ? ' (default)' : ''
}`
).succeed();
return buildCredentials;
}
Log.log(
`No Keystore found for configuration: ${name}${
maybeBuildCredentials?.isDefault ? ' (default)' : ''
}`
);
return null;
}
}
@@ -0,0 +1,88 @@
import {
AndroidAppBuildCredentialsFragment,
AndroidKeystoreType,
} from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { Context } from '../../context';
import { readAndroidCredentialsAsync } from '../../credentialsJson/read';
import {
SelectAndroidBuildCredentials,
SelectAndroidBuildCredentialsResultType,
} from '../../manager/SelectAndroidBuildCredentials';
import { AppLookupParams } from '../api/GraphqlClient';
import { getKeystoreWithType } from '../utils/keystoreNew';
import { BackupKeystore } from './DownloadKeystore';
export class SetupBuildCredentialsFromCredentialsJson {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: Context): Promise<AndroidAppBuildCredentialsFragment | null> {
if (ctx.nonInteractive) {
throw new Error(
'Setting up build credentials from credentials.json is only available in interactive mode'
);
}
let localCredentials;
try {
localCredentials = await readAndroidCredentialsAsync(ctx.projectDir);
} catch (error) {
Log.error(
'Reading credentials from credentials.json failed. Make sure this file is correct and all credentials are present there.'
);
throw error;
}
const selectBuildCredentialsResult = await new SelectAndroidBuildCredentials(this.app).runAsync(
ctx
);
if (
selectBuildCredentialsResult.resultType ===
SelectAndroidBuildCredentialsResultType.EXISTING_CREDENTIALS &&
selectBuildCredentialsResult.result.androidKeystore
) {
const buildCredentials = selectBuildCredentialsResult.result;
const confirmOverwrite = await confirmAsync({
message: `The build configuration ${buildCredentials.name} already has a keystore configured. Overwrite?`,
});
if (!confirmOverwrite) {
return null;
}
await new BackupKeystore(this.app).runAsync(ctx, buildCredentials);
}
const providedKeystoreWithType = getKeystoreWithType(localCredentials.keystore);
if (providedKeystoreWithType.type === AndroidKeystoreType.Unknown) {
const confirmKeystoreIsSketchy = await confirmAsync({
message: `The keystore you provided could not be parsed and may be corrupt. Proceed anyway?`,
});
if (!confirmKeystoreIsSketchy) {
return null;
}
}
const keystoreFragment = await ctx.android.createKeystoreAsync(
this.app.account,
providedKeystoreWithType
);
let buildCredentials: AndroidAppBuildCredentialsFragment;
if (
selectBuildCredentialsResult.resultType ===
SelectAndroidBuildCredentialsResultType.CREATE_REQUEST
) {
buildCredentials = await ctx.android.createAndroidAppBuildCredentialsAsync(this.app, {
...selectBuildCredentialsResult.result,
androidKeystoreId: keystoreFragment.id,
});
} else {
buildCredentials = await ctx.android.updateAndroidAppBuildCredentialsAsync(
selectBuildCredentialsResult.result,
{
androidKeystoreId: keystoreFragment.id,
}
);
}
Log.succeed('Keystore updated');
return buildCredentials;
}
}
@@ -1,14 +1,15 @@
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
import { updateAndroidCredentialsAsync } from '../../credentialsJson/update';
export class UpdateCredentialsJson implements Action {
constructor(private projectFullName: string) {}
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
export class UpdateCredentialsJson {
async runAsync(
ctx: Context,
buildCredentials: AndroidAppBuildCredentialsFragment
): Promise<void> {
Log.log('Updating Android credentials in credentials.json');
await updateAndroidCredentialsAsync(ctx);
await updateAndroidCredentialsAsync(ctx, buildCredentials);
Log.succeed(
'Android part of your local credentials.json is synced with values stored on EAS servers.'
);
@@ -1,46 +0,0 @@
import chalk from 'chalk';
import Log from '../../../log';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
import { Keystore, keystoreSchema } from '../credentials';
import { generateRandomKeystoreAsync } from '../utils/keystore';
export class UpdateKeystore implements Action {
constructor(private projectFullName: string) {}
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
if (await ctx.android.fetchKeystoreAsync(this.projectFullName)) {
this.displayWarning();
}
const keystore = await this.provideOrGenerateAsync();
await ctx.android.updateKeystoreAsync(this.projectFullName, keystore);
Log.succeed('Keystore updated');
}
private async provideOrGenerateAsync(): Promise<Keystore> {
const providedKeystore = await askForUserProvidedAsync(keystoreSchema);
if (providedKeystore) {
return providedKeystore;
}
return await generateRandomKeystoreAsync();
}
private displayWarning() {
Log.newLine();
Log.warn(
`Updating your Android build credentials will remove previous version from our servers, this is a ${chalk.bold(
'PERMANENT and IRREVERSIBLE action.'
)}`
);
Log.warn(
chalk.bold(
'Android Keystore must be identical to the one previously used to submit your app to the Google Play Store.'
)
);
Log.newLine();
}
}
@@ -0,0 +1,33 @@
import { testLegacyAndroidFcmFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { AssignFcm } from '../AssignFcm';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
describe(AssignFcm, () => {
it('assigns an fcm api key in Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: false,
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const assignFcmAction = new AssignFcm(appLookupParams);
await assignFcmAction.runAsync(ctx, testLegacyAndroidFcmFragment);
// expect app credentials to be fetched/created, then updated
expect(
ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync as any
).toHaveBeenCalledTimes(1);
expect(ctx.android.updateAndroidAppCredentialsAsync as any).toHaveBeenCalledTimes(1);
});
it('works in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const assignFcmAction = new AssignFcm(appLookupParams);
// dont fail if users are running in non-interactive mode
await expect(
assignFcmAction.runAsync(ctx, testLegacyAndroidFcmFragment)
).resolves.not.toThrowError();
});
});
@@ -0,0 +1,140 @@
import { confirmAsync, promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
testAndroidAppCredentialsFragment,
testJksAndroidKeystoreFragment,
testLegacyAndroidAppCredentialsFragment,
testLegacyAndroidBuildCredentialsFragment,
testLegacyAndroidFcmFragment,
} from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import {
canCopyLegacyCredentialsAsync,
getAppLookupParamsFromContext,
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 () => {
const ctx = createCtxMock({
nonInteractive: true,
android: {
...getNewAndroidApiMock(),
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(() => null),
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(
() => testLegacyAndroidAppCredentialsFragment
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
expect(canCopyLegacyCredentials).toBe(true);
});
it('returns false if the user has modern credentials', async () => {
const ctx = createCtxMock({
nonInteractive: true,
android: {
...getNewAndroidApiMock(),
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(
() => testAndroidAppCredentialsFragment
),
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(
() => testLegacyAndroidAppCredentialsFragment
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
expect(canCopyLegacyCredentials).toBe(false);
});
it('returns false if the user has no legacy credentials', async () => {
const ctx = createCtxMock({
nonInteractive: true,
android: {
...getNewAndroidApiMock(),
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(() => null),
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(() => null),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
expect(canCopyLegacyCredentials).toBe(false);
});
it('works in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
android: {
...getNewAndroidApiMock(),
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(() => null),
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(
() => testLegacyAndroidAppCredentialsFragment
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
expect(canCopyLegacyCredentials).toBe(true);
});
});
describe(promptUserAndCopyLegacyCredentialsAsync, () => {
it('copies all legacy credentials to EAS if the user is eligible', async () => {
(promptAsync as jest.Mock).mockImplementation(() => ({ providedName: 'test-provided-name' }));
const ctx = createCtxMock({
nonInteractive: false,
android: {
...getNewAndroidApiMock(),
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(() => null),
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(
() => testLegacyAndroidAppCredentialsFragment
),
getLegacyAndroidAppBuildCredentialsAsync: jest.fn(
() => testLegacyAndroidBuildCredentialsFragment
),
createKeystoreAsync: jest.fn(() => testJksAndroidKeystoreFragment),
createFcmAsync: jest.fn(() => testLegacyAndroidFcmFragment),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
await promptUserAndCopyLegacyCredentialsAsync(ctx, appLookupParams);
expect(
ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync as any
).toHaveBeenCalledTimes(1);
expect(ctx.android.createFcmAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.updateAndroidAppCredentialsAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createAndroidAppBuildCredentialsAsync as any).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
android: {
...getNewAndroidApiMock(),
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(() => null),
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(
() => testLegacyAndroidAppCredentialsFragment
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
await expect(
promptUserAndCopyLegacyCredentialsAsync(ctx, appLookupParams)
).rejects.toThrowError();
});
});
});
@@ -0,0 +1,27 @@
import { promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreateFcm } from '../CreateFcm';
jest.mock('../../../../prompts');
(promptAsync as jest.Mock).mockImplementation(() => ({ fcmApiKey: 'blah' }));
describe(CreateFcm, () => {
it('creates an fcm api key in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const createFcmAction = new CreateFcm(appLookupParams.account);
await createFcmAction.runAsync(ctx);
// expect fcm api key to be created on expo servers
expect(ctx.android.createFcmAsync as any).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const createFcmAction = new CreateFcm(appLookupParams.account);
// fail if users are running in non-interactive mode
await expect(createFcmAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -0,0 +1,27 @@
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreateKeystore } from '../CreateKeystore';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
describe('CreateKeystore', () => {
it('creates a keystore in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = getAppLookupParamsFromContext(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('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
// fail if users are running in non-interactive mode
await expect(createKeystoreAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -0,0 +1,45 @@
import fs from 'fs-extra';
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } 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 downloadKeystoreAction = new DownloadKeystore({ app: appLookupParams });
await downloadKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment);
expect(fsWriteFileSpy as any).toHaveBeenCalledTimes(1);
expect(fsWriteFileSpy as any).toHaveBeenCalledWith(
`@${appLookupParams.account.name}__${appLookupParams.projectName}.jks`,
testAndroidBuildCredentialsFragment.androidKeystore?.keystore,
'base64'
);
fsWriteFileSpy.mockRestore();
});
it('works in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const downloadKeystoreAction = new DownloadKeystore({ app: appLookupParams });
await expect(
downloadKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment)
).resolves.not.toThrowError();
});
});
@@ -0,0 +1,45 @@
import { confirmAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
testAndroidAppCredentialsFragment,
} from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } 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({
nonInteractive: false,
android: {
...getNewAndroidApiMock(),
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(
() => testAndroidAppCredentialsFragment
),
},
});
const appLookupParams = getAppLookupParamsFromContext(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 removeFcmApiKeyAction = new RemoveFcm(appLookupParams);
await expect(removeFcmApiKeyAction.runAsync(ctx)).rejects.toThrowError();
});
});
Loaded 100 of 273 files, more files were not shown because too many files have changed in this diff. Show more