Compare commits

...
180 Commits
Author SHA1 Message Date
Dominik Sokal 0c94f80f68 v0.26.0 2021-09-08 09:50:50 +02:00
Wojciech Kozyra 95f308737e [eas-cli] support for names with unicode only characters (#596)
* [eas-cli] support for names with unicode only characters

* update CHANGELOG
2021-09-08 09:46:12 +02:00
Dominik Sokal 2a2a0392cd [ENG-755] drop support for eas submit CLI configuration params (#590)
* drop submit flags in favor of profiles

* use platform-specific mutations

* update CHANGELOG

* update asc app id log

* address PR feedback

* use release as default submit profile only when eas.json exists

* address more PR feedback

* add link to docs

* address even more PR feedback

* fix detecting release submit profile
2021-09-07 16:14:19 +02:00
Dominik Sokal bcbdae8278 upgrade axios to 0.21.4 2021-09-07 10:05:24 +02:00
Dominik Sokal 805aee67ee add eas diagnostics + github issue templates (#594)
* add 'eas diagnostics' command

* add issue templates

* update CHANGELOG
2021-09-06 16:03:51 +02:00
Dominik Sokal 0477984d7a add CODE_OF_CONDUCT.md 2021-09-06 15:58:39 +02:00
Wojciech Kozyra 4e96196e07 remove @babel/core from package.json (#593) 2021-09-06 15:46:28 +02:00
Mike Willbanks 25ee2abdaa [eas-cli] git config --get core.ignorecase exits with code 1 (#592)
When `git config --get core.ignorecase` is run and the variable is not
currently set, it will return with an exit code of 1 thus causing the
`eas build` command to stop running. To solve this, we try/catch and
then return undefined to allow for the default behavior.
2021-09-06 14:44:06 +02:00
kgc00 75b71ae457 [eas-cli] Adds Keystore Create Tracking (#579)
- Converts credentials command to an `EASCommand`
- Adds analytics for android keystore create commands
2021-09-03 09:36:22 -07:00
Dominik Sokal 7548fc35fd update CHANGELOG 2021-09-01 10:12:33 +02:00
Dominik Sokal 5bc14b5b30 v0.25.0 2021-09-01 10:00:39 +02:00
L bee98ce25e [eas-cli] Update metro config check to support Windows (#588)
* Update metro config check to support Windows

Since Windows use backtick, the added regex address this issue.

* Update CHANGELOG.md

* Update CHANGELOG.md

* Fix eslint error
2021-09-01 09:54:29 +02:00
Dominik Sokal 0e6a5315ab clean up repo deps + use volta (#589)
* move eslint-plugin-graphql from root to eas-cli

* install @babel/core to suppress yarn install warning

* use volta

* fix lint warning in .eslintrc.js

* address PR feedback

* bump @types/node to 16.7.8
2021-08-31 17:11:52 +02:00
Wojciech KozyraandDominik Sokal 91b51528ef [eas-cli] detect case inconsistencies on macos (#570)
* [eas-cli] detect case inconsistencies on macos

* update CHANGELOG

* Apply suggestions from code review

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

* add explanatiion to undefined value

* add better error message + fyi page

Co-authored-by: Dominik Sokal <[email protected]>
2021-08-31 16:56:36 +02:00
Wojciech KozyraandDominik Sokal 5e3962afc2 [eas-cli] fix detecting googleServicesFile with EAS_NO_VCS=1 (#583)
* [eas-cli] fix detecting  services file with

* update CHANGELOG

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

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-08-31 16:30:29 +02:00
Dominik Sokal f07c52d2fb [ENG-755] add --no-wait support for eas submit (#578)
* support --no-wait in eas submit

* update CHANGELOG
2021-08-31 14:49:15 +02:00
Dominik Sokal 50846a1a96 Use ts-jest instead of babel-jest (#585)
* run tests with ts-jest

* remove babel from dependencies

* update CHANGELOG
2021-08-31 12:18:20 +02:00
Dominik Sokal 4cb726f374 bump typescript target to ES2019 (#584)
* bump typescript target to ES2019

* update CHANGELOG
2021-08-30 15:32:48 +02:00
Dominik Sokal da9d1b4984 replace @hapi/joi with joi + upgrade dependencies (#582)
* replace @hapi/joi with joi

* upgrade fs-extra to 10.0.0

* upgrade typescript to 4.4.2

* upgrade other eas-cli dependencies

* update repo deps

* fix typos

* add ': any' to every catch statement

* downgrade some deps as they require ts project to output ESM

* fix fs mock

* update snapshots

* downgrade indent-string (esm issue)

* update CHANGELOG
2021-08-30 14:07:25 +02:00
Dominik Sokal 64ad52dc45 drop support for node < 12 (#581)
* drop support for node < 12

* run CI workflows on node 16

* update CHANGELOG
2021-08-30 09:57:59 +02:00
Bartłomiej Klocek 6169e5ff5b Add ESLint rule to prevent relative package imports (#576) 2021-08-25 14:43:12 +02:00
Dominik Sokal ef33973f7f update CHANGELOG 2021-08-25 11:57:34 +02:00
Dominik Sokal 33a5254f2a v0.24.1 2021-08-25 11:48:08 +02:00
Dominik Sokal 0d737ecf6d update CHANGELOG 2021-08-25 11:39:46 +02:00
Dominik Sokal 951eea2396 fix EasJsonReader import 2021-08-25 11:37:04 +02:00
Dominik Sokal 75968bf449 update CHANGELOG 2021-08-25 11:25:43 +02:00
Dominik Sokal 32aa46ac69 v0.24.0 2021-08-25 11:15:02 +02:00
339cae2532 Bump apple-utils to v25 (#574)
* Bump apple-utils to v25

* Update CHANGELOG.md

* Apply suggestions from code review

Co-authored-by: Evan Bacon <[email protected]>
Co-authored-by: Dominik Sokal <[email protected]>
2021-08-25 09:48:03 +02:00
Axel DelafosseandDominik Sokal e6ec3bfe6a [eas-cli] update QR code url for Android internal distribution build and generate smaller QR code (#573)
* [eas-cli] update qr code url and generate smaller qr code

* [eas-cli] update changelog

* [eas-cli] fix prettier

* [eas-cli] update changelog

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

* [eas-cli] update changelog

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

* [eas-cli] clean code

Co-authored-by: Dominik Sokal <[email protected]>
2021-08-24 17:45:32 +02:00
Juwan Wheatley 012518c375 Update expo.io references to expo.dev (#568)
* Update expo.io references to expo.dev

* fix snapshot
2021-08-23 19:48:37 -04:00
Brent Vatne cc8c8584d9 [eas-cli] Link to project page and add archive FYI link to build command output (#562) 2021-08-20 13:58:53 -07:00
jkh d1910b3c42 [eas-cli] add ability to double publish (#566) 2021-08-20 11:16:43 -07:00
Wojciech KozyraandDominik Sokal 4a1fa3d4fa [eas-cli] warn if NODE_ENV or googleServicesFile is missing (#569)
* [eas-cli] warn if NODE_ENV or googleServicesFile is missing

* update CHANGELOG

* Apply suggestions from code review

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-08-20 12:31:46 +02:00
Wojciech Kozyra 61178c9d06 add changesNotSentForReview option to android submissions (#560)
* add changesNotSentForReview option to android submits

* update CHANGELOG

* fix types in tests

* review feedback
2021-08-19 15:01:14 +02:00
Wojciech KozyraandDominik Sokal 094c04c7f0 [eas-cli] support --json flag for build commands (#567)
* [eas-cli] support --json flag for build commands

* sanitize json object

* update CHANGELOG

* Apply suggestions from code review

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-08-19 14:58:19 +02:00
kgc00 37cca4878d [eas-cli] Introduce RudderStack (#563)
- adds RudderStack to the analytics file
- reworks some of the existing amplitude tracking event structure to properly record data
2021-08-18 10:46:19 -07:00
Wojciech Kozyra 2b40e1ec05 [eas-cli] support submit profiles (#555)
* [eas-cli] support submit profiles

* Update CHANGELOG

* review feedback
2021-08-13 11:53:58 +02:00
Bartłomiej Klocek 6e95d3afcc [eas cli] Fix SKU not being applied to App Store Connect (#559)
* [eas submit] Fix SKU not being applied to App Store Connect

* Update CHANGELOG
2021-08-13 10:36:09 +02:00
Wojciech Kozyra 6b9b9f0be2 [eas-cli] fix metadata for simulator builds (#556)
* [eas-cli] fix metadata for simulator builds

* Update CHANGELOG
2021-08-13 10:08:27 +02:00
Brent Vatne f14b963b29 [eas-cli] Stop using project name in build details URL (#554) 2021-08-12 21:53:47 -07:00
andy ddd2814e8f [submit] log submissions link at end of eas-submit (#553) 2021-08-11 11:00:22 -07:00
jkh 36f15a81a9 [changelog] fix formatting (#552) 2021-08-06 07:47:31 -07:00
jkh ece93dac20 [eas-cli] add --auto flag to publish (#549) 2021-08-06 05:58:04 -07:00
Brent Vatne 0226dbba5a Update CHANGELOG and README 2021-08-05 15:34:16 -07:00
Brent Vatne e773e77185 v0.23.0 2021-08-05 15:19:01 -07:00
Brent Vatne 893cbb2ec5 [eas-cli] Pass through build settings metadata context to app version resolver (#550) 2021-08-05 15:17:30 -07:00
jkh 1898de2609 [eas-cli] add type to generated manifest fragment (#535) 2021-08-05 06:50:20 -07:00
jkh fe03cc28c4 [eas-cli] stop sending deprecated storageBucket field (#547) 2021-08-03 13:55:00 -07:00
Wojciech Kozyra 5309b7b6ce Update CHANGELOG 2021-08-02 13:23:20 +02:00
Wojciech Kozyra bc9b8442d7 v0.22.4 2021-08-02 13:07:52 +02:00
Wojciech Kozyra a84762b00d [eas-cli] override distribution when building for simulator 2021-08-02 12:57:59 +02:00
Wojciech Kozyra 2d117e1d7e v0.22.3 2021-08-02 12:49:51 +02:00
Sallar 986674942d [eas-json][eas-cli] add simulator key to build profile json validator (#546)
* Add simulator to build profile json validator

* Update CHANGELOG.md

* Fix PR number

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

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

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

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

* use new App.builds api

* add more filters to build:list

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

* review feedback

* review feedback 2

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

* update CHANGELOG

* address PR feedback

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

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

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

* remove unused dependencies

* upgrade tslib

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

* detection logic

* add tests

* Update CHANGELOG, ignore `node_modules`

* Display a chooser for multiple files

* Improve tests

* move `filterAsync` to a separate file

* Apply suggestions from code review

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

* Fix indent

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

* update CHANGELOG

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

* Update CHANGELOG.md

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

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-07-26 15:33:44 +02:00
Dominik Sokal 9f63ad4527 update CHANGELOG 2021-07-23 10:33:19 +02:00
Dominik Sokal 878028271a v0.22.0 2021-07-23 10:25:00 +02:00
Dominik Sokal d4e15bbcbf add iosEnterpriseProvisioning to build metadata (#522)
* generate graphql code

* add iosEnterpriseProvisioning to build metadata

* update CHANGELOG

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

* changelog

* Update capabilityIdentifiers.ts

* Update capabilityIdentifiers.ts

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

* update CHANGELOG

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

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

* Display build details in submission summary

* Update tests

* Add test for build id

* Unify build fragment naming

* Apply suggestion

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

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

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

* Apply suggestion

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

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

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

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

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

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

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

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

* update CHANGELOG

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

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

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

* update changelog

* Apply suggestions from code review

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

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

* update CHANGELOG

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

* fyi

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

* [eas-cli] more credentials manager guardrails

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

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

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

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

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

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

* pr feedback

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

* update CHANGELOG

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

* update CHANGELOG

* address PR feedback
2021-06-29 10:51:48 +02:00
Quinlan Jung 90852b9557 update changelog 2021-06-28 16:01:48 -07:00
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
329 changed files with 21836 additions and 13366 deletions

No files matched your search

+3
View File
@@ -10,6 +10,7 @@ module.exports = {
jasmine: false,
},
rules: {
'no-console': 'warn',
'no-constant-condition': ['warn', { checkLoops: false }],
'sort-imports': [
'warn',
@@ -18,6 +19,8 @@ module.exports = {
},
],
'flowtype/no-types-missing-file-annotation': 'off',
curly: 'warn',
'import/no-relative-packages': 'error',
},
settings: {
react: {
+1
View File
@@ -0,0 +1 @@
**/yarn.lock linguist-generated
+47
View File
@@ -0,0 +1,47 @@
name: "\U0001F41B Bug Report in EAS CLI"
labels: 'bug'
description: 'You want to report a reproducible bug or regression in EAS CLI.'
body:
- type: markdown
attributes:
value: "We're looking to keep questions on our forums and bug reports on the GitHub repo. For questions and help using EAS CLI, post in our [community forums](https://forums.expo.dev/c/help-expo-application-services) (log in with your Expo developer account)."
- type: markdown
attributes:
value: Found a bug in the Expo SDK? Open a bug report [here](https://github.com/expo/expo/issues/new?assignees=&labels=needs+review&template=bug_report.yml) instead.
- type: markdown
attributes:
value: Thanks for taking the time to file a bug report! Please fill out this form as completely as possible.
- type: markdown
attributes:
value: If you leave out sections there is a high likelihood your issue will be closed. If you have a question, not a bug report, please post it on our [forums](https://forums.expo.dev/) instead.
- type: textarea
validations:
required: true
attributes:
label: Summary
description: Describe the issue in 1 or 2 sentences
placeholder: Clearly describe what the expected behavior is vs. what is actually happening. This should be as short as possible, while still communicating all the necessary information. If your summary is just 'X library/method isn't working', then you need to continue debugging yourself and provide more information.
- type: textarea
validations:
required: true
attributes:
label: Environment
placeholder: Run `eas diagnostics` and paste the output here
- type: textarea
validations:
required: false
attributes:
label: Error output
placeholder: If you're running into an error message, please run the same command with `EXPO_DEBUG=true <your command here>` and paste output here.
- type: textarea
validations:
required: true
attributes:
label: Reproducible demo or steps to reproduce from a blank project
description: 'This should include as little code as possible, do not simply link your entire project. Sharing a link to a [Snack](https://snack.expo.dev/) is a GREAT way to provide a reproducible demo :) If a reproducible demo, or a complete list of steps from blank project to bug, are not provided, it is very likely your issue will be closed. Read [here more guidance](https://stackoverflow.com/help/mcve).'
- type: markdown
attributes:
value: Please make sure contributors can follow the steps your provided in order to reproduce the bug.
- type: markdown
attributes:
value: "**Realize that it is up to you to debug your code and be as certain as possible that the bug is with EAS CLI, not with your own app.** [Here's an excellent guide to debugging you can follow](https://github.com/expo/fyi/blob/master/manual-debugging.md#how-to-narrow-down-the-source-of-an-error-a-step-by-step-guide-to-good-old-fashioned-manual-debugging)."
+14
View File
@@ -0,0 +1,14 @@
name: "\U0001F64B Enhancement"
labels: 'enhancement'
description: 'Want us to add something to EAS CLI?'
body:
- type: markdown
attributes:
value: Thanks for helping us make EAS even better! 😀 Unfortunately, this is not the right place for feature requests. Please find and upvote your feature (or create a new one) at https://expo.canny.io instead.
- type: dropdown
validations:
required: true
attributes:
label: Do you understand that any feature requests opened in this repository will be closed?
options:
- 'Yes'
+4 -4
View File
@@ -27,7 +27,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14'
node-version: '16'
- run: yarn install --frozen-lockfile --check-files
- run: yarn build
- name: Build tarballs
@@ -60,7 +60,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14'
node-version: '16'
- run: yarn install --frozen-lockfile --check-files
- run: yarn build
- name: Build tarballs
@@ -83,7 +83,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14'
node-version: '16'
- run: yarn install --frozen-lockfile --check-files
- run: sudo apt-get install nsis
- run: yarn build
@@ -112,4 +112,4 @@ jobs:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/win/${{ steps.x86.outputs.filename }}
asset_name: ${{ steps.x86.outputs.filename }}
asset_content_type: application/gzip
asset_content_type: application/gzip
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14'
node-version: '16'
- uses: preactjs/compressed-size-action@v2
with:
build-script: 'build:tarballs:linux'
+2 -2
View File
@@ -10,9 +10,9 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: ['12', '14']
node: ['12', '14', '16']
include:
- node: '14'
- node: '16'
coverage: true
name: Test with Node ${{ matrix.node }}
steps:
+1
View File
@@ -11,3 +11,4 @@ tmp
node_modules
coverage
.history
.DS_Store
+247 -5
View File
@@ -6,15 +6,257 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
- Require `eas submit` to be configured with eas.json submit profiles. Drop support for CLI params. ([#590](https://github.com/expo/eas-cli/pull/590) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Add `eas diagnostics` command. ([#594](https://github.com/expo/eas-cli/pull/594) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix `eas build` from throwing an exception when detecting the git `core.ignorecase` setting. ([#592](https://github.com/expo/eas-cli/pull/592) by [@mwillbanks](https://github.com/mwillbanks))
- Support app names that consist only of unicode characters. ([#596](https://github.com/expo/eas-cli/pull/596) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
## [0.25.0](https://github.com/expo/eas-cli/releases/tag/v0.25.0) - 2021-09-01
### 🛠 Breaking changes
- Drop support for node < 12. ([#581](https://github.com/expo/eas-cli/pull/581) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Support `--no-wait` in `eas submit`. ([#578](https://github.com/expo/eas-cli/pull/578) by [@dsokal](https://github.com/dsokal))
- Detect changes when `core.ignorecase` is set to true. ([#570](https://github.com/expo/eas-cli/pull/570) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Fix wrong warning for `metro.config.js` check on Windows. ([#587](https://github.com/expo/eas-cli/pull/588) by [@louisgv](https://github.com/louisgv))
- Fix detecting `googleServicesFile` with `EAS_NO_VCS=1`. ([#583](https://github.com/expo/eas-cli/pull/583) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Replace `@hapi/joi` with `joi`. Upgrade typescript to 4.4.2. Upgrade dependencies. ([#582](https://github.com/expo/eas-cli/pull/582) by [@dsokal](https://github.com/dsokal))
- Change typescript target from `ES2017` to `ES2019`. ([#584](https://github.com/expo/eas-cli/pull/584) by [@dsokal](https://github.com/dsokal))
- Use `ts-jest` instead of `babel-jest`. ([#585](https://github.com/expo/eas-cli/pull/585) by [@dsokal](https://github.com/dsokal))
## [0.24.1](https://github.com/expo/eas-cli/releases/tag/v0.24.1) - 2021-08-25
### 🐛 Bug fixes
- Fix wrong `EasJsonReader` import causing `eas submit` to fail. ([951ee](https://github.com/expo/eas-cli/commit/951eea2396591bd612aa7cb3038da35e45c33bb8) by [@dsokal](https://github.com/dsokal))
## [0.24.0](https://github.com/expo/eas-cli/releases/tag/v0.24.0) - 2021-08-25
### 🛠 Breaking changes
- Allow publishing of multiple update groups. ([#566](https://github.com/expo/eas-cli/pull/566) by [@jkhales](https://github.com/jkhales))
### 🎉 New features
- Retry status code 500-600 server errors from Apple servers with cookies auth. ([#574](https://github.com/expo/eas-cli/pull/574) by [@EvanBacon](https://github.com/EvanBacon))
- Throw internal server errors when Apple servers fail to authenticate. ([#574](https://github.com/expo/eas-cli/pull/574) by [@EvanBacon](https://github.com/EvanBacon))
- Add --auto flag for eas branch:publish. ([#549](https://github.com/expo/eas-cli/pull/549) by [@jkhales](https://github.com/jkhales))
- Add submissions link in output of `eas submit`. ([#553](https://github.com/expo/eas-cli/pull/553) by [@ajsmth](https://github.com/ajsmth))
- Add submit profiles. ([#555](https://github.com/expo/eas-cli/pull/555) by [@wkozyra95](https://github.com/wkozyra95))
- Add `changesNotSentForReview` option to android submissions. ([#560](https://github.com/expo/eas-cli/pull/560) by [@wkozyra95](https://github.com/wkozyra95))
- Support `--json` flag in build commands. ([#567](https://github.com/expo/eas-cli/pull/567) by [@wkozyra95](https://github.com/wkozyra95))
- Add link to https://expo.fyi/eas-build-archive in `eas build` output to make it easier to understand what is going on in the archive/upload phase of the build. ([#562](https://github.com/expo/eas-cli/pull/562) by [@brentvatne](https://github.com/brentvatne))
- Update QR code URL for Android internal distribution build and generate smaller QR code. ([#573](https://github.com/expo/eas-cli/pull/573) by [@axeldelafosse](https://github.com/axeldelafosse))
### 🐛 Bug fixes
- Fix `--sku` flag being ignored when running `eas submit -p ios`. ([#559](https://github.com/expo/eas-cli/pull/559) by [@barthap](https://github.com/barthap))
- Set correct distribution for simulator builds. ([#556](https://github.com/expo/eas-cli/pull/556) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Add warnings around common issues (`NODE_ENV=production`, git ignored `googleServicesFile`). ([#569](https://github.com/expo/eas-cli/pull/569) by [@wkozyra95](https://github.com/wkozyra95))
## [0.23.0](https://github.com/expo/eas-cli/releases/tag/v0.23.0) - 2021-08-05
### 🛠 Breaking changes
- Stop including deprecated storageBucket field when publishing. ([#547](https://github.com/expo/eas-cli/pull/547) by [@jkhales](https://github.com/jkhales))
### 🐛 Bug fixes
- Pass through metadata context to app version resolver. ([#550](https://github.com/expo/eas-cli/pull/550) by [@brentvatne](https://github.com/brentvatne))
## [0.22.4](https://github.com/expo/eas-cli/releases/tag/v0.22.4) - 2021-08-02
### 🐛 Bug fixes
- Fix build profile schema validation when `simulator` key is present for iOS. ([#546](https://github.com/expo/eas-cli/pull/546) by [@sallar](https://github.com/sallar))
## [0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2) - 2021-08-02
### 🛠 Breaking changes
- Introduce new format of eas.json (old eas.json will be migrated automatically). ([#537](https://github.com/expo/eas-cli/pull/537) by [@wkozyra95](https://github.com/wkozyra95))
### 🎉 New features
- Validate metro config for managed workflow projects. ([#534](https://github.com/expo/eas-cli/pull/534) by [@dsokal](https://github.com/dsokal))
- Add more filter params to `build:list`. ([#540](https://github.com/expo/eas-cli/pull/540) by [@dsokal](https://github.com/dsokal))
- Evaluate interpolated iOS version strings for metadata. ([#541](https://github.com/expo/eas-cli/pull/541) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Validate metadata client side to print better errors. ([#542](https://github.com/expo/eas-cli/pull/542) by [@wkozyra95](https://github.com/wkozyra95))
- Fix `new` build status when building both paltforms. ([#543](https://github.com/expo/eas-cli/pull/543) by [@wkozyra95](https://github.com/wkozyra95))
- Fix link to build details page where previously we used `project.name` instead of `project.slug` - leave out project segment entirely and depend on redirect. ([#554](https://github.com/expo/eas-cli/pull/554) by [@brentvatne](https://github.com/brentvatne))
## [0.22.1](https://github.com/expo/eas-cli/releases/tag/v0.22.1) - 2021-07-28
### 🎉 New features
- Include the file extension for update's assets in the manifest fragment. ([#532](https://github.com/expo/eas-cli/pull/535) by [@jkhales](https://github.com/jkhales))
- Use env variables from `eas.json` when evaluating `app.config.js`. ([#532](https://github.com/expo/eas-cli/pull/532) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix workflow detection when xcodeproj is an empty directory. ([#531](https://github.com/expo/eas-cli/pull/531) by [@wkozyra95](https://github.com/wkozyra95))
- Fix Android Keystore upload. ([#538](https://github.com/expo/eas-cli/pull/538) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Upgrade typescript to 4.3.5. ([#533](https://github.com/expo/eas-cli/pull/533) by [@dsokal](https://github.com/dsokal))
## [0.22.0](https://github.com/expo/eas-cli/releases/tag/v0.22.0) - 2021-07-23
### 🎉 New features
- Added support for automatically creating and linking iOS capability identifiers (Apple Pay, iCloud Containers, App Groups). ([#524](https://github.com/expo/eas-cli/pull/524) by [@EvanBacon](https://github.com/EvanBacon))
- Add `secret:create --force` command to overwrite existing secrets. ([#513](https://github.com/expo/eas-cli/pull/513) by [@bycedric](https://github.com/bycedric))
- Fall back to APK when building for internal distribution. ([#527](https://github.com/expo/eas-cli/pull/527) by [@dsokal](https://github.com/dsokal))
- Add `iosEnterpriseProvisioning` to build metadata. ([#522](https://github.com/expo/eas-cli/pull/522) by [@dsokal](https://github.com/dsokal))
- Autodetect Google Services JSON key path in `eas submit -p android`. ([#520](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
### 🐛 Bug fixes
- Fix iOS capability syncing on build. ([#521](https://github.com/expo/eas-cli/pull/521) by [@EvanBacon](https://github.com/EvanBacon))
- Fix unhandled error when amplitude domains are blocked. ([#512](https://github.com/expo/eas-cli/pull/512) by [@wkozyra95](https://github.com/wkozyra95))
- Use default value for `appBuildVersion` in build metadata when building an Android managed project. ([#526](https://github.com/expo/eas-cli/pull/526) by [@dsokal](https://github.com/dsokal))
## [0.21.0](https://github.com/expo/eas-cli/releases/tag/v0.21.0) - 2021-07-12
### 🎉 New features
- Add `project:*` commands. ([#500](https://github.com/expo/eas-cli/pull/500) by [@jkhales](https://github.com/jkhales))
- Added support for iOS 15 capabilities: Communication Notifications, Time Sensitive Notifications, Group Activities, and Family Controls. ([#499](https://github.com/expo/eas-cli/pull/499) by [@EvanBacon](https://github.com/EvanBacon))
- Show more build metadata in `build:view` and `build:list`. ([#504](https://github.com/expo/eas-cli/pull/504) and [#508](https://github.com/expo/eas-cli/pull/508) by [@dsokal](https://github.com/dsokal))
- Add runtime version to build metadata. ([#493](https://github.com/expo/eas-cli/pull/493) by [@dsokal](https://github.com/dsokal))
## [0.20.0](https://github.com/expo/eas-cli/releases/tag/v0.20.0) - 2021-07-09
### 🛠 Breaking changes
- Unifify generic and managed workflow, deprecate `workflow` field. ([#497](https://github.com/expo/eas-cli/pull/497) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Fix runtime version checks. ([#495](https://github.com/expo/eas-cli/pull/495) by [@dsokal](https://github.com/dsokal))
- Resolve `--android-package` correctly in `eas submit` command. ([#494](https://github.com/expo/eas-cli/pull/494) by [@wkozyra95](https://github.com/wkozyra95))
## [0.19.1](https://github.com/expo/eas-cli/releases/tag/v0.19.1) - 2021-07-02
### 🎉 New features
- Improve project workflow detection (fixes the case where the `android` and/or `ios` directories are git-ignored). ([#490](https://github.com/expo/eas-cli/pull/490) by [@dsokal](https://github.com/dsokal))
- Improve credentials workflow with project creation and current working directories ([#491](https://github.com/expo/eas-cli/pull/491) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Better debugging experience ([#492](https://github.com/expo/eas-cli/pull/492) by [@quinlanj](https://github.com/quinlanj))
## [0.19.0](https://github.com/expo/eas-cli/releases/tag/v0.19.0) - 2021-07-01
### 🎉 New features
- Auto-suggest application id and bundle identifier when running `eas build:configure` for a managed project. ([#487](https://github.com/expo/eas-cli/pull/487) by [@dsokal](https://github.com/dsokal))
- Support configuring one platform at a time when running `eas build`. ([#483](https://github.com/expo/eas-cli/pull/483) by [@brentvatne](https://github.com/brentvatne))
## [0.18.3](https://github.com/expo/eas-cli/releases/tag/v0.18.3) - 2021-06-28
### 🎉 New features
- Support bundle identifiers that begin with `.`. ([#459](https://github.com/expo/eas-cli/pull/459) by [@EvanBacon](https://github.com/EvanBacon))
- Push Key setup integrated with ios build command ([#473](https://github.com/expo/eas-cli/pull/473) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Fix android builds for PKCS12 keystores. ([#484](https://github.com/expo/eas-cli/pull/484) by [@quinlanj](https://github.com/quinlanj))
## [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
- 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 multitarget iOS projects. ([#414](https://github.com/expo/eas-cli/pull/414) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
### 🧹 Chores
- 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
+7
View File
@@ -0,0 +1,7 @@
# Community Code of Conduct
The Expo team is committed to fostering an open and welcoming environment.
**Our Community Guidelines can be found here:**
https://expo.dev/guidelines
-6
View File
@@ -1,6 +0,0 @@
module.exports = function (api) {
api.cache(true);
return {
presets: ['@expo/babel-preset-cli'],
};
};
+1 -3
View File
@@ -1,9 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testRegex: '/__tests__/.*(test|spec)\\.[jt]sx?$',
transform: {
'^.+\\.[jt]sx?$': ['babel-jest', { configFile: require.resolve('./babel.config.js') }],
},
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
collectCoverageFrom: ['<rootDir>/src/**/*.{ts,tsx,js,jsx}'],
};
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.16.0"
"version": "0.26.0"
}
+15 -9
View File
@@ -10,19 +10,25 @@
"start": "lerna run watch --parallel",
"watch": "yarn start",
"eas": "packages/eas-cli/bin/run",
"lint": "eslint . --ext .ts",
"lint": "eslint . --ext .ts --ext .js",
"release": "lerna version",
"test": "jest"
},
"devDependencies": {
"@expo/oclif-dev-cli": "1.24.0-expo",
"@types/jest": "^26.0.13",
"eslint": "^6.6.0",
"eslint-config-universe": "^4.0.0",
"eslint-plugin-graphql": "^4.0.0",
"jest": "^26.4.2",
"jest-watch-typeahead": "^0.6.1",
"lerna": "^3.22.1",
"prettier": "^2.0.5"
"@types/jest": "27.0.1",
"@types/node": "16.7.8",
"eslint": "7.32.0",
"eslint-config-universe": "7.0.1",
"jest": "27.1.0",
"jest-watch-typeahead": "0.6.4",
"lerna": "4.0.0",
"prettier": "2.3.2",
"ts-jest": "27.0.5",
"typescript": "4.4.2"
},
"volta": {
"node": "16.8.0",
"yarn": "1.22.11"
}
}
+2
View File
@@ -0,0 +1,2 @@
graphql.schema.json linguist-generated
src/graphql/generated.ts linguist-generated
+118 -95
View File
@@ -42,15 +42,19 @@ eas --help COMMAND
* [`eas build:configure`](#eas-buildconfigure)
* [`eas build:list`](#eas-buildlist)
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
* [`eas config`](#eas-config)
* [`eas credentials`](#eas-credentials)
* [`eas device:create`](#eas-devicecreate)
* [`eas device:list`](#eas-devicelist)
* [`eas device:view [UDID]`](#eas-deviceview-udid)
* [`eas diagnostics`](#eas-diagnostics)
* [`eas help [COMMAND]`](#eas-help-command)
* [`eas project:info`](#eas-projectinfo)
* [`eas project:init`](#eas-projectinit)
* [`eas secret:create`](#eas-secretcreate)
* [`eas secret:delete`](#eas-secretdelete)
* [`eas secret:list`](#eas-secretlist)
* [`eas submit --platform=(android|ios)`](#eas-submit---platformandroidios)
* [`eas submit`](#eas-submit)
* [`eas webhook:create`](#eas-webhookcreate)
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
* [`eas webhook:list`](#eas-webhooklist)
@@ -69,7 +73,7 @@ ALIASES
$ eas login
```
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/account/login.ts)_
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/account/login.js)_
## `eas account:logout`
@@ -83,7 +87,7 @@ ALIASES
$ eas logout
```
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/account/logout.ts)_
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/account/logout.js)_
## `eas account:view`
@@ -97,22 +101,22 @@ ALIASES
$ eas whoami
```
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/account/view.ts)_
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/account/view.js)_
## `eas analytics [STATUS]`
View or change analytics settings
view or change analytics settings
```
USAGE
$ eas analytics [STATUS]
```
_See code: [build/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/analytics.ts)_
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/analytics.js)_
## `eas build`
Start a build
start a build
```
USAGE
@@ -121,15 +125,15 @@ USAGE
OPTIONS
-p, --platform=(android|ios|all)
--clear-cache Clear cache before the build
--json Enable JSON output, non-JSON messages will be printed to stderr
--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.16.0/build/commands/build/index.ts)_
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/build/index.js)_
## `eas build:cancel [BUILD_ID]`
@@ -140,7 +144,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [build/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/build/cancel.js)_
## `eas build:configure`
@@ -152,10 +156,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.16.0/build/commands/build/configure.ts)_
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/build/configure.js)_
## `eas build:list`
@@ -166,12 +169,29 @@ USAGE
$ eas build:list
OPTIONS
--appBuildVersion=appBuildVersion
--appIdentifier=appIdentifier
--appVersion=appVersion
--buildProfile=buildProfile
--channel=channel
--distribution=(store|internal|simulator)
--gitCommitHash=gitCommitHash
--json Enable JSON output, non-JSON messages will be printed
to stderr
--limit=limit
--platform=(all|android|ios)
--status=(in-queue|in-progress|errored|finished|canceled)
--runtimeVersion=runtimeVersion
--sdkVersion=sdkVersion
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/build/list.ts)_
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/build/list.js)_
## `eas build:view [BUILD_ID]`
@@ -180,20 +200,38 @@ view a build for your project
```
USAGE
$ eas build:view [BUILD_ID]
OPTIONS
--json Enable JSON output, non-JSON messages will be printed to stderr
```
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/build/view.ts)_
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/build/view.js)_
## `eas config`
show the eas.json config
```
USAGE
$ eas config
OPTIONS
-p, --platform=(android|ios)
--profile=profile
```
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/config.js)_
## `eas credentials`
Manage your credentials
manage your credentials
```
USAGE
$ eas credentials
```
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/credentials.ts)_
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/credentials.js)_
## `eas device:create`
@@ -204,7 +242,7 @@ USAGE
$ eas device:create
```
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/device/create.ts)_
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/device/create.js)_
## `eas device:list`
@@ -218,7 +256,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/device/list.ts)_
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/device/list.js)_
## `eas device:view [UDID]`
@@ -229,7 +267,18 @@ USAGE
$ eas device:view [UDID]
```
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/device/view.ts)_
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/device/view.js)_
## `eas diagnostics`
log environment info to the console
```
USAGE
$ eas diagnostics
```
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/diagnostics.js)_
## `eas help [COMMAND]`
@@ -246,7 +295,32 @@ OPTIONS
--all see all commands in CLI
```
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.0/src/commands/help.ts)_
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.3/src/commands/help.ts)_
## `eas project:info`
information about the current project
```
USAGE
$ eas project:info
```
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/project/info.js)_
## `eas project:init`
create or link an EAS project
```
USAGE
$ eas project:init
ALIASES
$ eas init
```
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/project/init.js)_
## `eas secret:create`
@@ -257,12 +331,13 @@ USAGE
$ eas secret:create
OPTIONS
--force Delete and recreate existing secrets
--name=name Name of the secret
--scope=(account|project) [default: project] Scope for the secret
--value=value Value of the secret
```
_See code: [build/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/secret/create.ts)_
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/secret/create.js)_
## `eas secret:delete`
@@ -279,7 +354,7 @@ 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.16.0/build/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/secret/delete.js)_
## `eas secret:list`
@@ -290,86 +365,34 @@ USAGE
$ eas secret:list
```
_See code: [build/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/secret/list.ts)_
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/secret/list.js)_
## `eas submit --platform=(android|ios)`
## `eas submit`
Submits build artifact to app store
submit build archive to app store
```
USAGE
$ eas submit --platform=(android|ios)
$ eas submit
OPTIONS
-p, --platform=(android|ios) For which platform you want to submit a build
-p, --platform=(android|ios)
--id=id ID of the build to submit
--latest Submit the latest build for specified platform
--path=path Path to the .apk/.aab/.ipa file
--profile=profile Name of the submit profile from eas.json. Defaults to "release" if defined in eas.json.
--url=url App archive url
--verbose Always print logs from Submission Service
--[no-]wait Wait for submission to complete
Android specific options=android-package Android package name (default: expo.android.package
from app config)
iOS specific options=app-name The name of your app as it will appear on the App
Store (default: expo.name from app config)
iOS specific options=apple-id Your Apple ID username (you can also set
EXPO_APPLE_ID env variable)
iOS specific options=apple-team-id Your Apple Developer Team ID
iOS specific options=asc-app-id App Store Connect unique application Apple ID
number. Providing this param results in skipping
app creation step. Learn more.
iOS specific options=bundle-identifier Your iOS Bundle Identifier (default:
expo.ios.bundleIdentifier from app config)
iOS specific options=company-name The name of your company, needed only for the first
upload of any app to App Store
--id=id ID of the build to submit
Android specific options=key Path to the JSON key used to authenticate with
Google Play
iOS specific options=language [default: en-US] Primary language (e.g. English,
German, ...)
--latest Submit the latest build for specified platform
--path=path Path to the .apk/.aab file
Android specific options=(completed|draft|halted|inProgress) [default: completed] Release status (used when
uploading new APKs/AABs)
iOS specific options=sku An unique ID for your app that is not visible on
the App Store, will be generated unless provided
Android specific options=(production|beta|alpha|internal|rollout) [default: internal] The track of the application to
use
Android specific options=(apk|aab) Android archive type
--url=url App archive url
--verbose Always print logs from Submission Service
DESCRIPTION
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
ALIASES
$ eas build:submit
EXAMPLES
$ eas submit --platform=ios
- Fully interactive iOS submission
$ eas submit --platform=android
- Fully interactive Android submission
$ eas submit -p android --latest --key=/path/to/google-services.json
- Minimal non-interactive Android submission, however it can ask you for other params if not specified
$ EXPO_APPLE_APP_SPECIFIC_PASSWORD=xxx eas submit -p ios --latest [email protected] --asc-app-id=1234567890,
- Minimal non-interactive iOS submission, assuming you already have an app in App Store Connect
and provide its App ID
```
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/submit.ts)_
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/submit.js)_
## `eas webhook:create`
@@ -388,7 +411,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [build/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/webhook/create.js)_
## `eas webhook:delete [ID]`
@@ -402,7 +425,7 @@ ARGUMENTS
ID ID of the webhook to delete
```
_See code: [build/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/webhook/delete.js)_
## `eas webhook:list`
@@ -416,7 +439,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.16.0/build/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/webhook/list.js)_
## `eas webhook:update`
@@ -436,7 +459,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [build/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/webhook/update.js)_
## `eas webhook:view ID`
@@ -450,5 +473,5 @@ ARGUMENTS
ID ID of the webhook to view
```
_See code: [build/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.16.0/build/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/webhook/view.js)_
<!-- commandsstop -->
+10
View File
@@ -1,2 +1,12 @@
import { fs } from 'memfs';
// needed because of a weird bug with tempy (dependency of @expo/config-plugins)
fs.mkdirSync('/tmp');
if (process.env.TMPDIR) {
fs.mkdirSync(process.env.TMPDIR, { recursive: true });
}
// fs-extra@10 is not compatible with memfs, the following line fixes tests
(fs.realpath as any).native = jest.fn();
module.exports = fs;
-3
View File
@@ -1,3 +0,0 @@
module.exports = async () => {
process.env.TZ = 'UTC';
};
+4670 -1602
View File
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -2,5 +2,6 @@ module.exports = {
...require('../../jest/jest.config.js'),
rootDir: __dirname,
roots: ['src', '__mocks__'],
globalSetup: './global-setup.ts',
globalSetup: './jest/global-setup.ts',
setupFilesAfterEnv: ['./jest/setup-after-env.ts'],
};
+5
View File
@@ -0,0 +1,5 @@
module.exports = async () => {
process.env.TZ = 'UTC';
process.env.EAS_NO_VCS = '1';
process.env.EAS_PROJECT_ROOT = '/app';
};
+6
View File
@@ -0,0 +1,6 @@
/* eslint-disable no-console */
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
console.error = jest.fn();
});
+64 -61
View File
@@ -1,111 +1,104 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.16.0",
"author": "Expo <support@expo.io>",
"version": "0.26.0",
"author": "Expo <support@expo.dev>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@amplitude/identify": "1.5.0",
"@amplitude/node": "1.5.0",
"@expo/apple-utils": "0.0.0-alpha.20",
"@amplitude/identify": "1.7.0",
"@amplitude/node": "1.7.0",
"@expo/apple-utils": "0.0.0-alpha.25",
"@expo/config": "3.3.42",
"@expo/config-plugins": "1.0.33",
"@expo/eas-build-job": "0.2.35",
"@expo/eas-json": "^0.16.0",
"@expo/config-plugins": "3.1.0",
"@expo/eas-build-job": "0.2.46",
"@expo/eas-json": "^0.26.0",
"@expo/json-file": "8.2.25",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.11",
"@expo/plist": "0.0.14",
"@expo/plugin-warn-if-update-available": "1.7.0",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.0.7",
"@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",
"@oclif/errors": "1.3.5",
"@oclif/plugin-help": "3.2.3",
"@urql/core": "2.3.1",
"@urql/exchange-retry": "0.3.0",
"chalk": "4.1.2",
"cli-table3": "0.6.0",
"dateformat": "3.0.3",
"dateformat": "4.5.1",
"env-paths": "2.2.0",
"fast-glob": "3.2.4",
"envinfo": "7.8.1",
"fast-glob": "3.2.7",
"figures": "3.2.0",
"form-data": "3.0.0",
"fs-extra": "9.0.1",
"fs-extra": "10.0.0",
"getenv": "1.0.0",
"got": "11.5.2",
"graphql": "15.3.0",
"graphql-tag": "2.11.0",
"got": "11.8.2",
"graphql": "15.5.1",
"graphql-tag": "2.12.5",
"ignore": "5.1.8",
"indent-string": "4.0.0",
"jks-js": "1.0.0",
"jks-js": "1.0.1",
"joi": "17.4.2",
"keychain": "1.3.0",
"lodash": "4.17.20",
"mime": "2.4.7",
"lodash": "4.17.21",
"mime": "2.5.2",
"minimatch": "3.0.4",
"nanoid": "3.1.23",
"nanoid": "3.1.25",
"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",
"resolve-from": "5.0.0",
"semver": "7.3.5",
"strip-ansi": "6.0.0",
"tar": "6.0.5",
"tar": "6.1.11",
"tempy": "0.7.0",
"terminal-link": "2.1.1",
"timeago.js": "4.0.2",
"tslib": "1.14.1",
"turndown": "7.0.0",
"tslib": "2.3.1",
"turndown": "7.1.1",
"untildify": "4.0.0",
"uuid": "8.3.0",
"uuid": "8.3.2",
"wrap-ansi": "7.0.0"
},
"devDependencies": {
"@babel/core": "^7.11.6",
"@babel/preset-env": "^7.11.5",
"@babel/preset-typescript": "^7.10.4",
"@expo/babel-preset-cli": "^0.2.17",
"@graphql-codegen/cli": "^1.19.2",
"@graphql-codegen/introspection": "^1.18.0",
"@graphql-codegen/typescript": "^1.17.11",
"@graphql-codegen/typescript-operations": "^1.17.10",
"@types/color-string": "^1.5.0",
"@types/dateformat": "^3.0.1",
"@types/dateformat": "3.0.1",
"@types/envinfo": "7.8.1",
"@types/form-data": "^2.5.0",
"@types/fs-extra": "^9.0.1",
"@types/fs-extra": "9.0.12",
"@types/getenv": "^1.0.0",
"@types/lodash": "^4.14.161",
"@types/mime": "^2.0.3",
"@types/node": "^12",
"@types/lodash": "4.14.172",
"@types/mime": "2.0.3",
"@types/node-fetch": "^2.5.7",
"@types/node-forge": "^0.9.5",
"@types/progress": "^2.0.3",
"@types/prompts": "^2.0.9",
"@types/semver": "^7.3.4",
"@types/tar": "^4.0.3",
"@types/tough-cookie": "^4.0.0",
"@types/uuid": "^8.3.0",
"@types/wrap-ansi": "^3.0.0",
"@vercel/ncc": "^0.23.0",
"axios": "^0.21.0",
"babel-jest": "^26.3.0",
"memfs": "^3.2.0",
"@types/semver": "7.3.8",
"@types/tar": "4.0.5",
"@types/tough-cookie": "4.0.1",
"@types/uuid": "8.3.1",
"@types/wrap-ansi": "3.0.0",
"axios": "0.21.4",
"eslint-plugin-graphql": "4.0.0",
"memfs": "3.2.2",
"mockdate": "^3.0.2",
"nock": "^13.0.5",
"ts-node": "^8",
"typescript": "^3.9.7",
"wonka": "^4.0.14"
"nock": "^13.0.5"
},
"engines": {
"node": ">=8.0.0"
"node": ">=12.0.0"
},
"files": [
"/bin",
@@ -129,15 +122,27 @@
],
"helpClass": "./build/help",
"topics": {
"account": {
"description": "manage your account"
},
"build": {
"description": "build app binaries"
},
"channel": {
"description": "manage update channels"
},
"device": {
"description": "manage your Apple devices for internal distribution"
},
"project": {
"description": "manage your project"
},
"release": {
"description": "manage update releases"
},
"secret": {
"description": "manage project and account secrets"
},
"update": {
"description": "manage individual updates"
},
@@ -166,14 +171,12 @@
"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",
"gitHead": "161808bc37c255fdea6801d69d23dbc725c056c3"
}
}
+10
View File
@@ -0,0 +1,10 @@
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);
// eslint-disable-next-line no-console
console.log('Patched README path generation');
+2 -2
View File
@@ -24,7 +24,7 @@ describe('apiClient', () => {
let error: Error | null = null;
try {
await apiClient.post('test');
} catch (e) {
} catch (e: any) {
error = e;
}
@@ -44,7 +44,7 @@ describe('apiClient', () => {
let error: Error | null = null;
try {
await apiClient.post('test');
} catch (e) {
} catch (e: any) {
error = e;
}
expect(error).toBeInstanceOf(RequestError);
+88 -19
View File
@@ -1,6 +1,8 @@
import { Identify } from '@amplitude/identify';
import * as Amplitude from '@amplitude/node';
import RudderAnalytics from '@expo/rudder-sdk-node';
import os from 'os';
import { URL } from 'url';
import { v4 as uuidv4 } from 'uuid';
import UserSettings from './user/UserSettings';
@@ -18,8 +20,9 @@ const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
linux: 'Linux',
};
let client: Amplitude.NodeClient | null = null;
let userIdentifyCalled = false;
let amplitudeClient: Amplitude.NodeClient | null = null;
let rudderstackClient: RudderAnalytics | null = null;
let userIdentified = false;
let identifyData: {
userId: string;
deviceId: string;
@@ -32,11 +35,33 @@ export async function initAsync(): Promise<void> {
}
const amplitudeEnabled = await UserSettings.getAsync('amplitudeEnabled', true);
if (amplitudeEnabled) {
const apiKey =
const config =
process.env.EXPO_STAGING || process.env.EXPO_LOCAL
? 'cdebbc678931403439486c4750781544'
: '4ac443afd5073c0df6169291db1d3495';
client = Amplitude.init(apiKey);
? {
// staging environment
amplitudeWriteKey: 'cdebbc678931403439486c4750781544',
rudderstackWriteKey: '1wpX20Da4ltFGSXbPFYUL00Chb7',
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
}
: {
// prod environment
amplitudeWriteKey: '4ac443afd5073c0df6169291db1d3495',
rudderstackWriteKey: '1wpXLFxmujq86etH6G6cc90hPcC',
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
};
amplitudeClient = Amplitude.init(config.amplitudeWriteKey, {
retryClass: new Amplitude.OfflineRetryHandler(config.amplitudeWriteKey),
});
rudderstackClient = new RudderAnalytics(
config.rudderstackWriteKey,
new URL('/v1/batch', config.rudderstackDataPlaneURL).toString(),
{
flushInterval: 300,
}
);
rudderstackClient.logger.silent = true;
}
}
@@ -53,6 +78,7 @@ export async function setUserDataAsync(
if (!savedDeviceId) {
await UserSettings.setAsync('amplitudeDeviceId', deviceId);
}
identifyData = {
userId,
deviceId,
@@ -63,39 +89,82 @@ export async function setUserDataAsync(
}
export async function flushAsync(): Promise<void> {
if (client) {
await client.flush();
if (rudderstackClient) {
rudderstackClient.flush();
}
if (amplitudeClient) {
await amplitudeClient.flush();
}
}
export function logEvent(name: string, properties: Record<string, any> = {}) {
if (client) {
ensureUserIdentified();
const { userId, deviceId } = identifyData ?? {};
client.logEvent({
if (!amplitudeClient && !rudderstackClient) {
return;
}
ensureUserIdentified();
const { userId, deviceId } = identifyData ?? {};
const commonEventProperties = { source_version: packageJSON?.version, source: 'eas cli' };
if (amplitudeClient) {
amplitudeClient.logEvent({
event_type: name,
event_properties: properties,
event_properties: { ...properties, ...commonEventProperties },
...(userId && { user_id: userId }),
...(deviceId && { device_id: deviceId }),
...getContext(),
...getAmplitudeContext(),
});
}
if (rudderstackClient) {
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
rudderstackClient.track({
event: name,
properties: { ...properties, ...commonEventProperties },
...identity,
context: getRudderStackContext(),
});
}
}
function ensureUserIdentified() {
if (client && !userIdentifyCalled && identifyData) {
client.identify(identifyData.userId, identifyData.deviceId, identifyData.identify);
userIdentifyCalled = true;
if (!(rudderstackClient || amplitudeClient) || userIdentified || !identifyData) {
return;
}
if (amplitudeClient) {
amplitudeClient.identify(identifyData.userId, identifyData.deviceId, identifyData.identify);
}
if (rudderstackClient) {
rudderstackClient.identify({
userId: identifyData.userId,
anonymousId: identifyData.deviceId,
});
}
userIdentified = true;
}
function getContext() {
function getAmplitudeContext() {
const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform();
return {
os_name: platform,
os_version: os.release(),
device_brand: platform,
device_model: platform,
app_version: packageJSON?.version,
};
}
function getRudderStackContext() {
const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform();
return {
os: { name: platform, version: os.release() },
device: { type: platform, model: platform },
app: { name: 'eas cli', version: packageJSON?.version ?? undefined },
};
}
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
}
+2 -2
View File
@@ -50,10 +50,10 @@ export function getExpoApiBaseUrl(): string {
export function getExpoWebsiteBaseUrl(): string {
if (process.env.EXPO_STAGING) {
return `https://staging.expo.io`;
return `https://staging.expo.dev`;
} else if (process.env.EXPO_LOCAL) {
return `http://expo.test`;
} else {
return `https://expo.io`;
return `https://expo.dev`;
}
}
@@ -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';
@@ -14,20 +14,16 @@ export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig)
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
const updatedBuildGradleContents = AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
projectDir,
buildGradleContents
);
const updatedBuildGradleContents =
AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
projectDir,
buildGradleContents
);
await fs.writeFile(buildGradlePath, updatedBuildGradleContents);
}
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 +48,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);
@@ -78,13 +71,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 +80,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 +89,27 @@ 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,
AndroidConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
);
if (!stringifiedRequestHeaders) {
return null;
}
return JSON.parse(stringifiedRequestHeaders)['expo-channel-name'] ?? null;
} catch (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>
`;
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { readVersionCode, readVersionName } from '../version';
import { readVersionCodeAsync, readVersionNameAsync } from '../version';
jest.mock('fs');
@@ -20,35 +20,35 @@ beforeEach(() => {
fs.mkdirpSync(os.tmpdir());
});
describe(readVersionCode, () => {
describe(readVersionCodeAsync, () => {
describe('generic project', () => {
it('reads the version code from native code', () => {
it('reads the version code from native code', async () => {
const exp = initGenericProject();
const versionCode = readVersionCode('/repo', exp);
const versionCode = await readVersionCodeAsync('/repo', exp);
expect(versionCode).toBe(123);
});
});
describe('managed project', () => {
it('reads the version code from expo config', () => {
it('reads the version code from expo config', async () => {
const exp = initManagedProject();
const versionCode = readVersionCode('/repo', exp);
const versionCode = await readVersionCodeAsync('/repo', exp);
expect(versionCode).toBe(123);
});
});
});
describe(readVersionName, () => {
describe(readVersionNameAsync, () => {
describe('generic project', () => {
it('reads the version name from native code', () => {
it('reads the version name from native code', async () => {
const exp = initGenericProject();
const versionName = readVersionName('/repo', exp);
const versionName = await readVersionNameAsync('/repo', exp);
expect(versionName).toBe('1.0');
});
});
describe('managed project', () => {
it('reads the version from expo config', () => {
it('reads the version from expo config', async () => {
const exp = initManagedProject();
const versionName = readVersionName('/repo', exp);
const versionName = await readVersionNameAsync('/repo', exp);
expect(versionName).toBe('1.0.0');
});
});
@@ -72,6 +72,7 @@ function initGenericProject(): ExpoConfig {
versionName "1.0"
}
}`,
'./android/app/src/main/AndroidManifest.xml': 'fake',
},
'/repo'
);
+46 -63
View File
@@ -1,8 +1,6 @@
import { Android, Metadata, Workflow } from '@expo/eas-build-job';
import { EasConfig } from '@expo/eas-json';
import { Android, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import nullthrows from 'nullthrows';
import AndroidCredentialsProvider, {
AndroidCredentials,
@@ -10,44 +8,31 @@ import AndroidCredentialsProvider, {
import { createCredentialsContextAsync } from '../../credentials/context';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import Log from '../../log';
import { getOrConfigureApplicationIdAsync } from '../../project/android/applicationId';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
getApplicationIdAsync,
} from '../../project/android/applicationId';
import { toggleConfirmAsync } from '../../prompts';
import { CredentialsResult, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommandContext, createBuildContext } from '../context';
import { findAccountByName } from '../../user/Account';
import {
BuildRequestSender,
CredentialsResult,
prepareBuildRequestForPlatformAsync,
} from '../build';
import { BuildContext } from '../context';
import { transformMetadata } from '../graphql';
import { Platform } from '../types';
import { logCredentialsSource } from '../utils/credentials';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { transformGenericJob, transformManagedJob } from './graphql';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
export async function prepareAndroidBuildAsync(
commandCtx: CommandContext,
easConfig: EasConfig
): Promise<() => Promise<string | undefined>> {
const buildCtx = createBuildContext<Platform.ANDROID>({
commandCtx,
platform: Platform.ANDROID,
easConfig,
});
const { buildProfile } = buildCtx;
ctx: BuildContext<Platform.ANDROID>
): Promise<BuildRequestSender> {
const { buildProfile } = ctx;
if (
buildProfile.workflow === Workflow.GENERIC &&
!(await fs.pathExists(path.join(commandCtx.projectDir, 'android')))
) {
throw new Error(
`"android" directory not found. If you're trying to build a managed project, set ${chalk.bold(
`builds.android.${commandCtx.profile}.workflow`
)} in "eas.json" to "managed".`
);
}
if (
buildProfile.workflow === Workflow.GENERIC &&
buildProfile.distribution === 'internal' &&
buildProfile.gradleCommand?.match(/bundle/)
) {
if (buildProfile.distribution === 'internal' && buildProfile.gradleCommand?.match(/bundle/)) {
Log.addNewLineIfNone();
Log.warn(
`You're building your Android app for internal distribution. However, we've detected that the Gradle command you defined (${chalk.underline(
@@ -63,17 +48,22 @@ This means that it will most likely produce an AAB and you will not be able to i
}
}
// this function throws if application id is invalid
await getOrConfigureApplicationIdAsync(commandCtx.projectDir, commandCtx.exp);
checkNodeEnvVariable(ctx);
await checkGoogleServicesFileAsync(ctx);
if (ctx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
}
return await prepareBuildRequestForPlatformAsync({
ctx: buildCtx,
ctx,
ensureCredentialsAsync: ensureAndroidCredentialsAsync,
ensureProjectConfiguredAsync: async () => {
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
await validateAndSyncProjectConfigurationAsync(commandCtx.projectDir, commandCtx.exp);
if (ctx.workflow === Workflow.GENERIC) {
await validateAndSyncProjectConfigurationAsync(ctx.projectDir, ctx.exp);
}
},
getMetadataContext: () => ({}),
prepareJobAsync,
sendBuildRequestAsync: async (
appId: string,
@@ -81,30 +71,18 @@ This means that it will most likely produce an AAB and you will not be able to i
metadata: Metadata
): Promise<BuildResult> => {
const graphqlMetadata = transformMetadata(metadata);
if (job.type === Workflow.GENERIC) {
const graphqlJob = transformGenericJob(job);
return await BuildMutation.createAndroidGenericBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
} else {
const graphqlJob = transformManagedJob(job);
return await BuildMutation.createAndroidManagedBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
}
const graphqlJob = transformJob(job);
return await BuildMutation.createAndroidBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
},
});
}
function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean {
return (
ctx.buildProfile.workflow === Workflow.MANAGED ||
(ctx.buildProfile.workflow === Workflow.GENERIC && !ctx.buildProfile.withoutCredentials)
);
return !ctx.buildProfile.withoutCredentials;
}
async function ensureAndroidCredentialsAsync(
@@ -113,16 +91,21 @@ async function ensureAndroidCredentialsAsync(
if (!shouldProvideCredentials(ctx)) {
return;
}
const androidApplicationIdentifier = await getApplicationIdAsync(ctx.projectDir, ctx.exp);
const provider = new AndroidCredentialsProvider(
await createCredentialsContextAsync(ctx.commandCtx.projectDir, {
nonInteractive: ctx.commandCtx.nonInteractive,
await createCredentialsContextAsync(ctx.projectDir, {
exp: ctx.exp,
nonInteractive: ctx.nonInteractive,
}),
{
app: {
projectName: ctx.commandCtx.projectName,
accountName: ctx.commandCtx.accountName,
account: nullthrows(
findAccountByName(ctx.user.accounts, ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.projectName,
androidApplicationIdentifier,
},
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
}
);
const { credentialsSource } = ctx.buildProfile;
@@ -3,17 +3,17 @@ import { AndroidConfig } from '@expo/config-plugins';
import Log from '../../log';
import {
getOrConfigureApplicationIdAsync,
ensureApplicationIdIsDefinedForManagedProjectAsync,
warnIfAndroidPackageDefinedInAppConfigForGenericProject,
} from '../../project/android/applicationId';
import { gitAddAsync } from '../../utils/git';
import vcs from '../../vcs';
import { ConfigureContext } from '../context';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void> {
if (!ctx.hasAndroidNativeProject) {
await getOrConfigureApplicationIdAsync(ctx.projectDir, ctx.exp);
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
return;
}
@@ -22,7 +22,7 @@ export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
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);
+12 -25
View File
@@ -1,44 +1,31 @@
import { Android } from '@expo/eas-build-job';
import {
AndroidGenericJobInput,
AndroidManagedBuildType,
AndroidManagedJobInput,
} from '../../graphql/generated';
import { transformProjectArchive } from '../graphql';
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
import { transformProjectArchive, transformWorkflow } from '../graphql';
export function transformGenericJob(job: Android.GenericJob): AndroidGenericJobInput {
export function transformJob(job: Android.Job): AndroidJobInput {
return {
type: transformWorkflow(job.type),
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
updates: job.updates,
secrets: job.secrets,
builderEnvironment: job.builderEnvironment,
cache: job.cache,
gradleCommand: job.gradleCommand,
artifactPath: job.artifactPath,
};
}
export function transformManagedJob(job: Android.ManagedJob): AndroidManagedJobInput {
return {
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
secrets: job.secrets,
builderEnvironment: job.builderEnvironment,
cache: job.cache,
username: job.username,
buildType: transformBuildType(job.buildType),
buildType: job.buildType && transformBuildType(job.buildType),
};
}
function transformBuildType(buildType: Android.ManagedBuildType): AndroidManagedBuildType {
if (buildType === Android.ManagedBuildType.APK) {
return AndroidManagedBuildType.Apk;
} else if (buildType === Android.ManagedBuildType.APP_BUNDLE) {
return AndroidManagedBuildType.AppBundle;
function transformBuildType(buildType: Android.BuildType): AndroidBuildType {
if (buildType === Android.BuildType.APK) {
return AndroidBuildType.Apk;
} else if (buildType === Android.BuildType.APP_BUNDLE) {
return AndroidBuildType.AppBundle;
} else {
return AndroidManagedBuildType.DevelopmentClient;
return AndroidBuildType.DevelopmentClient;
}
}
@@ -1,54 +1,32 @@
import { Android, ArchiveSource, Cache, Job, Workflow, sanitizeJob } from '@expo/eas-build-job';
import { AndroidGenericBuildProfile, AndroidManagedBuildProfile } from '@expo/eas-json';
import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
import { AndroidBuildProfile } from '@expo/eas-json';
import path from 'path';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { readEnvironmentSecretsAsync } from '../../credentials/credentialsJson/read';
import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { gitRootDirectoryAsync } from '../../utils/git';
import vcs from '../../vcs';
import { BuildContext } from '../context';
import { Platform } from '../types';
interface JobData {
projectArchive: ArchiveSource;
credentials?: AndroidCredentials;
}
const cacheDefaults = {
disabled: false,
customPaths: [],
cacheDefaultPaths: true,
};
export async function prepareJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Job> {
if (ctx.buildProfile.workflow === Workflow.GENERIC) {
const partialJob = await prepareGenericJobAsync(ctx, jobData, ctx.buildProfile);
return sanitizeJob(partialJob);
} else if (ctx.buildProfile.workflow === Workflow.MANAGED) {
const partialJob = await prepareManagedJobAsync(ctx, jobData, ctx.buildProfile);
return sanitizeJob(partialJob);
} else {
throw new Error("Unknown workflow. Shouldn't happen");
}
}
interface CommonJobProperties {
platform: Platform.ANDROID;
projectArchive: ArchiveSource;
builderEnvironment: Android.BuilderEnvironment;
cache: Cache;
secrets: {
buildCredentials?: {
keystore: Android.Keystore;
};
environmentSecrets?: Record<string, string>;
};
}
async function prepareJobCommonAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Partial<CommonJobProperties>> {
const environmentSecrets = await readEnvironmentSecretsAsync(ctx.commandCtx.projectDir);
const credentials = jobData.credentials;
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildProfile: AndroidBuildProfile = ctx.buildProfile;
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
const { credentials } = jobData;
const buildCredentials = credentials
? {
buildCredentials: {
@@ -62,59 +40,41 @@ async function prepareJobCommonAsync(
}
: {};
return {
let buildType = ctx.buildProfile.buildType;
if (!buildType && !buildProfile.gradleCommand && ctx.buildProfile.distribution === 'internal') {
buildType = Android.BuildType.APK;
}
const job: Android.Job = {
type: ctx.workflow,
platform: Platform.ANDROID,
projectRootDirectory,
projectArchive: jobData.projectArchive,
builderEnvironment: {
image: ctx.buildProfile.image,
node: ctx.buildProfile.node,
yarn: ctx.buildProfile.yarn,
ndk: ctx.buildProfile.ndk,
expoCli: ctx.buildProfile.expoCli,
env: ctx.buildProfile.env,
image: buildProfile.image ?? 'default',
node: buildProfile.node,
yarn: buildProfile.yarn,
ndk: buildProfile.ndk,
expoCli: buildProfile.expoCli,
env: buildProfile.env,
},
cache: {
...ctx.buildProfile.cache,
clear: ctx.commandCtx.clearCache,
...cacheDefaults,
...buildProfile.cache,
clear: ctx.clearCache,
},
secrets: {
...(environmentSecrets ? { environmentSecrets } : {}),
...buildCredentials,
},
};
}
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
async function prepareGenericJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData,
buildProfile: AndroidGenericBuildProfile
): Promise<Partial<Android.GenericJob>> {
const projectRootDirectory =
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
return {
...(await prepareJobCommonAsync(ctx, jobData)),
type: Workflow.GENERIC,
gradleCommand: buildProfile.gradleCommand,
artifactPath: buildProfile.artifactPath,
releaseChannel: buildProfile.releaseChannel,
projectRootDirectory,
};
}
async function prepareManagedJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData,
buildProfile: AndroidManagedBuildProfile
): Promise<Partial<Android.ManagedJob>> {
const projectRootDirectory =
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
return {
...(await prepareJobCommonAsync(ctx, jobData)),
type: Workflow.MANAGED,
username,
buildType: buildProfile.buildType,
releaseChannel: buildProfile.releaseChannel,
projectRootDirectory,
buildType: buildProfile.developmentClient ? Android.BuildType.DEVELOPMENT_CLIENT : buildType,
};
return sanitizeJob(job);
}
+12 -6
View File
@@ -3,10 +3,13 @@ import { AndroidConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import fs from 'fs-extra';
import { resolveWorkflow } from '../../project/workflow';
import { resolveWorkflowAsync } from '../../project/workflow';
export function readVersionCode(projectDir: string, exp: ExpoConfig): number | undefined {
const workflow = resolveWorkflow(projectDir, Platform.ANDROID);
export async function readVersionCodeAsync(
projectDir: string,
exp: ExpoConfig
): Promise<number | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
const buildGradle = readBuildGradle(projectDir);
const matchResult = buildGradle?.match(/versionCode (.*)/);
@@ -16,12 +19,15 @@ export function readVersionCode(projectDir: string, exp: ExpoConfig): number | u
return undefined;
}
} else {
return AndroidConfig.Version.getVersionCode(exp) ?? undefined;
return AndroidConfig.Version.getVersionCode(exp) ?? 1;
}
}
export function readVersionName(projectDir: string, exp: ExpoConfig): string | undefined {
const workflow = resolveWorkflow(projectDir, Platform.ANDROID);
export async function readVersionNameAsync(
projectDir: string,
exp: ExpoConfig
): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
const buildGradle = readBuildGradle(projectDir);
const matchResult = buildGradle?.match(/versionName ["'](.*)["']/);
+151 -40
View File
@@ -1,28 +1,28 @@
import { ArchiveSource, ArchiveSourceType, Job, Metadata } from '@expo/eas-build-job';
import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import ora from 'ora';
import { UploadSessionType } from '../graphql/generated';
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
import { BuildResult } from '../graphql/mutations/BuildMutation';
import Log from '../log';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log, { learnMore } from '../log';
import { promptAsync } from '../prompts';
import { uploadAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
import { sleep } from '../utils/promise';
import vcs from '../vcs';
import { requestedPlatformDisplayNames } from './constants';
import { BuildContext } from './context';
import { runLocalBuildAsync } from './local';
import { collectMetadata } from './metadata';
import { Platform, TrackingContext } from './types';
import { MetadataContext, collectMetadata } from './metadata';
import { 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;
@@ -41,72 +41,85 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
ctx: BuildContext<TPlatform>
): Promise<CredentialsResult<Credentials> | undefined>;
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
getMetadataContext: () => MetadataContext<TPlatform>;
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
}
export type BuildRequestSender = () => Promise<BuildFragment | undefined>;
export async function prepareBuildRequestForPlatformAsync<
TPlatform extends Platform,
Credentials,
TJob extends Job
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<() => Promise<string | undefined>> {
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
const { ctx } = builder;
const credentialsResult = await withAnalyticsAsync(
async () => await builder.ensureCredentialsAsync(builder.ctx),
async () => await builder.ensureCredentialsAsync(ctx),
{
successEvent: Event.GATHER_CREDENTIALS_SUCCESS,
failureEvent: Event.GATHER_CREDENTIALS_FAIL,
trackingCtx: builder.ctx.trackingCtx,
trackingCtx: ctx.trackingCtx,
}
);
if (!builder.ctx.commandCtx.skipProjectConfiguration) {
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(builder.ctx), {
if (!ctx.skipProjectConfiguration) {
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(ctx), {
successEvent: Event.CONFIGURE_PROJECT_SUCCESS,
failureEvent: Event.CONFIGURE_PROJECT_FAIL,
trackingCtx: builder.ctx.trackingCtx,
trackingCtx: ctx.trackingCtx,
});
}
if (!(await isGitStatusCleanAsync())) {
if (await vcs.hasUncommittedChangesAsync()) {
Log.addNewLineIfNone();
await reviewAndCommitChangesAsync(
`[EAS Build] Run EAS Build for ${
requestedPlatformDisplayNames[builder.ctx.platform as Platform]
}`,
{
nonInteractive: builder.ctx.commandCtx.nonInteractive,
}
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
{ nonInteractive: ctx.nonInteractive }
);
}
const projectArchive = builder.ctx.commandCtx.local
const projectArchive = ctx.local
? ({
type: ArchiveSourceType.PATH,
path: (await makeProjectTarballAsync()).path,
} as const)
: ({
type: ArchiveSourceType.S3,
bucketKey: await uploadProjectAsync(builder.ctx),
bucketKey: await uploadProjectAsync(ctx),
} as const);
const metadata = await collectMetadata(builder.ctx, {
credentialsSource: credentialsResult?.source,
});
const job = await builder.prepareJobAsync(builder.ctx, {
const metadataContext = builder.getMetadataContext();
const metadata = await collectMetadata(ctx, metadataContext);
const job = await builder.prepareJobAsync(ctx, {
projectArchive,
credentials: credentialsResult?.credentials,
});
return async () => {
if (builder.ctx.commandCtx.local) {
if (ctx.local) {
await runLocalBuildAsync(job);
return undefined;
} else {
try {
return await sendBuildRequestAsync(builder, job, metadata);
} catch (error) {
} catch (error: any) {
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli version.');
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
) {
Log.error(
'EAS Build is down for maintenance, please try again later. Check https://status.expo.dev/ for updates.'
);
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED'
) {
Log.error(
'EAS Build free tier is temporarily disabled, please try again later. Check https://status.expo.dev/ for updates.'
);
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
) {
@@ -146,7 +159,10 @@ async function uploadProjectAsync<TPlatform extends Platform>(
`Uploading to EAS Build (${formatBytes(projectTarball.size * ratio)} / ${formatBytes(
projectTarball.size
)})`,
completedMessage: `Uploaded to EAS`,
completedMessage: (duration: string) =>
`Uploaded to EAS ${chalk.dim(duration)} ${learnMore(
'https://expo.fyi/eas-build-archive'
)}`,
})
);
return bucketKey;
@@ -168,7 +184,7 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
builder: Builder<TPlatform, Credentials, TJob>,
job: TJob,
metadata: Metadata
): Promise<string> {
): Promise<BuildFragment> {
const { ctx } = builder;
return await withAnalyticsAsync(
async () => {
@@ -177,13 +193,13 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
}
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
ctx.commandCtx.projectId,
ctx.projectId,
job,
metadata
);
printDeprecationWarnings(deprecationInfo);
return build.id;
return build;
},
{
successEvent: Event.BUILD_REQUEST_SUCCESS,
@@ -205,7 +221,7 @@ async function withAnalyticsAsync<Result>(
const result = await fn();
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
return result;
} catch (error) {
} catch (error: any) {
Analytics.logEvent(analytics.failureEvent, {
...analytics.trackingCtx,
reason: error.message,
@@ -225,7 +241,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;
}
@@ -258,10 +274,105 @@ 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,
});
}
}
export async function waitForBuildEndAsync(
buildIds: string[],
{ timeoutSec = 3600, intervalSec = 30 } = {}
): Promise<(BuildFragment | null)[]> {
Log.log(
`Waiting for build${buildIds.length > 1 ? 's' : ''} to complete. You can press Ctrl+C to exit.`
);
const spinner = ora().start();
let time = new Date().getTime();
const endTime = time + timeoutSec * 1000;
while (time <= endTime) {
const builds: (BuildFragment | null)[] = await Promise.all(
buildIds.map(async buildId => {
try {
return await BuildQuery.byIdAsync(buildId, { useCache: false });
} catch (err) {
return null;
}
})
);
if (builds.length === 1) {
const build = nullthrows(builds[0]);
switch (build.status) {
case BuildStatus.Finished:
spinner.succeed('Build finished');
return builds;
case BuildStatus.New:
spinner.text = 'Build created';
break;
case BuildStatus.InQueue:
spinner.text = 'Build queued...';
break;
case BuildStatus.Canceled:
spinner.text = 'Build canceled';
spinner.stopAndPersist();
return builds;
case BuildStatus.InProgress:
spinner.text = 'Build in progress...';
break;
case BuildStatus.Errored:
spinner.fail('Build failed');
if (build.error) {
return builds;
} else {
throw new Error(`Standalone build failed!`);
}
default:
spinner.warn('Unknown status.');
throw new Error(`Unknown status: ${builds} - aborting!`);
}
} else {
if (builds.filter(build => build?.status === BuildStatus.Finished).length === builds.length) {
spinner.succeed('All builds have finished');
return builds;
} else if (
builds.filter(build =>
build?.status
? [BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(
build.status
)
: false
).length === builds.length
) {
spinner.fail('Some of the builds were canceled or failed.');
return builds;
} else {
const newBuilds = builds.filter(build => build?.status === BuildStatus.New).length;
const inQueue = builds.filter(build => build?.status === BuildStatus.InQueue).length;
const inProgress = builds.filter(build => build?.status === BuildStatus.InProgress).length;
const errored = builds.filter(build => build?.status === BuildStatus.Errored).length;
const finished = builds.filter(build => build?.status === BuildStatus.Finished).length;
const canceled = builds.filter(build => build?.status === BuildStatus.Canceled).length;
const unknownState = builds.length - newBuilds - inQueue - inProgress - errored - finished;
spinner.text = [
newBuilds && `Builds created: ${newBuilds}`,
inQueue && `Builds in queue: ${inQueue}`,
inProgress && `Builds in progress: ${inProgress}`,
canceled && `Builds canceled: ${canceled}`,
errored && chalk.red(`Builds failed: ${errored}`),
finished && chalk.green(`Builds finished: ${finished}`),
unknownState && chalk.red(`Builds in unknown state: ${unknownState}`),
]
.filter(i => i)
.join('\t');
}
}
time = new Date().getTime();
await sleep(intervalSec * 1000);
}
spinner.warn('Timed out');
throw new Error(
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
);
}
+89 -67
View File
@@ -1,23 +1,20 @@
import { getConfig } from '@expo/config';
import { EasJsonReader } from '@expo/eas-json';
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJson, EasJsonReader } from '@expo/eas-json';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import Log from '../log';
import { promptAsync } from '../prompts';
import Log, { learnMore } from '../log';
import { resolveWorkflowAsync } from '../project/workflow';
import { confirmAsync, promptAsync } from '../prompts';
import { ensureLoggedInAsync } from '../user/actions';
import { 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',
@@ -25,13 +22,43 @@ const configureCommitMessage = {
[RequestedPlatform.All]: 'Configure EAS Build',
};
export async function ensureProjectConfiguredAsync(
projectDir: string,
requestedPlatform: RequestedPlatform
): Promise<void> {
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir))) {
return;
}
const message = 'This project is not configured to build with EAS. Set it up now?';
const confirm = await confirmAsync({ message });
if (confirm) {
await configureAsync({
projectDir,
platform: requestedPlatform,
});
if (await vcs.hasUncommittedChangesAsync()) {
error(
'Build process requires clean working tree, please commit all your changes and run `eas build` again',
{ exit: 1 }
);
}
} else {
error(
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
'https://docs.expo.dev/build/eas-json'
)})`,
{ exit: 1 }
);
}
}
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,14 +66,15 @@ 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
),
shouldConfigureIos: [RequestedPlatform.All, RequestedPlatform.Ios].includes(options.platform),
hasAndroidNativeProject: await fs.pathExists(path.join(options.projectDir, 'android')),
hasIosNativeProject: await fs.pathExists(path.join(options.projectDir, 'ios')),
hasAndroidNativeProject:
(await resolveWorkflowAsync(options.projectDir, Platform.ANDROID)) === Workflow.GENERIC,
hasIosNativeProject:
(await resolveWorkflowAsync(options.projectDir, Platform.IOS)) === Workflow.GENERIC,
};
Log.newLine();
@@ -58,7 +86,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,62 +95,56 @@ export async function configureAsync(options: {
}
}
const EAS_JSON_MANAGED_DEFAULT: EasJson = {
build: {
release: {},
development: {
developmentClient: true,
distribution: 'internal',
},
},
submit: {
release: {},
},
};
const EAS_JSON_GENERIC_DEFAULT: EasJson = {
build: {
release: {},
development: {
distribution: 'internal',
android: {
gradleCommand: ':app:assembleDebug',
},
ios: {
buildConfiguration: 'Debug',
},
},
},
submit: {
release: {},
},
};
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
const easJsonPath = path.join(ctx.projectDir, 'eas.json');
let existingEasJson;
const easJsonPath = EasJsonReader.formatEasJsonPath(ctx.projectDir);
if (await fs.pathExists(easJsonPath)) {
const reader = new EasJsonReader(ctx.projectDir, ctx.requestedPlatform);
await reader.validateAsync();
const reader = new EasJsonReader(ctx.projectDir);
await reader.readAndValidateAsync();
existingEasJson = await reader.readRawAsync();
Log.withTick('Validated eas.json');
// If we have already populated eas.json with the default fields for the
// platform then proceed
if (ctx.requestedPlatform !== 'all' && existingEasJson.builds[ctx.requestedPlatform]) {
return;
} else if (
ctx.requestedPlatform === 'all' &&
existingEasJson.builds.ios &&
existingEasJson.builds.android
) {
return;
}
return;
}
const shouldInitIOS =
['all', 'ios'].includes(ctx.requestedPlatform) && !existingEasJson?.builds.ios;
const shouldInitAndroid =
['all', 'android'].includes(ctx.requestedPlatform) && !existingEasJson?.builds.android;
const easJson = {
builds: {
...existingEasJson,
...(shouldInitAndroid
? {
android: {
release: {
workflow: ctx.hasAndroidNativeProject ? 'generic' : 'managed',
},
},
}
: null),
...(shouldInitIOS
? {
ios: {
release: {
workflow: ctx.hasIosNativeProject ? 'generic' : 'managed',
},
},
}
: null),
},
};
const easJson =
ctx.hasAndroidNativeProject && ctx.hasIosNativeProject
? EAS_JSON_GENERIC_DEFAULT
: EAS_JSON_MANAGED_DEFAULT;
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
await gitAddAsync(easJsonPath, { intentToAdd: true });
Log.withTick(`${existingEasJson ? 'Updated' : 'Generated'} eas.json`);
await vcs.trackFileAsync(easJsonPath);
Log.withTick('Generated eas.json');
}
enum ShouldCommitChanges {
@@ -155,7 +177,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);
}
}
-11
View File
@@ -1,4 +1,3 @@
import { AppPlatform } from '../graphql/generated';
import { RequestedPlatform } from './types';
export const requestedPlatformDisplayNames: Record<RequestedPlatform, string> = {
@@ -6,13 +5,3 @@ export const requestedPlatformDisplayNames: Record<RequestedPlatform, string> =
[RequestedPlatform.Android]: 'Android',
[RequestedPlatform.All]: 'Android and iOS',
};
export const appPlatformDisplayNames: Record<AppPlatform, string> = {
[AppPlatform.Android]: 'Android',
[AppPlatform.Ios]: 'iOS',
};
export const appPlatformEmojis = {
[AppPlatform.Ios]: '🍎',
[AppPlatform.Android]: '🤖',
};
+106 -92
View File
@@ -1,83 +1,23 @@
import { ExpoConfig } from '@expo/config';
import { AndroidBuildProfile, EasConfig, IosBuildProfile } from '@expo/eas-json';
import { Platform, Workflow } from '@expo/eas-build-job';
import { AndroidBuildProfile, BuildProfile, IosBuildProfile } from '@expo/eas-json';
import JsonFile from '@expo/json-file';
import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
import { getProjectAccountName } from '../project/projectUtils';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { findAccountByName } from '../user/Account';
import { Actor } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
import { Platform, RequestedPlatform, TrackingContext } from './types';
import { RequestedPlatform, TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
export interface CommandContext {
requestedPlatform: RequestedPlatform;
profile: string;
projectDir: string;
projectId: string;
user: Actor;
accountName: string;
projectName: string;
exp: ExpoConfig;
nonInteractive: boolean;
local: boolean;
clearCache: boolean;
skipCredentialsCheck: boolean;
skipProjectConfiguration: boolean;
waitForBuildEnd: boolean;
}
export async function createCommandContextAsync({
requestedPlatform,
profile,
exp,
projectDir,
projectId,
nonInteractive = false,
local,
clearCache = false,
skipCredentialsCheck = false,
skipProjectConfiguration = false,
waitForBuildEnd,
}: {
requestedPlatform: RequestedPlatform;
profile: string;
exp: ExpoConfig;
projectId: string;
projectDir: string;
nonInteractive: boolean;
local: boolean;
clearCache: boolean;
skipCredentialsCheck: boolean;
skipProjectConfiguration: boolean;
waitForBuildEnd: boolean;
}): Promise<CommandContext> {
const user = await ensureLoggedInAsync();
const accountName = getProjectAccountName(exp, user);
const projectName = exp.slug;
return {
requestedPlatform,
profile,
projectDir,
projectId,
user,
accountName,
projectName,
exp,
nonInteractive,
local,
clearCache,
skipCredentialsCheck,
skipProjectConfiguration,
waitForBuildEnd,
};
}
export interface ConfigureContext {
user: Actor;
projectDir: string;
exp: ExpoConfig;
allowExperimental: boolean;
requestedPlatform: RequestedPlatform;
shouldConfigureAndroid: boolean;
shouldConfigureIos: boolean;
@@ -85,45 +25,119 @@ export interface ConfigureContext {
hasIosNativeProject: boolean;
}
type PlatformBuildProfile<T extends Platform> = T extends Platform.ANDROID
? AndroidBuildProfile
: IosBuildProfile;
export interface BuildContext<T extends Platform> {
commandCtx: CommandContext;
trackingCtx: TrackingContext;
accountName: string;
buildProfile: BuildProfile<T>;
buildProfileName: string;
clearCache: boolean;
exp: ExpoConfig;
local: boolean;
nonInteractive: boolean;
platform: T;
buildProfile: PlatformBuildProfile<T>;
projectDir: string;
projectId: string;
projectName: string;
skipProjectConfiguration: boolean;
trackingCtx: TrackingContext;
user: Actor;
workflow: Workflow;
}
export function createBuildContext<T extends Platform>({
export async function createBuildContextAsync<T extends Platform>({
buildProfileName,
buildProfile,
clearCache = false,
local,
nonInteractive = false,
platform,
easConfig,
commandCtx,
projectDir,
skipProjectConfiguration = false,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
clearCache: boolean;
local: boolean;
nonInteractive: boolean;
platform: T;
easConfig: EasConfig;
commandCtx: CommandContext;
}): BuildContext<T> {
const buildProfile = easConfig.builds[platform] as PlatformBuildProfile<T> | undefined;
if (!buildProfile) {
throw new Error(`${platform} build profile does not exist`);
}
projectDir: string;
skipProjectConfiguration: boolean;
}): Promise<BuildContext<T>> {
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
const accountId = findAccountByName(commandCtx.user.accounts, commandCtx.accountName)?.id;
const user = await ensureLoggedInAsync();
const accountName = getProjectAccountName(exp, user);
const projectName = exp.slug;
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
const workflow = await resolveWorkflowAsync(projectDir, platform);
const accountId = findAccountByName(user.accounts, accountName)?.id;
const devClientProperties = getDevClientEventProperties({
platform,
projectDir,
buildProfile,
});
const trackingCtx = {
tracking_id: uuidv4(),
platform,
...(accountId && { account_id: accountId }),
account_name: commandCtx.accountName,
project_id: commandCtx.projectId,
project_type: buildProfile.workflow,
account_name: accountName,
project_id: projectId,
project_type: workflow,
...devClientProperties,
};
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
return {
commandCtx,
trackingCtx,
platform,
accountName,
buildProfile,
buildProfileName,
clearCache,
exp,
local,
nonInteractive,
platform,
projectDir,
projectId,
projectName,
skipProjectConfiguration,
trackingCtx,
user,
workflow,
};
}
function getDevClientEventProperties({
platform,
projectDir,
buildProfile,
}: {
platform: Platform;
projectDir: string;
buildProfile: AndroidBuildProfile | IosBuildProfile;
}): Partial<TrackingContext> {
let includesDevClient;
const version = tryGetDevClientVersion(projectDir);
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
} else if (platform === Platform.IOS && 'buildConfiguration' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.buildConfiguration === 'Debug');
} else if (buildProfile.developmentClient) {
includesDevClient = true;
} 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;
}
}
-167
View File
@@ -1,167 +0,0 @@
import { EasJsonReader } from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import ora from 'ora';
import { BuildFragment, BuildStatus } from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log from '../log';
import { sleep } from '../utils/promise';
import { 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';
export async function buildAsync(commandCtx: CommandContext): Promise<void> {
await ensureGitRepoExistsAsync();
await ensureGitStatusIsCleanAsync(commandCtx.nonInteractive);
const scheduledBuilds = await startBuildsAsync(commandCtx);
if (commandCtx.local) {
return;
}
Log.newLine();
printLogsUrls(commandCtx.accountName, scheduledBuilds);
Log.newLine();
if (commandCtx.waitForBuildEnd) {
const builds = await waitForBuildEndAsync(scheduledBuilds.map(i => i.buildId));
printBuildResults(commandCtx.accountName, builds);
exitWithNonZeroCodeIfSomeBuildsFailed(builds);
}
}
async function startBuildsAsync(
commandCtx: CommandContext
): Promise<{ platform: Platform; buildId: string }[]> {
const shouldBuildAndroid = [RequestedPlatform.Android, RequestedPlatform.All].includes(
commandCtx.requestedPlatform
);
const shouldBuildIos = [RequestedPlatform.Ios, RequestedPlatform.All].includes(
commandCtx.requestedPlatform
);
const easConfig = await new EasJsonReader(
commandCtx.projectDir,
commandCtx.requestedPlatform
).readAsync(commandCtx.profile);
const builds: {
platform: Platform;
sendBuildRequestAsync: () => Promise<string | undefined>;
}[] = [];
if (shouldBuildAndroid) {
const sendBuildRequestAsync = await prepareAndroidBuildAsync(commandCtx, easConfig);
builds.push({ platform: Platform.ANDROID, sendBuildRequestAsync });
}
if (shouldBuildIos) {
const sendBuildRequestAsync = await prepareIosBuildAsync(commandCtx, easConfig);
builds.push({ platform: Platform.IOS, sendBuildRequestAsync });
}
return (
await Promise.all(
builds.map(async ({ platform, sendBuildRequestAsync }) => ({
platform,
buildId: await sendBuildRequestAsync(),
}))
)
).filter((build): build is { platform: Platform; buildId: string } => !!build.buildId);
}
async function waitForBuildEndAsync(
buildIds: string[],
{ timeoutSec = 3600, intervalSec = 30 } = {}
): Promise<(BuildFragment | null)[]> {
Log.log('Waiting for build to complete. You can press Ctrl+C to exit.');
const spinner = ora().start();
let time = new Date().getTime();
const endTime = time + timeoutSec * 1000;
while (time <= endTime) {
const builds: (BuildFragment | null)[] = await Promise.all(
buildIds.map(async buildId => {
try {
return await BuildQuery.byIdAsync(buildId, { useCache: false });
} catch (err) {
return null;
}
})
);
if (builds.length === 1) {
const build = nullthrows(builds[0]);
switch (build.status) {
case BuildStatus.Finished:
spinner.succeed('Build finished');
return builds;
case BuildStatus.InQueue:
spinner.text = 'Build queued...';
break;
case BuildStatus.Canceled:
spinner.text = 'Build canceled';
spinner.stopAndPersist();
return builds;
case BuildStatus.InProgress:
spinner.text = 'Build in progress...';
break;
case BuildStatus.Errored:
spinner.fail('Build failed');
if (build.error) {
return builds;
} else {
throw new Error(`Standalone build failed!`);
}
default:
spinner.warn('Unknown status.');
throw new Error(`Unknown status: ${builds} - aborting!`);
}
} else {
if (builds.filter(build => build?.status === BuildStatus.Finished).length === builds.length) {
spinner.succeed('All builds have finished');
return builds;
} else if (
builds.filter(build =>
build?.status
? [BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(
build.status
)
: false
).length === builds.length
) {
spinner.fail('Some of the builds were canceled or failed.');
return builds;
} else {
const inQueue = builds.filter(build => build?.status === BuildStatus.InQueue).length;
const inProgress = builds.filter(build => build?.status === BuildStatus.InProgress).length;
const errored = builds.filter(build => build?.status === BuildStatus.Errored).length;
const finished = builds.filter(build => build?.status === BuildStatus.Finished).length;
const canceled = builds.filter(build => build?.status === BuildStatus.Canceled).length;
const unknownState = builds.length - inQueue - inProgress - errored - finished;
spinner.text = [
inQueue && `Builds in queue: ${inQueue}`,
inProgress && `Builds in progress: ${inProgress}`,
canceled && `Builds canceled: ${canceled}`,
errored && chalk.red(`Builds failed: ${errored}`),
finished && chalk.green(`Builds finished: ${finished}`),
unknownState && chalk.red(`Builds in unknown state: ${unknownState}`),
]
.filter(i => i)
.join('\t');
}
}
time = new Date().getTime();
await sleep(intervalSec * 1000);
}
spinner.warn('Timed out');
throw new Error(
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
);
}
function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
i => i.status === BuildStatus.Errored
);
if (failedBuilds.length > 0) {
process.exit(1);
}
}
+15 -1
View File
@@ -2,6 +2,7 @@ import { ArchiveSource, ArchiveSourceType, Metadata, Workflow } from '@expo/eas-
import {
BuildCredentialsSource,
BuildIosEnterpriseProvisioning,
BuildMetadataInput,
BuildWorkflow,
DistributionType,
@@ -32,6 +33,9 @@ export function transformMetadata(metadata: Metadata): BuildMetadataInput {
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
distribution: metadata.distribution && transformDistribution(metadata.distribution),
workflow: metadata.workflow && transformWorkflow(metadata.workflow),
iosEnterpriseProvisioning:
metadata.iosEnterpriseProvisioning &&
transformIosEnterpriseProvisioning(metadata.iosEnterpriseProvisioning),
};
}
@@ -55,10 +59,20 @@ function transformDistribution(distribution: Metadata['distribution']): Distribu
}
}
function transformWorkflow(workflow: Metadata['workflow']): BuildWorkflow {
export function transformWorkflow(workflow: Workflow): BuildWorkflow {
if (workflow === Workflow.GENERIC) {
return BuildWorkflow.Generic;
} else {
return BuildWorkflow.Managed;
}
}
export function transformIosEnterpriseProvisioning(
enterpriseProvisioning: Metadata['iosEnterpriseProvisioning']
): BuildIosEnterpriseProvisioning {
if (enterpriseProvisioning === 'adhoc') {
return BuildIosEnterpriseProvisioning.Adhoc;
} else {
return BuildIosEnterpriseProvisioning.Universal;
}
}
@@ -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';
@@ -75,7 +75,7 @@ async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig)
async function readExpoPlistAsync(projectDir: string): Promise<IOSConfig.ExpoPlist> {
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
return await readPlistAsync(expoPlistPath);
return (await readPlistAsync(expoPlistPath)) as IOSConfig.ExpoPlist;
}
async function writeExpoPlistAsync(
@@ -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;
}
}
@@ -1,3 +0,0 @@
export function getBundleIdentifier(): string {
return 'com.bundle.id';
}
@@ -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>
`;
@@ -15,12 +15,7 @@ import {
jest.mock('fs');
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.removeSync(os.tmpdir());
@@ -154,7 +149,7 @@ describe(readBuildNumberAsync, () => {
describe('generic project', () => {
it('reads the build number from native code', async () => {
const exp = initGenericProject();
const buildNumber = await readBuildNumberAsync('/repo', exp);
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
expect(buildNumber).toBe('1');
});
});
@@ -162,7 +157,7 @@ describe(readBuildNumberAsync, () => {
describe('managed project', () => {
it('reads the build number from expo config', async () => {
const exp = initManagedProject();
const buildNumber = await readBuildNumberAsync('/repo', exp);
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
expect(buildNumber).toBe('1');
});
});
@@ -172,21 +167,33 @@ describe(readShortVersionAsync, () => {
describe('generic project', () => {
it('reads the short version from native code', async () => {
const exp = initGenericProject();
const shortVersion = await readShortVersionAsync('/repo', exp);
const shortVersion = await readShortVersionAsync('/repo', exp, {});
expect(shortVersion).toBe('1.0.0');
});
it('evaluates interpolated build number', async () => {
const exp = initGenericProject({
shortVersion: '$(CURRENT_PROJECT_VERSION)',
});
const buildNumber = await readShortVersionAsync('/repo', exp, {
CURRENT_PROJECT_VERSION: '1.0.0',
});
expect(buildNumber).toBe('1.0.0');
});
});
describe('managed project', () => {
it('reads the version from app config', async () => {
const exp = initGenericProject();
const shortVersion = await readShortVersionAsync('/repo', exp);
const shortVersion = await readShortVersionAsync('/repo', exp, {});
expect(shortVersion).toBe('1.0.0');
});
});
});
function initGenericProject(): ExpoConfig {
function initGenericProject({
shortVersion = '1.0.0',
version = '1',
}: { shortVersion?: string; version?: string } = {}): ExpoConfig {
vol.fromJSON(
{
'./app.json': JSON.stringify({
@@ -202,9 +209,9 @@ function initGenericProject(): ExpoConfig {
<plist version="1.0">
<dict>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<string>${shortVersion}</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>${version}</string>
</dict>
</plist>`,
},
+48 -128
View File
@@ -1,76 +1,74 @@
import { IOSConfig } from '@expo/config-plugins';
import { Ios, Job, Metadata, Workflow } from '@expo/eas-build-job';
import { EasConfig } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import sortBy from 'lodash/sortBy';
import path from 'path';
import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { IosCredentials } from '../../credentials/ios/types';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import Log from '../../log';
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
import { XcodeBuildContext, resolveTargetsAsync } from '../../project/ios/target';
import { sanitizedProjectName } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { JobData, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommandContext, createBuildContext } from '../context';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext } from '../context';
import { transformMetadata } from '../graphql';
import { Platform } from '../types';
import { IosMetadataContext } from '../metadata';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { ensureIosCredentialsAsync } from './credentials';
import { transformGenericJob, transformManagedJob } from './graphql';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
export async function prepareIosBuildAsync(
commandCtx: CommandContext,
easConfig: EasConfig
): Promise<() => Promise<string | undefined>> {
const buildCtx = createBuildContext<Platform.IOS>({
commandCtx,
platform: Platform.IOS,
easConfig,
});
ctx: BuildContext<Platform.IOS>
): Promise<BuildRequestSender> {
const { buildProfile } = ctx;
if (
buildCtx.buildProfile.workflow === Workflow.GENERIC &&
!(await fs.pathExists(path.join(commandCtx.projectDir, 'ios')))
) {
throw new Error(
`"ios" directory not found. If you're trying to build a managed project, set ${chalk.bold(
`builds.ios.${commandCtx.profile}.workflow`
)} in "eas.json" to "managed".`
);
if (ctx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
}
if (buildCtx.buildProfile.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(
commandCtx.projectDir,
commandCtx.exp
);
}
checkNodeEnvVariable(ctx);
await checkGoogleServicesFileAsync(ctx);
const xcodeBuildContext = await resolveXcodeBuildContextAsync(buildCtx);
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
projectDir: ctx.projectDir,
nonInteractive: ctx.nonInteractive,
exp: ctx.exp,
},
buildProfile
);
const targets = await resolveTargetsAsync(
{
projectDir: commandCtx.projectDir,
exp: commandCtx.exp,
projectDir: ctx.projectDir,
exp: ctx.exp,
},
xcodeBuildContext
);
return await prepareBuildRequestForPlatformAsync({
ctx: buildCtx,
ctx,
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
return ensureIosCredentialsAsync(ctx, targets);
},
ensureProjectConfiguredAsync: async () => {
await validateAndSyncProjectConfigurationAsync({
projectDir: commandCtx.projectDir,
exp: commandCtx.exp,
buildProfile: buildCtx.buildProfile,
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile,
});
},
getMetadataContext: (): IosMetadataContext => {
if (ctx.workflow === Workflow.MANAGED) {
return { buildSettings: {} };
}
const applicationTarget = findApplicationTarget(targets);
const project = IOSConfig.XcodeUtils.getPbxproj(ctx.projectDir);
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
targetName: applicationTarget.targetName,
buildConfiguration: applicationTarget.buildConfiguration,
});
const buildSettings = xcBuildConfiguration?.buildSettings ?? {};
return { buildSettings };
},
prepareJobAsync: async (
ctx: BuildContext<Platform.IOS>,
jobData: JobData<IosCredentials>
@@ -83,90 +81,12 @@ export async function prepareIosBuildAsync(
metadata: Metadata
): Promise<BuildResult> => {
const graphqlMetadata = transformMetadata(metadata);
if (job.type === Workflow.GENERIC) {
const graphqlJob = transformGenericJob(job);
return await BuildMutation.createIosGenericBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
} else {
const graphqlJob = transformManagedJob(job);
return await BuildMutation.createIosManagedBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
}
const graphqlJob = transformJob(job);
return await BuildMutation.createIosBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
},
});
}
async function resolveXcodeBuildContextAsync(
buildCtx: BuildContext<Platform.IOS>
): Promise<XcodeBuildContext> {
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
const buildScheme = buildCtx.buildProfile.scheme ?? (await resolveSchemeAsync(buildCtx));
return {
buildScheme,
buildConfiguration:
buildCtx.buildProfile.schemeBuildConfiguration ??
(await IOSConfig.BuildScheme.getArchiveBuildConfigurationForSchemeAsync(
buildCtx.commandCtx.projectDir,
buildScheme
)),
applicationTarget: await IOSConfig.BuildScheme.getApplicationTargetNameForSchemeAsync(
buildCtx.commandCtx.projectDir,
buildScheme
),
};
} else {
const expoName = buildCtx.commandCtx.exp.name;
if (!expoName) {
throw new Error('"expo.name" is required in your app.json');
}
const sanitizedExpoName = sanitizedProjectName(expoName);
if (!sanitizedExpoName) {
throw new Error('"expo.name" needs to contain some alphanumeric characters');
}
return {
buildScheme: sanitizedExpoName,
};
}
}
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;
}
}
+5 -3
View File
@@ -1,5 +1,5 @@
import { ExpoConfig } from '@expo/config';
import { Workflow } from '@expo/eas-build-job';
import { Platform, Workflow } from '@expo/eas-build-job';
import { IosBuildProfile, VersionAutoIncrement } from '@expo/eas-json';
import Log from '../../log';
@@ -7,6 +7,7 @@ import {
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
warnIfBundleIdentifierDefinedInAppConfigForGenericProject,
} from '../../project/ios/bundleIdentifier';
import { resolveWorkflowAsync } from '../../project/workflow';
import { ConfigureContext } from '../context';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
@@ -35,8 +36,9 @@ export async function validateAndSyncProjectConfigurationAsync({
exp: ExpoConfig;
buildProfile: IosBuildProfile;
}): Promise<void> {
const { workflow, autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const { autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
if (isExpoUpdatesInstalled(projectDir)) {
+9 -20
View File
@@ -1,13 +1,12 @@
import nullthrows from 'nullthrows';
import { Platform } from '@expo/eas-build-job';
import { createCredentialsContextAsync } from '../../credentials/context';
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 { findAccountByName } from '../../user/Account';
import { CredentialsResult } from '../build';
import { BuildContext } from '../context';
import { Platform } from '../types';
import { logCredentialsSource } from '../utils/credentials';
export async function ensureIosCredentialsAsync(
@@ -18,26 +17,16 @@ export async function ensureIosCredentialsAsync(
return;
}
const { projectDir } = buildCtx.commandCtx;
const ctx = await createCredentialsContextAsync(projectDir, {
nonInteractive: buildCtx.commandCtx.nonInteractive,
const credentialsCtx = await createCredentialsContextAsync(buildCtx.projectDir, {
exp: buildCtx.exp,
nonInteractive: buildCtx.nonInteractive,
});
const provider = new IosCredentialsProvider(ctx, {
app: {
account: nullthrows(
findAccountByName(ctx.user.accounts, buildCtx.commandCtx.accountName),
`You do not have access to account: ${buildCtx.commandCtx.accountName}`
),
projectName: buildCtx.commandCtx.projectName,
},
const provider = new IosCredentialsProvider(credentialsCtx, {
app: getAppFromContext(credentialsCtx),
targets,
iosCapabilitiesOptions: {
entitlements: await resolveEntitlementsJsonAsync(
buildCtx.commandCtx.projectDir,
buildCtx.buildProfile.workflow
),
entitlements: await resolveEntitlementsJsonAsync(buildCtx.projectDir, buildCtx.workflow),
},
distribution: buildCtx.buildProfile.distribution ?? 'store',
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
@@ -53,5 +42,5 @@ export async function ensureIosCredentialsAsync(
}
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
return buildCtx.buildProfile.distribution !== 'simulator';
return !buildCtx.buildProfile.simulator;
}
+11 -20
View File
@@ -3,37 +3,28 @@ import nullthrows from 'nullthrows';
import {
DistributionType,
IosGenericJobInput,
IosBuildType,
IosJobInput,
IosJobSecretsInput,
IosManagedBuildType,
IosManagedJobInput,
} from '../../graphql/generated';
import { transformProjectArchive } from '../graphql';
import { transformProjectArchive, transformWorkflow } from '../graphql';
export function transformGenericJob(job: Ios.GenericJob): IosGenericJobInput {
export function transformJob(job: Ios.Job): IosJobInput {
return {
type: transformWorkflow(job.type),
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
updates: job.updates,
distribution: job.distribution && transformDistributionType(job.distribution),
secrets: transformIosSecrets(job.secrets),
builderEnvironment: job.builderEnvironment,
cache: job.cache,
scheme: job.scheme,
buildConfiguration: job.buildConfiguration,
artifactPath: job.artifactPath,
};
}
export function transformManagedJob(job: Ios.ManagedJob): IosManagedJobInput {
return {
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
distribution: job.distribution && transformDistributionType(job.distribution),
secrets: transformIosSecrets(job.secrets),
builderEnvironment: job.builderEnvironment,
cache: job.cache,
buildType: job.buildType && transformBuildType(job.buildType),
username: job.username,
};
@@ -71,10 +62,10 @@ function transformIosSecrets(secrets: {
};
}
function transformBuildType(buildType: Ios.ManagedBuildType): IosManagedBuildType {
if (buildType === Ios.ManagedBuildType.DEVELOPMENT_CLIENT) {
return IosManagedBuildType.DevelopmentClient;
function transformBuildType(buildType: Ios.BuildType): IosBuildType {
if (buildType === Ios.BuildType.DEVELOPMENT_CLIENT) {
return IosBuildType.DevelopmentClient;
} else {
return IosManagedBuildType.Release;
return IosBuildType.Release;
}
}
+46 -87
View File
@@ -1,14 +1,12 @@
import { ArchiveSource, Cache, Ios, Job, Workflow, sanitizeJob } from '@expo/eas-build-job';
import { IosGenericBuildProfile, IosManagedBuildProfile } from '@expo/eas-json';
import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
import path from 'path';
import semver from 'semver';
import { readEnvironmentSecretsAsync } from '../../credentials/credentialsJson/read';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { gitRootDirectoryAsync } from '../../utils/git';
import vcs from '../../vcs';
import { BuildContext } from '../context';
import { Platform } from '../types';
interface JobData {
projectArchive: ArchiveSource;
@@ -16,54 +14,34 @@ interface JobData {
buildScheme: string;
}
const cacheDefaults = {
disabled: false,
customPaths: [],
cacheDefaultPaths: true,
};
export async function prepareJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData
): Promise<Job> {
if (ctx.buildProfile.workflow === Workflow.GENERIC) {
const partialJob = await prepareGenericJobAsync(ctx, jobData, ctx.buildProfile);
return sanitizeJob(partialJob);
} else if (ctx.buildProfile.workflow === Workflow.MANAGED) {
const partialJob = await prepareManagedJobAsync(ctx, jobData, ctx.buildProfile);
return sanitizeJob(partialJob);
} else {
throw new Error("Unknown workflow. Shouldn't happen");
}
}
interface CommonJobProperties {
platform: Platform.IOS;
projectArchive: ArchiveSource;
builderEnvironment: Ios.BuilderEnvironment;
releaseChannel?: string;
distribution?: Ios.DistributionType;
cache: Cache;
secrets: {
buildCredentials: Ios.BuildCredentials;
environmentSecrets?: Record<string, string>;
};
}
async function prepareJobCommonAsync(
ctx: BuildContext<Platform.IOS>,
{ credentials, projectArchive }: { credentials?: IosCredentials; projectArchive: ArchiveSource }
): Promise<Partial<CommonJobProperties>> {
const environmentSecrets = await readEnvironmentSecretsAsync(ctx.commandCtx.projectDir);
const buildCredentials: CommonJobProperties['secrets']['buildCredentials'] = {};
if (credentials) {
const targetNames = Object.keys(credentials);
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
if (jobData.credentials) {
const targetNames = Object.keys(jobData.credentials);
for (const targetName of targetNames) {
buildCredentials[targetName] = prepareTargetCredentials(credentials[targetName]);
buildCredentials[targetName] = prepareTargetCredentials(jobData.credentials[targetName]);
}
}
return {
const job: Ios.Job = {
type: ctx.workflow,
platform: Platform.IOS,
projectArchive,
distribution: ctx.buildProfile.distribution,
projectArchive: jobData.projectArchive,
projectRootDirectory,
distribution: ctx.buildProfile.simulator ? 'simulator' : ctx.buildProfile.distribution,
builderEnvironment: {
image: ctx.buildProfile.image,
image: resolveImage(ctx),
node: ctx.buildProfile.node,
yarn: ctx.buildProfile.yarn,
bundler: ctx.buildProfile.bundler,
@@ -73,14 +51,37 @@ async function prepareJobCommonAsync(
env: ctx.buildProfile.env,
},
cache: {
...cacheDefaults,
...ctx.buildProfile.cache,
clear: ctx.commandCtx.clearCache,
clear: ctx.clearCache,
},
secrets: {
...(environmentSecrets ? { environmentSecrets } : {}),
buildCredentials,
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
scheme: jobData.buildScheme,
buildConfiguration: ctx.buildProfile.buildConfiguration,
artifactPath: ctx.buildProfile.artifactPath,
buildType: ctx.buildProfile.developmentClient
? Ios.BuildType.DEVELOPMENT_CLIENT
: Ios.BuildType.RELEASE,
username,
};
return sanitizeJob(job);
}
function resolveImage(ctx: BuildContext<Platform.IOS>): Ios.BuilderEnvironment['image'] {
// see https://linear.app/expo/issue/ENG-1396/make-default-image-dependent-on-sdk-version
if (!ctx.buildProfile.image && ctx.exp.sdkVersion) {
const majorSdkVersion = semver.major(ctx.exp.sdkVersion);
if (majorSdkVersion <= 41) {
return 'macos-catalina-10.15-xcode-12.4';
}
}
return ctx.buildProfile.image ?? 'default';
}
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
@@ -92,45 +93,3 @@ function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.Tar
},
};
}
async function prepareGenericJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData,
buildProfile: IosGenericBuildProfile
): Promise<Partial<Ios.GenericJob>> {
const projectRootDirectory =
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
return {
...(await prepareJobCommonAsync(ctx, {
credentials: jobData.credentials,
projectArchive: jobData.projectArchive,
})),
type: Workflow.GENERIC,
scheme: jobData.buildScheme,
buildConfiguration: buildProfile.schemeBuildConfiguration,
artifactPath: buildProfile.artifactPath,
releaseChannel: buildProfile.releaseChannel,
projectRootDirectory,
};
}
async function prepareManagedJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData,
buildProfile: IosManagedBuildProfile
): Promise<Partial<Ios.ManagedJob>> {
const projectRootDirectory =
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
return {
...(await prepareJobCommonAsync(ctx, {
credentials: jobData.credentials,
projectArchive: jobData.projectArchive,
})),
type: Workflow.MANAGED,
buildType: buildProfile.buildType,
username,
releaseChannel: buildProfile.releaseChannel,
projectRootDirectory,
};
}
+14 -7
View File
@@ -4,11 +4,13 @@ import { Platform, Workflow } from '@expo/eas-build-job';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import semver from 'semver';
import type { XCBuildConfiguration } from 'xcode';
import Log from '../../log';
import { resolveWorkflow } from '../../project/workflow';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { evaluateString } from '../utils/template';
import { readPlistAsync, writePlistAsync } from './plist';
export enum BumpStrategy {
@@ -104,12 +106,16 @@ export async function bumpVersionInAppJsonAsync({
export async function readShortVersionAsync(
projectDir: string,
exp: ExpoConfig
exp: ExpoConfig,
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<string | undefined> {
const workflow = resolveWorkflow(projectDir, Platform.IOS);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir);
return infoPlist.CFBundleShortVersionString;
return (
infoPlist.CFBundleShortVersionString &&
evaluateString(infoPlist.CFBundleShortVersionString, buildSettings)
);
} else {
return exp.version;
}
@@ -117,12 +123,13 @@ export async function readShortVersionAsync(
export async function readBuildNumberAsync(
projectDir: string,
exp: ExpoConfig
exp: ExpoConfig,
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<string | undefined> {
const workflow = resolveWorkflow(projectDir, Platform.IOS);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir);
return infoPlist.CFBundleVersion;
return infoPlist.CFBundleVersion && evaluateString(infoPlist.CFBundleVersion, buildSettings);
} else {
return IOSConfig.Version.getBuildNumber(exp);
}
+1 -1
View File
@@ -18,7 +18,7 @@ export async function runLocalBuildAsync(job: Job): Promise<void> {
await spawnAsync(execNameOrPath, [arg], {
stdio: 'inherit',
});
} catch (err) {
} catch (err: any) {
if (err.code === 'ENOENT') {
Log.warn(`Could not resolve executable ${execNameOrPath}.`);
Log.warn(
+113 -49
View File
@@ -1,17 +1,23 @@
import { Metadata } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
import { IosEnterpriseProvisioning } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import { getApplicationId } from '../project/android/applicationId';
import { getBundleIdentifier } from '../project/ios/bundleIdentifier';
import { getApplicationIdAsync } from '../project/android/applicationId';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
import { getUsername } from '../project/projectUtils';
import { ensureLoggedInAsync } from '../user/actions';
import { gitCommitHashAsync } from '../utils/git';
import { readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync } from './android/UpdatesModule';
import { readVersionCode, readVersionName } from './android/version';
import vcs from '../vcs';
import {
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
} from './android/UpdatesModule';
import { readVersionCodeAsync, readVersionNameAsync } 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';
/**
@@ -21,77 +27,135 @@ import { isExpoUpdatesInstalled } from './utils/updates';
*/
const packageJSON = require('../../package.json');
export type MetadataContext<T extends Platform> = T extends Platform.ANDROID
? object
: IosMetadataContext;
export interface IosMetadataContext {
buildSettings: XCBuildConfiguration['buildSettings'];
}
export async function collectMetadata<T extends Platform>(
ctx: BuildContext<T>,
{
credentialsSource,
}: {
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
}
platformContext?: MetadataContext<T>
): Promise<Metadata> {
return {
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
const distribution =
('simulator' in ctx.buildProfile && ctx.buildProfile.simulator
? 'simulator'
: ctx.buildProfile.distribution) ?? 'store';
const metadata = {
trackingContext: ctx.trackingCtx,
appVersion: await resolveAppVersionAsync(ctx),
appBuildVersion: await resolveAppBuildVersionAsync(ctx),
appVersion: await resolveAppVersionAsync(ctx, platformContext),
appBuildVersion: await resolveAppBuildVersionAsync(ctx, platformContext),
cliVersion: packageJSON.version,
workflow: ctx.buildProfile.workflow,
credentialsSource,
sdkVersion: ctx.commandCtx.exp.sdkVersion,
releaseChannel: await resolveReleaseChannel(ctx),
distribution: ctx.buildProfile.distribution ?? 'store',
appName: ctx.commandCtx.exp.name,
appIdentifier: resolveAppIdentifier(ctx),
buildProfile: ctx.commandCtx.profile,
gitCommitHash: await gitCommitHashAsync(),
username: getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync()),
workflow: ctx.workflow,
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: ctx.exp.runtimeVersion,
...channelOrReleaseChannel,
distribution,
appName: ctx.exp.name,
appIdentifier: await resolveAppIdentifierAsync(ctx),
buildProfile: ctx.buildProfileName,
gitCommitHash: await vcs.getCommitHashAsync(),
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
ctx as BuildContext<Platform.IOS>
),
}),
};
return sanitizeMetadata(metadata);
}
async function resolveAppVersionAsync<T extends Platform>(
ctx: BuildContext<T>
ctx: BuildContext<T>,
platformContext?: MetadataContext<T>
): Promise<string | undefined> {
if (ctx.platform === Platform.IOS) {
return await readShortVersionAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
const iosContext = platformContext as IosMetadataContext | undefined;
return await readShortVersionAsync(ctx.projectDir, ctx.exp, iosContext?.buildSettings ?? {});
} else {
return readVersionName(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return await readVersionNameAsync(ctx.projectDir, ctx.exp);
}
}
async function resolveAppBuildVersionAsync<T extends Platform>(
ctx: BuildContext<T>
ctx: BuildContext<T>,
platformContext?: MetadataContext<T>
): Promise<string | undefined> {
if (ctx.platform === Platform.IOS) {
return await readBuildNumberAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
const iosContext = platformContext as IosMetadataContext | undefined;
return await readBuildNumberAsync(ctx.projectDir, ctx.exp, iosContext?.buildSettings ?? {});
} else {
const versionCode = readVersionCode(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
const versionCode = await readVersionCodeAsync(ctx.projectDir, ctx.exp);
return versionCode !== undefined ? String(versionCode) : undefined;
}
}
function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string {
async function resolveAppIdentifierAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<string> {
if (ctx.platform === Platform.IOS) {
return getBundleIdentifier(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return await getBundleIdentifierAsync(ctx.projectDir, ctx.exp);
} else {
return getApplicationId(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return await getApplicationIdAsync(ctx.projectDir, ctx.exp);
}
}
async function resolveReleaseChannel<T extends Platform>(
async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<{ channel: string } | { releaseChannel: string } | null> {
if (!isExpoUpdatesInstalled(ctx.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.projectDir)) ?? 'default';
}
case Platform.IOS: {
return (await readIosReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
}
default:
return 'default';
}
}
async function getNativeChannelAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<string | undefined> {
if (!isExpoUpdatesInstalled(ctx.commandCtx.projectDir)) {
return undefined;
switch (ctx.platform) {
case Platform.ANDROID: {
return (await readAndroidChannelSafelyAsync(ctx.projectDir)) ?? undefined;
}
case Platform.IOS: {
return (await readIosChannelSafelyAsync(ctx.projectDir)) ?? undefined;
}
}
if (ctx.buildProfile.releaseChannel) {
return ctx.buildProfile.releaseChannel;
}
return undefined;
}
let maybeReleaseChannel: string | null;
if (ctx.platform === Platform.ANDROID) {
maybeReleaseChannel = await readAndroidReleaseChannelSafelyAsync(ctx.commandCtx.projectDir);
} else {
maybeReleaseChannel = await readIosReleaseChannelSafelyAsync(ctx.commandCtx.projectDir);
}
return maybeReleaseChannel ?? 'default';
function resolveIosEnterpriseProvisioning(
ctx: BuildContext<Platform.IOS>
): IosEnterpriseProvisioning | undefined {
return ctx.buildProfile.enterpriseProvisioning;
}
+8 -3
View File
@@ -1,5 +1,3 @@
export { Platform } from '@expo/eas-build-job';
export enum RequestedPlatform {
Android = 'android',
Ios = 'ios',
@@ -7,6 +5,7 @@ export enum RequestedPlatform {
}
export enum BuildStatus {
NEW = 'new',
IN_QUEUE = 'in-queue',
IN_PROGRESS = 'in-progress',
ERRORED = 'errored',
@@ -14,4 +13,10 @@ export enum BuildStatus {
CANCELED = 'canceled',
}
export type TrackingContext = Record<string, string | number>;
export enum BuildDistributionType {
STORE = 'store',
INTERNAL = 'internal',
SIMULATOR = 'simulator',
}
export type TrackingContext = Record<string, string | number | boolean>;
@@ -18,6 +18,8 @@ export enum Event {
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
}
export default {
@@ -7,9 +7,10 @@ import { requestedPlatformDisplayNames } from '../constants';
export function logCredentialsSource(credentialsSource: CredentialsSource, platform: Platform) {
let message = `Using ${credentialsSource} ${requestedPlatformDisplayNames[platform]} credentials`;
if (credentialsSource === CredentialsSource.LOCAL)
if (credentialsSource === CredentialsSource.LOCAL) {
message += ` ${chalk.dim('(credentials.json)')}`;
else if (credentialsSource === CredentialsSource.REMOTE)
} else if (credentialsSource === CredentialsSource.REMOTE) {
message += ` ${chalk.dim('(Expo server)')}`;
}
Log.succeed(message);
}
+55 -20
View File
@@ -1,14 +1,17 @@
import chalk from 'chalk';
import { BuildFragment, BuildStatus as GraphQLBuildStatus } from '../../graphql/generated';
import {
AppPlatform,
BuildFragment,
BuildStatus as GraphQLBuildStatus,
} from '../../graphql/generated';
import { appPlatformDisplayNames } from '../../platform';
import formatFields from '../../utils/formatFields';
import { appPlatformDisplayNames } from '../constants';
import { getBuildLogsUrl } from './url';
export function formatGraphQLBuild(build: BuildFragment) {
const actor = getActorName(build);
const account = build.project.__typename === 'App' ? build.project.ownerAccount.name : 'unknown';
const fields: { label: string; value: string }[] = [
const fields: { label: string; value?: string | null }[] = [
{ label: 'ID', value: build.id },
{
label: 'Platform',
@@ -18,6 +21,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:
@@ -35,53 +40,83 @@ export function formatGraphQLBuild(build: BuildFragment) {
},
{
label: 'Distribution',
value: build.distribution?.toLowerCase() ?? chalk.gray('unknown'),
value: build.distribution?.toLowerCase(),
},
{
label: 'Enterprise Provisioning',
value: build.iosEnterpriseProvisioning?.toLowerCase(),
},
{
label: 'Release Channel',
value: build.releaseChannel ?? chalk.gray('unknown'),
value: build.releaseChannel,
},
{
label: 'Channel',
value: build.channel,
},
{
label: 'SDK Version',
value: build.sdkVersion,
},
{
label: 'Runtime Version',
value: build.runtimeVersion,
},
{
label: 'Version',
value: build.appVersion,
},
{
label: build.platform === AppPlatform.Android ? 'Version code' : 'Build number',
value: build.appBuildVersion,
},
{
label: 'Commit',
value: build.gitCommitHash,
},
{
label: 'Logs',
value: getBuildLogsUrl({ buildId: build.id, account }),
value: getBuildLogsUrl(build),
},
{
label: 'Artifact',
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>';
case GraphQLBuildStatus.Canceled:
case GraphQLBuildStatus.Errored:
return '---------';
return null;
case GraphQLBuildStatus.Finished: {
const url = build.artifacts?.buildUrl;
return url ? url : chalk.red('not found');
}
default:
return 'unknown';
return null;
}
},
},
{ 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' },
];
return formatFields(fields);
const filteredFields = fields.filter(({ value }) => value !== undefined && value !== null) as {
label: string;
value: string;
}[];
return formatFields(filteredFields);
}
const getActorName = (build: BuildFragment): string => {
@@ -4,6 +4,7 @@ import indentString from 'indent-string';
import qrcodeTerminal from 'qrcode-terminal';
import {
AppPlatform,
BuildError,
BuildFragment,
BuildStatus,
@@ -12,51 +13,39 @@ import {
EasBuildDeprecationInfoType,
} from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import {
appPlatformDisplayNames,
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../constants';
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
import { printJsonOnlyOutput } from '../../utils/json';
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
export function printLogsUrls(
accountName: string,
builds: { platform: 'android' | 'ios'; buildId: string }[]
): void {
export function printLogsUrls(builds: BuildFragment[]): void {
if (builds.length === 1) {
const { buildId } = builds[0];
const logsUrl = getBuildLogsUrl({
buildId,
account: accountName,
});
Log.log(`Build details: ${chalk.underline(logsUrl)}`);
Log.log(`Build details: ${chalk.underline(getBuildLogsUrl(builds[0]))}`);
} else {
builds.forEach(({ buildId, platform }) => {
const logsUrl = getBuildLogsUrl({
buildId,
account: accountName,
});
builds.forEach(build => {
const logsUrl = getBuildLogsUrl(build);
Log.log(
`${requestedPlatformDisplayNames[platform]} build details: ${chalk.underline(logsUrl)}`
`${appPlatformDisplayNames[build.platform]} build details: ${chalk.underline(logsUrl)}`
);
});
}
}
export function printBuildResults(accountName: string, builds: (BuildFragment | null)[]): void {
Log.newLine();
if (builds.length === 1) {
const [build] = builds;
assert(build, 'Build should be defined');
printBuildResult(accountName, build);
export function printBuildResults(builds: (BuildFragment | null)[], json: boolean): void {
if (json) {
printJsonOnlyOutput(builds);
} else {
(builds.filter(i => i) as BuildFragment[]).forEach(build =>
printBuildResult(accountName, build)
);
Log.newLine();
if (builds.length === 1) {
const [build] = builds;
assert(build, 'Build should be defined');
printBuildResult(build);
} else {
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
}
}
}
function printBuildResult(accountName: string, build: BuildFragment): void {
function printBuildResult(build: BuildFragment): void {
Log.addNewLineIfNone();
if (build.status === BuildStatus.Errored) {
const userError = build.error;
@@ -80,12 +69,14 @@ function printBuildResult(accountName: string, build: BuildFragment): void {
}
if (build.distribution === DistributionType.Internal) {
const logsUrl = getBuildLogsUrl({
buildId: build.id,
account: accountName,
});
const installUrl = getInternalDistributionInstallUrl(build);
qrcodeTerminal.generate(installUrl, code => console.log(`${indentString(code, 2)}\n`));
const logsUrl = getBuildLogsUrl(build);
// It's tricky to install the .apk file directly on Android so let's fallback
// to the build details page and let people press the button to download there
const qrcodeUrl =
build.platform === AppPlatform.Ios ? getInternalDistributionInstallUrl(build) : logsUrl;
qrcodeTerminal.generate(qrcodeUrl, { small: true }, code =>
Log.log(`${indentString(code, 2)}\n`)
);
Log.log(
`${appPlatformEmojis[build.platform]} Open this link on your ${
appPlatformDisplayNames[build.platform]
+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,
};
@@ -0,0 +1,5 @@
import template from 'lodash/template';
export function evaluateString(s: string, vars: Record<string, any>): string {
return template(s, { interpolate: /\$\(([\s\S]+?)\)/g })(vars);
}
+11 -8
View File
@@ -3,14 +3,17 @@ import assert from 'assert';
import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api';
import { AppPlatform, BuildFragment } from '../../graphql/generated';
export function getBuildLogsUrl({
buildId,
account,
}: {
buildId: string;
account: string;
}): string {
return `${getExpoWebsiteBaseUrl()}/accounts/${account}/builds/${buildId}`;
export function getProjectDashboardUrl(accountName: string, projectName: string): string {
return `${getExpoWebsiteBaseUrl()}/accounts/${accountName}/projects/${projectName}`;
}
export function getBuildLogsUrl(build: BuildFragment): string {
const { project } = build;
if (project.__typename === 'App') {
return `${getExpoWebsiteBaseUrl()}/accounts/${project.ownerAccount.name}/builds/${build.id}`;
} else {
return `${getExpoWebsiteBaseUrl()}/builds/${build.id}`;
}
}
export function getArtifactUrl(artifactId: string): string {
+52
View File
@@ -0,0 +1,52 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import fs from 'fs-extra';
import path from 'path';
import Log, { learnMore } from '../log';
import vcs from '../vcs';
import { BuildContext } from './context';
export function checkNodeEnvVariable(ctx: BuildContext<Platform>): void {
if (ctx.buildProfile.env?.NODE_ENV === 'production') {
Log.warn(
'You set NODE_ENV=production in the build profile. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
);
Log.newLine();
}
}
export async function checkGoogleServicesFileAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<void> {
if (ctx.workflow === Workflow.GENERIC || ctx.buildProfile?.env?.GOOGLE_SERVICES_FILE) {
return;
}
const googleServicesFilePath = ctx.exp[ctx.platform]?.googleServicesFile;
if (!googleServicesFilePath) {
return;
}
const rootDir = await vcs.getRootPathAsync();
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
if (
(await fs.pathExists(absGoogleServicesFilePath)) &&
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
(await vcs.isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
) {
Log.warn(
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
);
Log.warn(
`Use EAS Secret to pass all values that you don't want to include in your version control. ${learnMore(
'https://docs.expo.dev/build-reference/variables/#using-secrets-in-environment-variables'
)}`
);
Log.warn(
'If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting GOOGLE_SERVICES_FILE in your build profile in eas.json to any non-falsy value.'
);
Log.newLine();
}
}
function isInsideDirectory(file: string, directory: string): boolean {
return file.startsWith(directory);
}
@@ -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,85 @@
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';
TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
jest.mock('../../user/User');
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
jest.mock('../../analytics', () => {
const { AnalyticsEvent } = jest.requireActual('../../analytics');
return {
AnalyticsEvent,
logEvent: jest.fn(),
initAsync: jest.fn(),
flushAsync: jest.fn(),
};
});
beforeEach(() => {
(getUserAsync as jest.Mock).mockReset().mockImplementation(() => mockJester);
(ensureLoggedInAsync as jest.Mock).mockReset().mockImplementation(() => mockJester);
(initAsync as jest.Mock).mockReset();
(flushAsync as jest.Mock).mockReset();
(logEvent as jest.Mock).mockReset();
});
describe(EasCommand.name, () => {
describe('without exceptions', () => {
// The first test in this suite should have an increased timeout
// because of the implementation of Command from @oclif/command.
// It seems that loading config takes significant amount of time
// and I'm not sure how to mock it.
//
// See https://github.com/oclif/command/blob/master/src/command.ts#L80
// and look for "Config.load"
it('ensures the user is logged in', async () => {
await TestEasCommand.run();
expect(ensureLoggedInAsync).toHaveBeenCalled();
}, 15_000);
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
+1 -1
View File
@@ -4,7 +4,7 @@ import Log from '../log';
import UserSettings from '../user/UserSettings';
export default class AnalyticsView extends Command {
static description = 'View or change analytics settings';
static description = 'view or change analytics settings';
static args = [{ name: 'STATUS', options: ['on', 'off'] }];
@@ -16,7 +16,7 @@ import {
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { getBranchNameAsync } from '../../utils/git';
import vcs from '../../vcs';
export async function createUpdateBranchOnAppAsync({
appId,
@@ -93,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),
}));
}
@@ -22,7 +22,7 @@ import { promptAsync, toggleConfirmAsync } from '../../prompts';
async function getBranchInfoAsync({
appId,
name,
}: GetBranchInfoQueryVariables): Promise<{ branchId: string }> {
}: GetBranchInfoQueryVariables): Promise<GetBranchInfoQuery> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<GetBranchInfoQuery, GetBranchInfoQueryVariables>(
@@ -46,11 +46,7 @@ async function getBranchInfoAsync({
)
.toPromise()
);
const branchId = data.app?.byId.updateBranchByName?.id;
if (!branchId) {
throw new Error(`Could not find branch ${name} on ${appId}`);
}
return { branchId };
return data;
}
async function deleteBranchOnAppAsync({
@@ -122,7 +118,11 @@ export default class BranchDelete extends Command {
}));
}
const { branchId } = await getBranchInfoAsync({ appId: projectId, name });
const data = await getBranchInfoAsync({ appId: projectId, name });
const branchId = data.app?.byId.updateBranchByName?.id;
if (!branchId) {
throw new Error(`Could not find branch ${name} on ${fullName}`);
}
if (!jsonFlag) {
Log.addNewLineIfNone();
@@ -100,6 +100,8 @@ export default class BranchList extends Command {
}),
])
);
Log.addNewLineIfNone();
Log.log(chalk.bold('Branches with their most recent update group:'));
Log.log(table.toString());
if (branches.length >= BRANCHES_LIMIT) {
+206 -83
View File
@@ -1,9 +1,8 @@
import { getConfig, getDefaultTarget } from '@expo/config';
import { ExpoConfig, getConfig, getDefaultTarget } from '@expo/config';
import { getRuntimeVersionForSDKVersion } from '@expo/sdk-runtime-versions';
import { Command, flags } from '@oclif/command';
import assert from 'assert';
import chalk from 'chalk';
import Table from 'cli-table3';
import dateFormat from 'dateformat';
import gql from 'graphql-tag';
import { uniqBy } from 'lodash';
@@ -11,29 +10,29 @@ import ora from 'ora';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
Actor,
GetUpdateGroupAsyncQuery,
Robot,
RootQueryUpdatesByGroupArgs,
Update,
UpdateInfoGroup,
User,
ViewBranchQuery,
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import Log from '../../log';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import {
PublishPlatform,
buildBundlesAsync,
buildUpdateInfoGroupAsync,
buildUnsortedUpdateInfoGroupAsync,
collectAssets,
uploadAssetsAsync,
} from '../../project/publish';
import { promptAsync, selectAsync } from '../../prompts';
import { formatUpdate } from '../../update/utils';
import { getLastCommitMessageAsync } from '../../utils/git';
import formatFields from '../../utils/formatFields';
import vcs from '../../vcs';
import { createUpdateBranchOnAppAsync } from './create';
import { listBranchesAsync } from './list';
import { viewUpdateBranchAsync } from './view';
@@ -67,6 +66,33 @@ async function getUpdateGroupAsync({
return updatesByGroup;
}
async function ensureBranchExists({
appId,
name: branchName,
}: {
appId: string;
name: string;
}): Promise<{
id: string;
updates: Exclude<
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
null | undefined
>['updates'];
}> {
const { app } = await viewUpdateBranchAsync({
appId,
name: branchName,
});
const updateBranch = app?.byId.updateBranchByName;
if (updateBranch) {
const { id, updates } = updateBranch;
return { id, updates };
}
const newUpdateBranch = await createUpdateBranchOnAppAsync({ appId, name: branchName });
return { id: newUpdateBranch.id, updates: [] };
}
export default class BranchPublish extends Command {
static hidden = true;
static description = 'Publish an update group to a branch.';
@@ -80,7 +106,7 @@ export default class BranchPublish extends Command {
static flags = {
message: flags.string({
description: 'Short message describing the updates.',
description: 'short message describing the updates.',
required: false,
}),
republish: flags.boolean({
@@ -102,7 +128,7 @@ export default class BranchPublish extends Command {
}),
platform: flags.enum({
char: 'p',
description: `Only publish to a single platform`,
description: `only publish to a single platform`,
options: [...defaultPublishPlatforms, 'all'],
default: 'all',
required: false,
@@ -111,13 +137,19 @@ export default class BranchPublish extends Command {
description: `return a json with the new update group.`,
default: false,
}),
auto: flags.boolean({
description:
'use the current git branch and commit message for the EAS branch and update message',
default: false,
}),
};
async run() {
let {
args: { name },
args: { name: branchName },
flags: {
json: jsonFlag,
auto: autoFlag,
message,
republish,
group,
@@ -134,34 +166,28 @@ export default class BranchPublish extends Command {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = await getProjectAccountNameAsync(exp);
let { slug, runtimeVersion, sdkVersion } = exp;
const { exp } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
isPublicConfig: true,
});
// When a SDK version is supplied instead of a runtime version and we're in the managed workflow
// construct the runtimeVersion with special meaning indicating that the runtime is an
// Expo SDK preset runtime that can be launched in Expo Go.
const isManagedProject = getDefaultTarget(projectDir) === 'managed';
if (!runtimeVersion && sdkVersion && isManagedProject) {
Log.withTick('Generating runtime version from sdk version');
runtimeVersion = getRuntimeVersionForSDKVersion(sdkVersion);
}
const runtimeVersions = getRuntimeVersionObject(exp, platformFlag, projectDir);
if (!runtimeVersion) {
throw new Error(
"Couldn't find 'runtimeVersion'. Please specify it under the 'expo' key in 'app.json'"
);
}
const projectId = await getProjectIdAsync(exp);
if (!name) {
if (!branchName && autoFlag) {
branchName =
(await vcs.getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`;
}
if (!branchName) {
const validationMessage = 'branch name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
const branches = await listBranchesAsync({ projectId });
name = await selectAsync<string>(
branchName = await selectAsync<string>(
'which branch would you like to publish on?',
branches.map(branch => {
return {
@@ -172,15 +198,15 @@ export default class BranchPublish extends Command {
};
})
);
assert(branchName, 'branch name must be specified.');
}
assert(name, 'branch name must be specified.');
const { id: branchId, updates } = await viewUpdateBranchAsync({
const { id: branchId, updates } = await ensureBranchExists({
appId: projectId,
name,
name: branchName,
});
let updateInfoGroup: UpdateInfoGroup = {};
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
let oldMessage: string, oldRuntimeVersion: string;
if (republish) {
// If we are republishing, we don't need to worry about building the bundle or uploading the assets.
@@ -208,7 +234,7 @@ export default class BranchPublish extends Command {
}));
if (updateGroups.length === 0) {
throw new Error(
`There are no updates on branch "${name}" published on the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
`There are no updates on branch "${branchName}" published on the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
);
}
@@ -224,7 +250,7 @@ export default class BranchPublish extends Command {
);
if (updatesToRepublishFilteredByPlatform.length === 0) {
throw new Error(
`There are no updates on branch "${name}" published on the platform(s) "${platformFlag}" with group ID "${
`There are no updates on branch "${branchName}" published on the platform(s) "${platformFlag}" with group ID "${
group ? group : updatesToRepublish[0].group
}". Did you mean to publish a new update instead?`
);
@@ -247,7 +273,7 @@ export default class BranchPublish extends Command {
const { manifestFragment } = update;
const platform = update.platform as PublishPlatform;
updateInfoGroup[platform] = JSON.parse(manifestFragment);
unsortedUpdateInfoGroups[platform] = JSON.parse(manifestFragment);
}
// These are the same for each member of an update group
@@ -265,7 +291,7 @@ export default class BranchPublish extends Command {
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
const assets = collectAssets({ inputDir: inputDir!, platforms });
await uploadAssetsAsync(assets);
updateInfoGroup = await buildUpdateInfoGroupAsync(assets);
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
assetSpinner.succeed('Uploaded assets!');
} catch (e) {
assetSpinner.fail('Failed to upload assets');
@@ -273,6 +299,10 @@ export default class BranchPublish extends Command {
}
}
if (!message && autoFlag) {
message = (await vcs.getLastCommitMessageAsync())?.trim();
}
if (!message) {
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
@@ -284,20 +314,43 @@ 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),
}));
}
let newUpdateGroup;
const runtimeToPlatformMapping: Record<string, string[]> = {};
for (const runtime of new Set(Object.values(runtimeVersions))) {
runtimeToPlatformMapping[runtime] = Object.entries(runtimeVersions)
.filter(pair => pair[1] === runtime)
.map(pair => pair[0]);
}
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups = Object.entries(runtimeToPlatformMapping).map(([runtime, platforms]) => {
const localUpdateInfoGroup = Object.fromEntries(
platforms.map(platform => [
platform,
unsortedUpdateInfoGroups[platform as keyof UpdateInfoGroup],
])
);
if (republish && !oldRuntimeVersion) {
throw new Error(
'Can not find the runtime version of the update group that is being republished.'
);
}
return {
branchId,
updateInfoGroup: localUpdateInfoGroup,
runtimeVersion: republish ? oldRuntimeVersion : runtime,
message,
};
});
let newUpdates;
const publishSpinner = ora('Publishing...').start();
try {
newUpdateGroup = await PublishMutation.publishUpdateGroupAsync({
branchId,
updateInfoGroup,
runtimeVersion: republish ? oldRuntimeVersion! : runtimeVersion,
message,
});
newUpdates = await PublishMutation.publishUpdateGroupAsync(updateGroups);
publishSpinner.succeed('Published!');
} catch (e) {
publishSpinner.fail('Failed to published updates');
@@ -305,48 +358,118 @@ export default class BranchPublish extends Command {
}
if (jsonFlag) {
Log.log(JSON.stringify(newUpdateGroup));
Log.log(JSON.stringify(newUpdates));
} else {
const outputMessage = new Table({
wordWrap: true,
chars: {
top: '',
'top-mid': '',
'top-left': '',
'top-right': '',
bottom: '',
'bottom-mid': '',
'bottom-left': '',
'bottom-right': '',
left: '',
'left-mid': '',
mid: '',
'mid-mid': '',
right: '',
'right-mid': '',
middle: ' ',
},
style: { 'padding-left': 0, 'padding-right': 0 },
});
outputMessage.push(
[chalk.dim('project'), `@${accountName}/${slug}`],
[chalk.dim('branch'), name],
[chalk.dim('runtimeVersion'), runtimeVersion],
[chalk.dim('groupID'), newUpdateGroup.group],
[chalk.dim('message'), message]
);
Log.log(outputMessage.toString());
if (new Set(newUpdates.map(update => update.group)).size > 1) {
Log.addNewLineIfNone();
Log.log(
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
);
}
Log.addNewLineIfNone();
for (const runtime of new Set(Object.values(runtimeVersions))) {
const platforms = newUpdates
.filter(update => update.runtimeVersion === runtime)
.map(update => update.platform);
const newUpdate = newUpdates.find(update => update.runtimeVersion === runtime);
if (!newUpdate) {
throw new Error(`Publish response is missing updates with runtime ${runtime}.`);
}
Log.log(
formatFields([
{ label: 'branch', value: branchName },
{ label: 'runtime version', value: runtime },
{ label: 'platform', value: platforms.join(',') },
{ label: 'update group ID', value: newUpdate.group },
{ label: 'message', value: message },
])
);
Log.addNewLineIfNone();
}
}
}
}
function formatUpdateTitle(
update: Pick<Update, 'message' | 'createdAt' | 'runtimeVersion'> & {
actor?: Pick<Actor, 'firstName'> | null;
function getRuntimeVersionObject(
exp: ExpoConfig,
platformFlag: string,
projectDir: string
): Record<string, string> {
let { runtimeVersion: defaultRuntimeVersion, sdkVersion } = exp;
// When a SDK version is supplied instead of a runtime version and we're in the managed workflow
// construct the runtimeVersion with special meaning indicating that the runtime is an
// Expo SDK preset runtime that can be launched in Expo Go.
const isManagedProject = getDefaultTarget(projectDir) === 'managed';
if (!defaultRuntimeVersion && sdkVersion && isManagedProject) {
Log.withTick('Generating runtime version from sdk version');
defaultRuntimeVersion = getRuntimeVersionForSDKVersion(sdkVersion);
}
const iOSRuntimeVersion = (exp as any).ios?.runtimeVersion; // TODO-JJ remove cast to any
const androidRuntimeVersion = (exp as any).android?.runtimeVersion; // TODO-JJ remove cast to any
let runtimeVersions: Record<string, string>;
switch (platformFlag) {
case 'ios': {
runtimeVersions = {
ios: iOSRuntimeVersion ?? defaultRuntimeVersion,
};
break;
}
case 'android': {
runtimeVersions = {
android: androidRuntimeVersion ?? defaultRuntimeVersion,
};
break;
}
case 'all': {
runtimeVersions = {
ios: iOSRuntimeVersion ?? defaultRuntimeVersion,
android: androidRuntimeVersion ?? defaultRuntimeVersion,
};
break;
}
default:
throw new Error('Platform flag must be "ios", "android", or "all"');
}
if (Object.values(runtimeVersions).some(runtime => !runtime)) {
throw new Error(
"Couldn't find a 'runtimeVersion' for every platform. Please specify it under the 'expo' key in 'app.json'"
);
}
if (Object.values(runtimeVersions).some(runtime => typeof runtime !== 'string')) {
throw new Error(
`Please ensure that all of the runtime versions defined in the app.json are strings.`
);
}
return runtimeVersions;
}
function formatUpdateTitle(
update: Exclude<
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
null | undefined
>['updates'][number]
): string {
const { message, createdAt, actor, runtimeVersion } = update;
return `[${dateFormat(createdAt, 'mmm dd HH:MM')} by ${
actor?.firstName ?? 'unknown'
}, runtimeVersion: ${runtimeVersion}] ${message}`;
let actorName: string;
switch (actor?.__typename) {
case 'User': {
actorName = (actor as Pick<User, 'username' | 'id'>).username;
break;
}
case 'Robot': {
const { firstName, id } = actor as Pick<Robot, 'firstName' | 'id'>;
actorName = firstName ?? `robot: ${id.slice(0, 4)}...`;
break;
}
default:
actorName = 'unknown';
}
return `[${dateFormat(
createdAt,
'mmm dd HH:MM'
)} by ${actorName}, runtimeVersion: ${runtimeVersion}] ${message}`;
}
+19 -38
View File
@@ -6,41 +6,19 @@ import gql from 'graphql-tag';
import { groupBy } from 'lodash';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
Maybe,
Robot,
Update,
UpdateBranch,
User,
ViewBranchQuery,
ViewBranchQueryVariables,
} from '../../graphql/generated';
import { ViewBranchQuery, ViewBranchQueryVariables } from '../../graphql/generated';
import Log from '../../log';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
import formatFields from '../../utils/formatFields';
const PAGE_LIMIT = 10_000;
type TruncatedUpdate = Pick<Update, 'group' | 'message' | 'createdAt' | 'actor'>;
export async function viewUpdateBranchAsync({
appId,
name,
}: Pick<ViewBranchQueryVariables, 'appId' | 'name'>): Promise<
Pick<UpdateBranch, 'id' | 'name'> & {
updates: (Pick<
Update,
'id' | 'group' | 'message' | 'createdAt' | 'runtimeVersion' | 'platform' | 'manifestFragment'
> & {
actor?: Maybe<Pick<User, 'firstName' | 'id'> | Pick<Robot, 'firstName' | 'id'>>;
})[];
}
> {
}: Pick<ViewBranchQueryVariables, 'appId' | 'name'>): Promise<ViewBranchQuery> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<ViewBranchQuery, ViewBranchQueryVariables>(
@@ -83,11 +61,7 @@ export async function viewUpdateBranchAsync({
)
.toPromise()
);
const updateBranch = data.app?.byId.updateBranchByName;
if (!updateBranch) {
throw new Error(`Could not find branch "${name}"`);
}
return updateBranch;
return data;
}
export default class BranchView extends Command {
@@ -121,7 +95,6 @@ export default class BranchView extends Command {
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!name) {
@@ -137,10 +110,14 @@ export default class BranchView extends Command {
}));
}
const UpdateBranch = await viewUpdateBranchAsync({
const { app } = await viewUpdateBranchAsync({
appId: projectId,
name,
});
const UpdateBranch = app?.byId.updateBranchByName;
if (!UpdateBranch) {
throw new Error(`Could not find branch "${name}"`);
}
const updates = Object.values(groupBy(UpdateBranch.updates, u => u.group)).map(
group => group[0]
@@ -168,12 +145,16 @@ export default class BranchView extends Command {
]);
}
Log.withTick(
`️Branch: ${chalk.bold(UpdateBranch.name)} on project ${chalk.bold(
fullName
)}. Branch ID: ${chalk.bold(UpdateBranch.id)}`
Log.addNewLineIfNone();
Log.log(chalk.bold('Branch:'));
Log.log(
formatFields([
{ label: 'Name', value: UpdateBranch.name },
{ label: 'ID', value: UpdateBranch.id },
])
);
Log.log(chalk.bold('Recent update groups published on this branch:'));
Log.addNewLineIfNone();
Log.log(chalk.bold('Recently published update groups:'));
Log.log(groupTable.toString());
}
}
+15 -6
View File
@@ -4,7 +4,6 @@ import chalk from 'chalk';
import gql from 'graphql-tag';
import ora from 'ora';
import { appPlatformEmojis } from '../../build/constants';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
Build,
@@ -14,6 +13,7 @@ import {
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import { appPlatformEmojis } from '../../platform';
import {
findProjectRootAsync,
getProjectFullNameAsync,
@@ -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([
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.InQueue }),
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.InProgress }),
const [newBuilds, inQueueBuilds, inProgressBuilds] = await Promise.all([
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.New } }),
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.InQueue } }),
BuildQuery.allForAppAsync(projectId, { filter: { 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(),
});
@@ -70,7 +56,7 @@ function logSuccess(platform: RequestedPlatform) {
- Run ${chalk.bold('eas build')} when you are ready to create your first build.
- Once the build is completed, run ${chalk.bold('eas submit')} to upload the app to ${storesText}
- ${learnMore('https://docs.expo.io/build/introduction', {
- ${learnMore('https://docs.expo.dev/build/introduction', {
learnMoreMessage: 'Learn more about other capabilities of EAS Build',
})}`);
}
+218 -115
View File
@@ -1,35 +1,69 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { Platform, Workflow } from '@expo/eas-build-job';
import {
flushAsync as flushAnalyticsAsync,
initAsync as initAnalyticsAsync,
} from '../../analytics';
import { configureAsync } from '../../build/configure';
import { createCommandContextAsync } from '../../build/context';
import { buildAsync } from '../../build/create';
EasJsonReader,
hasMismatchedExtendsAsync,
isUsingDeprecatedFormatAsync,
migrateAsync,
} from '@expo/eas-json';
import { flags } from '@oclif/command';
import { error, exit } from '@oclif/errors';
import { prepareAndroidBuildAsync } from '../../build/android/build';
import { BuildRequestSender, waitForBuildEndAsync } from '../../build/build';
import { ensureProjectConfiguredAsync } from '../../build/configure';
import { BuildContext, createBuildContextAsync } from '../../build/context';
import { prepareIosBuildAsync } from '../../build/ios/build';
import { RequestedPlatform } from '../../build/types';
import { isGitStatusCleanAsync } from '../../build/utils/repository';
import Log, { learnMore } from '../../log';
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { BuildFragment, BuildStatus } from '../../graphql/generated';
import Log from '../../log';
import {
EAS_UNAVAILABLE_MESSAGE,
isEasEnabledForProjectAsync,
warnEasUnavailable,
} from '../../project/isEasEnabledForProject';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
import { findProjectRootAsync } from '../../project/projectUtils';
import { confirmAsync, promptAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions';
import { enableJsonOutput } from '../../utils/json';
import vcs from '../../vcs';
export default class Build extends Command {
static description = 'Start a build';
interface RawBuildFlags {
platform?: string;
'skip-credentials-check': boolean;
'skip-project-configuration': boolean;
profile: string;
'non-interactive': boolean;
local: boolean;
wait: boolean;
'clear-cache': boolean;
json: boolean;
}
interface BuildFlags {
requestedPlatform: RequestedPlatform;
skipProjectConfiguration: boolean;
profile: string;
nonInteractive: boolean;
local: boolean;
wait: boolean;
clearCache: boolean;
json: boolean;
}
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,
}),
json: flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
default: false,
}),
'skip-project-configuration': flags.boolean({
default: false,
@@ -59,115 +93,184 @@ export default class Build extends Command {
};
async run(): Promise<void> {
const { flags } = this.parse(Build);
await initAnalyticsAsync(); // TODO: run in oclif hook for every command
try {
await ensureLoggedInAsync();
const { flags: rawFlags } = this.parse(Build);
if (rawFlags.json) {
enableJsonOutput();
}
const flags = await this.sanitizeFlagsAsync(rawFlags);
const { requestedPlatform } = flags;
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();
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
let { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
await vcs.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
if (!flags.local && !(await isEasEnabledForProjectAsync(projectId))) {
warnEasUnavailable();
process.exitCode = 1;
return;
}
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
if (flags.local && !verifyOptionsForLocalBuilds(platform)) {
process.exitCode = 1;
return;
}
const easJsonReader = new EasJsonReader(projectDir);
const platformsToBuild = this.getPlatformsToBuild(requestedPlatform);
exp = (await ensureProjectConfiguredAsync(projectDir)) ?? exp;
const commandCtx = await createCommandContextAsync({
requestedPlatform: platform,
profile: flags.profile,
exp,
projectDir,
projectId,
nonInteractive,
clearCache: flags['clear-cache'],
skipCredentialsCheck: flags['skip-credentials-check'],
const startedBuilds: BuildFragment[] = [];
let metroConfigValidated = false;
for (const platform of platformsToBuild) {
const ctx = await createBuildContextAsync({
buildProfileName: flags.profile,
clearCache: flags.clearCache,
buildProfile: await easJsonReader.readBuildProfileAsync(platform, flags.profile),
local: flags.local,
skipProjectConfiguration: flags['skip-project-configuration'],
waitForBuildEnd: flags.wait,
nonInteractive: flags.nonInteractive,
platform,
projectDir,
skipProjectConfiguration: flags.skipProjectConfiguration,
});
await buildAsync(commandCtx);
} finally {
await flushAnalyticsAsync(); // TODO: run in oclif hook for every command
if (ctx.workflow === Workflow.MANAGED && !metroConfigValidated) {
await validateMetroConfigForManagedWorkflowAsync(ctx);
metroConfigValidated = true;
}
if (!ctx.local && !(await isEasEnabledForProjectAsync(ctx.projectId))) {
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
}
const maybeBuild = await this.startBuildAsync(ctx);
if (maybeBuild) {
startedBuilds.push(maybeBuild);
}
}
if (flags.local) {
return;
}
Log.newLine();
printLogsUrls(startedBuilds);
Log.newLine();
if (flags.wait) {
const builds = await waitForBuildEndAsync(startedBuilds.map(build => build.id));
printBuildResults(builds, flags.json);
this.exitWithNonZeroCodeIfSomeBuildsFailed(builds);
}
}
}
function verifyOptionsForLocalBuilds(platform: RequestedPlatform): boolean {
if (platform === RequestedPlatform.All) {
Log.error('Builds for multiple platforms are not supported with flag --local');
return false;
} else if (process.platform !== 'darwin' && platform === RequestedPlatform.Ios) {
Log.error('Unsupported platform, macOS is required to build apps for iOS');
return false;
} else {
return true;
}
}
private async sanitizeFlagsAsync(flags: RawBuildFlags): Promise<BuildFlags> {
const nonInteractive = flags['non-interactive'];
if (!flags.platform && nonInteractive) {
error('--platform is required when building in non-interactive mode', { exit: 1 });
}
if (flags.json && !nonInteractive) {
error('--json is allowed only when building in non-interactive mode', { exit: 1 });
}
const requestedPlatform =
(flags.platform as RequestedPlatform | undefined) ?? (await this.promptForPlatformAsync());
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
const { platform } = await promptAsync({
type: 'select',
message: 'Build for platforms',
name: 'platform',
choices: [
{
title: 'All',
value: RequestedPlatform.All,
},
{
title: 'iOS',
value: RequestedPlatform.Ios,
},
{
title: 'Android',
value: RequestedPlatform.Android,
},
],
});
return platform;
}
if (flags.local) {
if (requestedPlatform === RequestedPlatform.All) {
error('Builds for multiple platforms are not supported with flag --local', { exit: 1 });
} else if (process.platform !== 'darwin' && requestedPlatform === RequestedPlatform.Ios) {
error('Unsupported platform, macOS is required to build apps for iOS', { exit: 1 });
}
}
async function ensureProjectConfiguredAsync(projectDir: string): Promise<ExpoConfig | null> {
if (await fs.pathExists(path.join(projectDir, 'eas.json'))) {
return null;
}
const confirm = await confirmAsync({
message: 'This app is not set up for building with EAS. Set it up now?',
});
if (confirm) {
await configureAsync({
projectDir,
platform: RequestedPlatform.All,
allowExperimental: false,
});
if (!(await isGitStatusCleanAsync())) {
throw new Error(
'Build process requires clean git working tree, please commit all your changes and run `eas build` again'
if (flags['skip-credentials-check']) {
Log.warnDeprecatedFlag(
'skip-credentials-check',
'Build credentials validation is always skipped with the --non-interactive flag. You can also skip interactively.'
);
Log.newLine();
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
return exp;
} else {
throw new Error(
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
'https://docs.expo.io/build/eas-json'
)})`
return {
requestedPlatform,
skipProjectConfiguration: flags['skip-project-configuration'],
profile: flags['profile'],
nonInteractive,
local: flags['local'],
wait: flags['wait'],
clearCache: flags['clear-cache'],
json: flags['json'],
};
}
private async promptForPlatformAsync(): Promise<RequestedPlatform> {
const { platform } = await promptAsync({
type: 'select',
message: 'Build for platforms',
name: 'platform',
choices: [
{ title: 'All', value: RequestedPlatform.All },
{ title: 'iOS', value: RequestedPlatform.Ios },
{ title: 'Android', value: RequestedPlatform.Android },
],
});
return platform;
}
private getPlatformsToBuild(requestedPlatform: RequestedPlatform): Platform[] {
if (requestedPlatform === RequestedPlatform.All) {
return [Platform.ANDROID, Platform.IOS];
} else if (requestedPlatform === RequestedPlatform.Android) {
return [Platform.ANDROID];
} else {
return [Platform.IOS];
}
}
private async startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
let sendBuildRequestAsync: BuildRequestSender;
if (ctx.platform === Platform.ANDROID) {
sendBuildRequestAsync = await prepareAndroidBuildAsync(ctx as BuildContext<Platform.ANDROID>);
} else {
sendBuildRequestAsync = await prepareIosBuildAsync(ctx as BuildContext<Platform.IOS>);
}
return await sendBuildRequestAsync();
}
private exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
i => i.status === BuildStatus.Errored
);
if (failedBuilds.length > 0) {
exit(1);
}
}
}
export async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
if (await isUsingDeprecatedFormatAsync(projectDir)) {
const hasMismatchedExtendsKeys = await hasMismatchedExtendsAsync(projectDir);
if (nonInteractive) {
Log.error('We detected that your eas.json is using a deprecated format.');
Log.error(
'We will convert it automatically if run this command without --non-interactive flag. Alternatively, you can update eas.json manually according to https://docs.expo.dev/build/eas-json'
);
error('Unsupported eas.json format', { exit: 1 });
}
const confirm = await confirmAsync({
message:
'We detected that your eas.json is using a deprecated format, do you want to migrate to the new format?',
});
if (confirm) {
await migrateAsync(projectDir);
if (hasMismatchedExtendsKeys) {
Log.warn(
'"extends" keyword can only be migrated automatically to the new format if both Android and iOS profiles extend base profiles with the same names for both platforms'
);
Log.warn(
'Migration was successful, but you need to manually adjust the extend rules for your profiles'
);
error('Fix eas.json manually', { exit: 1 });
}
} else {
Log.error(
"Aborting, update your eas.json according to https://docs.expo.dev/build/eas-json and run 'eas build' again"
);
error('Unsupported eas.json format', { exit: 1 });
}
}
}
+76 -12
View File
@@ -3,9 +3,13 @@ import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import ora from 'ora';
import { BuildStatus, RequestedPlatform } from '../../build/types';
import { BuildDistributionType, BuildStatus, RequestedPlatform } from '../../build/types';
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
import { AppPlatform, BuildStatus as GraphQLBuildStatus } from '../../graphql/generated';
import {
AppPlatform,
DistributionType,
BuildStatus as GraphQLBuildStatus,
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import {
@@ -13,6 +17,7 @@ import {
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class BuildList extends Command {
static description = 'list all builds for your project';
@@ -21,8 +26,12 @@ export default class BuildList extends Command {
platform: flags.enum({
options: [RequestedPlatform.All, RequestedPlatform.Android, RequestedPlatform.Ios],
}),
json: flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
}),
status: flags.enum({
options: [
BuildStatus.NEW,
BuildStatus.IN_QUEUE,
BuildStatus.IN_PROGRESS,
BuildStatus.ERRORED,
@@ -30,16 +39,40 @@ export default class BuildList extends Command {
BuildStatus.CANCELED,
],
}),
distribution: flags.enum({
options: [
BuildDistributionType.STORE,
BuildDistributionType.INTERNAL,
BuildDistributionType.SIMULATOR,
],
}),
channel: flags.string(),
appVersion: flags.string(),
appBuildVersion: flags.string(),
sdkVersion: flags.string(),
runtimeVersion: flags.string(),
appIdentifier: flags.string(),
buildProfile: flags.string(),
gitCommitHash: flags.string(),
limit: flags.integer(),
};
async run() {
const { platform: requestedPlatform, status: buildStatus, limit = 10 } = this.parse(
BuildList
).flags;
const { flags } = this.parse(BuildList);
const {
json,
platform: requestedPlatform,
status: buildStatus,
distribution: buildDistribution,
limit = 10,
} = flags;
if (json) {
enableJsonOutput();
}
const platform = toAppPlatform(requestedPlatform);
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution);
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -51,8 +84,19 @@ export default class BuildList extends Command {
try {
const builds = await BuildQuery.allForAppAsync(projectId, {
limit,
platform,
status: graphqlBuildStatus,
filter: {
platform,
status: graphqlBuildStatus,
distribution: graphqlBuildDistribution,
channel: flags.channel,
appVersion: flags.appVersion,
appBuildVersion: flags.appBuildVersion,
sdkVersion: flags.sdkVersion,
runtimeVersion: flags.runtimeVersion,
appIdentifier: flags.appIdentifier,
buildProfile: flags.buildProfile,
gitCommitHash: flags.gitCommitHash,
},
});
if (builds.length) {
@@ -64,11 +108,15 @@ export default class BuildList extends Command {
spinner.succeed(`Showing last ${builds.length} builds for the project ${projectName}`);
}
const list = builds
.map(build => formatGraphQLBuild(build))
.join(`\n\n${chalk.dim('———')}\n\n`);
if (json) {
printJsonOnlyOutput(builds);
} else {
const list = builds
.map(build => formatGraphQLBuild(build))
.join(`\n\n${chalk.dim('———')}\n\n`);
Log.log(`\n${list}`);
Log.log(`\n${list}`);
}
} else {
spinner.fail(`Couldn't find any builds for the project ${projectName}`);
}
@@ -89,9 +137,11 @@ const toAppPlatform = (requestedPlatform?: RequestedPlatform): AppPlatform | und
}
};
const toGraphQLBuildStatus = (buildStatus: BuildStatus): GraphQLBuildStatus | undefined => {
const toGraphQLBuildStatus = (buildStatus?: BuildStatus): GraphQLBuildStatus | undefined => {
if (!buildStatus) {
return undefined;
} else if (buildStatus === BuildStatus.NEW) {
return GraphQLBuildStatus.New;
} else if (buildStatus === BuildStatus.IN_QUEUE) {
return GraphQLBuildStatus.InQueue;
} else if (buildStatus === BuildStatus.IN_PROGRESS) {
@@ -104,3 +154,17 @@ const toGraphQLBuildStatus = (buildStatus: BuildStatus): GraphQLBuildStatus | un
return GraphQLBuildStatus.Canceled;
}
};
const toGraphQLBuildDistribution = (
buildDistribution?: BuildDistributionType
): DistributionType | undefined => {
if (buildDistribution === BuildDistributionType.STORE) {
return DistributionType.Store;
} else if (buildDistribution === BuildDistributionType.INTERNAL) {
return DistributionType.Internal;
} else if (buildDistribution === BuildDistributionType.SIMULATOR) {
return DistributionType.Simulator;
} else {
return undefined;
}
};
+20 -3
View File
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { Command } from '@oclif/command';
import { Command, flags } from '@oclif/command';
import ora from 'ora';
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
@@ -11,14 +11,27 @@ import {
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class BuildView extends Command {
static description = 'view a build for your project';
static args = [{ name: 'BUILD_ID' }];
static flags = {
json: flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
}),
};
async run() {
const { BUILD_ID: buildId } = this.parse(BuildView).args;
const {
args: { BUILD_ID: buildId },
flags,
} = this.parse(BuildView);
if (flags.json) {
enableJsonOutput();
}
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -47,7 +60,11 @@ export default class BuildView extends Command {
spinner.succeed(`Showing the last build for the project ${projectName}`);
}
Log.log(`\n${formatGraphQLBuild(build)}`);
if (flags.json) {
printJsonOnlyOutput(build);
} else {
Log.log(`\n${formatGraphQLBuild(build)}`);
}
} catch (err) {
if (buildId) {
spinner.fail(`Something went wrong and we couldn't fetch the build with id ${buildId}`);
@@ -16,6 +16,7 @@ import {
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import formatFields from '../../utils/formatFields';
import { createUpdateBranchOnAppAsync } from '../branch/create';
async function createUpdateChannelOnAppAsync({
@@ -87,7 +88,6 @@ export default class ChannelCreate extends Command {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!channelName) {
@@ -140,10 +140,26 @@ export default class ChannelCreate extends Command {
return;
}
Log.addNewLineIfNone();
Log.withTick(
`️Created a new channel ${chalk.bold(newChannel.name)} on project ${chalk.bold(
fullName
)}. ${branchMessage} and have pointed the channel at it. You can now update your app by publishing!`
`Created a new channel on project ${chalk.bold(await getProjectFullNameAsync(exp))}`
);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: newChannel.id },
])
);
Log.addNewLineIfNone();
Log.withTick(`${branchMessage} and have pointed the channel at it.`);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: branchId },
])
);
Log.addNewLineIfNone();
Log.log(chalk.bold('You can now update your app by publishing!'));
}
}
@@ -146,13 +146,6 @@ export default class ChannelEdit extends Command {
'There is a rollout in progress. Please manage it with "channel:rollout" instead.'
);
}
const existingBranch = existingChannel.updateBranches[0];
Log.addNewLineIfNone();
Log.log(
chalk`Channel {bold ${existingChannel.name}} is currently set to branch {bold ${existingBranch.name}}`
);
Log.addNewLineIfNone();
if (!branchName) {
const validationMessage = 'branch name may not be empty.';
@@ -185,6 +178,7 @@ export default class ChannelEdit extends Command {
Log.withTick(
chalk`Channel {bold ${channel.name}} is now set to branch {bold ${branch.name}}.\n`
);
Log.addNewLineIfNone();
Log.log(
chalk`Users with builds on channel {bold ${channel.name}} will now receive the active update on {bold ${branch.name}}.`
);
@@ -1,6 +1,5 @@
import { getConfig } from '@expo/config';
import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
@@ -10,6 +9,7 @@ import {
} from '../../graphql/generated';
import Log from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import formatFields from '../../utils/formatFields';
import { logChannelDetails } from './view';
const CHANNEL_LIMIT = 10_000;
@@ -99,9 +99,14 @@ export default class ChannelList extends Command {
return;
}
Log.log(); // spacing
for (const channel of channels) {
Log.log(`Details of channel ${chalk.bold(chalk.cyan(channel.name))}:`);
Log.addNewLineIfNone();
Log.log(
formatFields([
{ label: 'Name', value: channel.name },
{ label: 'ID', value: channel.id },
])
);
logChannelDetails(channel);
}
}
@@ -37,9 +37,7 @@ async function promptForRolloutPercentAsync({
return rolloutPercent;
}
function getRolloutInfo(
getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery
): {
function getRolloutInfo(getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery): {
newBranch: Pick<UpdateBranch, 'name' | 'id'>;
oldBranch: Pick<UpdateBranch, 'name' | 'id'>;
currentPercent: number;
@@ -48,12 +46,14 @@ function getRolloutInfo(
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.branchMapping
);
const [newBranchId, oldBranchId] = branchMapping.data.map(d => d.branchId);
const newBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === newBranchId
)[0];
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
const newBranch =
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === newBranchId
)[0];
const oldBranch =
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
if (!newBranch || !oldBranch) {
throw new Error(
@@ -140,9 +140,10 @@ async function startRolloutAsync({
branchMapping: JSON.stringify(newBranchMapping),
});
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
const oldBranch =
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
if (!oldBranch) {
throw new Error(
`Branch mapping is missing its only branch for channel "${channelName}" on app "${fullName}"`
+15 -11
View File
@@ -11,11 +11,7 @@ import {
GetChannelByNameForAppQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import {
FormatUpdateParameter,
@@ -23,6 +19,7 @@ import {
formatUpdate,
getPlatformsForGroup,
} from '../../update/utils';
import formatFields from '../../utils/formatFields';
export type BranchMapping = {
version: number;
@@ -40,9 +37,11 @@ export type BranchMapping = {
* Get the branch mapping and determine whether it is a rollout.
* Ensure that the branch mapping is properly formatted.
*/
export function getBranchMapping(
branchMappingString?: string
): { branchMapping: BranchMapping; isRollout: boolean; rolloutPercent?: number } {
export function getBranchMapping(branchMappingString?: string): {
branchMapping: BranchMapping;
isRollout: boolean;
rolloutPercent?: number;
} {
if (!branchMappingString) {
throw new Error('Missing branch mapping.');
}
@@ -220,7 +219,6 @@ export default class ChannelView extends Command {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!channelName) {
@@ -250,9 +248,15 @@ export default class ChannelView extends Command {
return;
}
Log.withTick(
chalk`Channel: {bold ${channel.name}} on project {bold ${fullName}}. Channel ID: {bold ${channel.id}}`
Log.addNewLineIfNone();
Log.log(chalk.bold('Channel:'));
Log.log(
formatFields([
{ label: 'Name', value: channel.name },
{ label: 'ID', value: channel.id },
])
);
Log.addNewLineIfNone();
Log.log(
chalk`{bold Branches, pointed at by this channel, and their most recent update group:}`
);
+67
View File
@@ -0,0 +1,67 @@
import { getProjectConfigDescription } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { Command, flags } from '@oclif/command';
import Log from '../log';
import { getExpoConfig } from '../project/expoConfig';
import { findProjectRootAsync } from '../project/projectUtils';
import { selectAsync } from '../prompts';
import { handleDeprecatedEasJsonAsync } from './build';
export default class Config extends Command {
static description = 'show the eas.json config';
static flags = {
platform: flags.enum({ char: 'p', options: ['android', 'ios'] }),
profile: flags.string(),
};
async run() {
const { flags } = this.parse(Config);
const { platform: maybePlatform, profile: maybeProfile } = flags as {
platform?: Platform;
profile?: string;
};
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
await handleDeprecatedEasJsonAsync(projectDir, false);
const reader = new EasJsonReader(projectDir);
const profileName =
maybeProfile ??
(await selectAsync(
'Select build profile',
(
await reader.getBuildProfileNamesAsync()
).map(profileName => ({
title: profileName,
value: profileName,
}))
));
const platform =
maybePlatform ??
(await selectAsync('Select platform', [
{
title: 'Android',
value: Platform.ANDROID,
},
{
title: 'iOS',
value: Platform.IOS,
},
]));
const profile = await reader.readBuildProfileAsync(platform, profileName);
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
Log.log(getProjectConfigDescription(projectDir));
Log.newLine();
Log.log(JSON.stringify(config, null, 2));
Log.newLine();
Log.newLine();
Log.log(`Build profile "${profileName}" from eas.json for platform ${platform}`);
Log.newLine();
Log.log(JSON.stringify(profile, null, 2));
}
}
+4 -6
View File
@@ -1,14 +1,12 @@
import { Command } from '@oclif/command';
import { CredentialsManager } from '../credentials/CredentialsManager';
import EasCommand from '../commandUtils/EasCommand';
import { createCredentialsContextAsync } from '../credentials/context';
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
export default class Credentials extends Command {
static description = 'Manage your credentials';
export default class Credentials extends EasCommand {
static description = 'manage your credentials';
async run() {
const ctx = await createCredentialsContextAsync(process.cwd(), {});
new CredentialsManager(ctx).runActionAsync(new SelectPlatform());
await new SelectPlatform().runAsync(ctx);
}
}
@@ -45,7 +45,6 @@ If you are not sure what is the UDID of the device you are looking for, run:
spinner.fail(`Couldn't find a device with the UDID ${UDID}`);
}
} catch (e) {
console.log(e);
spinner.fail(`Something went wrong and we couldn't fetch the device with UDID ${UDID}`);
throw e;
}
@@ -0,0 +1,34 @@
import { Command } from '@oclif/command';
import envinfo from 'envinfo';
import Log from '../log';
const packageJSON = require('../../package.json');
export default class Diagnostics extends Command {
static description = 'log environment info to the console';
async run() {
const info = await envinfo.run(
{
System: ['OS', 'Shell'],
Binaries: ['Node', 'Yarn', 'npm'],
Utilities: ['Git'],
npmPackages: [
'expo',
'react',
'react-dom',
'react-native',
'react-native-web',
'react-navigation',
'@expo/webpack-config',
],
npmGlobalPackages: ['eas-cli'],
},
{
title: `EAS CLI ${packageJSON.version} environment info`,
}
);
Log.log(info);
}
}
@@ -0,0 +1,56 @@
import { getConfig } from '@expo/config';
import { Command } from '@oclif/command';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { AppInfoQuery, AppInfoQueryVariables } from '../../graphql/generated';
import Log from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import formatFields from '../../utils/formatFields';
async function projectInfoByIdAsync(appId: string): Promise<AppInfoQuery> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppInfoQuery, AppInfoQueryVariables>(
gql`
query AppInfo($appId: String!) {
app {
byId(appId: $appId) {
id
fullName
}
}
}
`,
{ appId }
)
.toPromise()
);
return data;
}
export default class ProjectInfo extends Command {
static description = 'information about the current project';
async run() {
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const { app } = await projectInfoByIdAsync(projectId);
if (!app) {
throw new Error(`Could not find project with ID: ${projectId}`);
}
Log.addNewLineIfNone();
Log.log(
formatFields([
{ label: 'fullName', value: app.byId.fullName },
{ label: 'ID', value: projectId },
])
);
}
}
@@ -2,12 +2,12 @@ import { getConfig } from '@expo/config';
import { Command } from '@oclif/command';
import chalk from 'chalk';
import Log from '../log';
import { findProjectRootAsync, setProjectIdAsync } from '../project/projectUtils';
import Log from '../../log';
import { findProjectRootAsync, setProjectIdAsync } from '../../project/projectUtils';
export default class InitView extends Command {
static hidden = true;
static description = 'Create or link an EAS project';
export default class ProjectInit extends Command {
static description = 'create or link an EAS project';
static aliases = ['init'];
async run() {
const projectDir = await findProjectRootAsync(process.cwd());
+43 -7
View File
@@ -3,6 +3,10 @@ import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
import {
EnvironmentSecretScope,
EnvironmentSecretsQuery,
} from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log';
import {
isEasEnabledForProjectAsync,
@@ -18,10 +22,6 @@ import { findAccountByName } from '../../user/Account';
import { getActorDisplayName } from '../../user/User';
import { ensureLoggedInAsync } from '../../user/actions';
export enum EnvironmentSecretScope {
ACCOUNT = 'account',
PROJECT = 'project',
}
export default class EnvironmentSecretCreate extends Command {
static description = 'Create an environment secret on the current project or owner account.';
@@ -37,12 +37,16 @@ export default class EnvironmentSecretCreate extends Command {
value: flags.string({
description: 'Value of the secret',
}),
force: flags.boolean({
description: 'Delete and recreate existing secrets',
default: false,
}),
};
async run() {
const actor = await ensureLoggedInAsync();
let {
flags: { name, value: secretValue, scope },
flags: { name, value: secretValue, scope, force },
} = this.parse(EnvironmentSecretCreate);
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
@@ -93,7 +97,9 @@ export default class EnvironmentSecretCreate extends Command {
},
}));
if (!name) throw new Error('Secret name may not be empty.');
if (!name) {
throw new Error('Secret name may not be empty.');
}
}
if (!secretValue) {
@@ -106,10 +112,26 @@ export default class EnvironmentSecretCreate extends Command {
validate: value => (value ? true : validationMessage),
}));
if (!secretValue) throw new Error(validationMessage);
if (!secretValue) {
throw new Error(validationMessage);
}
}
if (scope === EnvironmentSecretScope.PROJECT) {
if (force) {
const existingSecrets = await EnvironmentSecretsQuery.byAppIdAsync(projectId);
const existingSecret = existingSecrets.find(secret => secret.name === name);
if (existingSecret) {
await EnvironmentSecretMutation.delete(existingSecret.id);
Log.withTick(
`Deleting existing secret ${chalk.bold(name)} on project ${chalk.bold(
`@${accountName}/${slug}`
)}.`
);
}
}
const secret = await EnvironmentSecretMutation.createForApp(
{ name, value: secretValue },
projectId
@@ -136,6 +158,20 @@ export default class EnvironmentSecretCreate extends Command {
return;
}
if (force) {
const existingSecrets = await EnvironmentSecretsQuery.byAcccountNameAsync(projectId);
const existingSecret = existingSecrets.find(secret => secret.name === name);
if (existingSecret) {
await EnvironmentSecretMutation.delete(existingSecret.id);
Log.withTick(
`Deleting existing secret ${chalk.bold(name)} on account ${chalk.bold(
ownerAccount.name
)}.`
);
}
}
const secret = await EnvironmentSecretMutation.createForAccount(
{ name, value: secretValue },
ownerAccount.id
@@ -4,6 +4,7 @@ import chalk from 'chalk';
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
import {
EnvironmentSecretScope,
EnvironmentSecretWithScope,
EnvironmentSecretsQuery,
} from '../../graphql/queries/EnvironmentSecretsQuery';
@@ -15,12 +16,10 @@ import {
import {
findProjectRootAsync,
getProjectAccountNameAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions';
import { EnvironmentSecretScope } from './create';
export default class EnvironmentSecretDelete extends Command {
static description = `Delete an environment secret by ID.
@@ -38,7 +37,6 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectFullName = await getProjectFullNameAsync(exp);
const projectAccountName = await getProjectAccountNameAsync(exp);
if (!(await isEasEnabledForProjectAsync(projectId))) {
@@ -55,7 +53,7 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
if (!id) {
const validationMessage = 'You must select which secret to delete.';
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectFullName);
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectId);
({ secret } = await promptAsync({
type: 'autocomplete',
@@ -69,7 +67,9 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
id = secret?.id;
if (!id) throw new Error(validationMessage);
if (!id) {
throw new Error(validationMessage);
}
}
Log.addNewLineIfNone();
+1 -3
View File
@@ -13,7 +13,6 @@ import {
import {
findProjectRootAsync,
getProjectAccountNameAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
@@ -27,7 +26,6 @@ export default class EnvironmentSecretList extends Command {
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectFullName = await getProjectFullNameAsync(exp);
const projectAccountName = await getProjectAccountNameAsync(exp);
if (!(await isEasEnabledForProjectAsync(projectId))) {
@@ -40,7 +38,7 @@ export default class EnvironmentSecretList extends Command {
throw new Error("Please run this command inside your project's directory");
}
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectFullName);
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectId);
const table = new Table({
head: ['Name', 'Scope', 'ID', 'Updated at'],
+141 -171
View File
@@ -1,170 +1,81 @@
import { getConfig } from '@expo/config';
import { Command, flags } from '@oclif/command';
import { Platform } from '@expo/eas-build-job';
import { EasJsonReader, IosSubmitProfile } from '@expo/eas-json';
import { flags } from '@oclif/command';
import { exit } from '@oclif/errors';
import chalk from 'chalk';
import { AppPlatform } from '../graphql/generated';
import { learnMore } from '../log';
import EasCommand from '../commandUtils/EasCommand';
import { AppPlatform, SubmissionFragment, SubmissionStatus } from '../graphql/generated';
import Log, { learnMore } from '../log';
import { appPlatformDisplayNames, appPlatformEmojis } from '../platform';
import { isEasEnabledForProjectAsync, warnEasUnavailable } from '../project/isEasEnabledForProject';
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
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';
import { SubmitArchiveFlags } from '../submissions/types';
import { displayLogsAsync } from '../submissions/utils/logs';
import { printSubmissionDetailsUrls } from '../submissions/utils/urls';
import { waitForSubmissionsEndAsync } from '../submissions/utils/wait';
const COMMON_FLAGS = '';
const ANDROID_FLAGS = 'Android specific options';
const IOS_FLAGS = 'iOS specific options';
interface Flags {
verbose: boolean;
wait: boolean;
platform?: Platform;
profile?: string;
archiveFlags: SubmitArchiveFlags;
}
export default class BuildSubmit extends Command {
static description = 'Submits build artifact to app store';
static usage = 'submit --platform=(android|ios)';
export default class BuildSubmit extends EasCommand {
static description = `submit build archive to app store
See how to configure submits with eas.json: ${learnMore('https://docs.expo.dev/submit/eas-json/', {
learnMoreMessage: '',
})}`;
static aliases = ['build:submit'];
static examples = [
`$ eas submit --platform=ios
- Fully interactive iOS submission\n`,
`$ eas submit --platform=android
- Fully interactive Android submission\n`,
`$ eas submit -p android --latest --key=/path/to/google-services.json
- Minimal non-interactive Android submission, however it can ask you for other params if not specified\n`,
`$ EXPO_APPLE_APP_SPECIFIC_PASSWORD=xxx eas submit -p ios --latest [email protected] --asc-app-id=1234567890,
- Minimal non-interactive iOS submission, assuming you already have an app in App Store Connect
and provide its App ID`,
];
static flags = {
platform: flags.enum({
char: 'p',
description: 'For which platform you want to submit a build',
options: ['android', 'ios'],
helpLabel: COMMON_FLAGS,
}),
/* Common flags for both platforms */
profile: flags.string({
description:
'Name of the submit profile from eas.json. Defaults to "release" if defined in eas.json.',
}),
latest: flags.boolean({
description: 'Submit the latest build for specified platform',
exclusive: ['id', 'path', 'url'],
default: false,
helpLabel: COMMON_FLAGS,
}),
id: flags.string({
description: 'ID of the build to submit',
exclusive: ['latest, path, url'],
helpLabel: COMMON_FLAGS,
}),
path: flags.string({
description: 'Path to the .apk/.aab file',
description: 'Path to the .apk/.aab/.ipa file',
exclusive: ['latest', 'id', 'url'],
helpLabel: COMMON_FLAGS,
}),
url: flags.string({
description: 'App archive url',
exclusive: ['latest', 'id', 'path'],
helpLabel: COMMON_FLAGS,
}),
verbose: flags.boolean({
description: 'Always print logs from Submission Service',
default: false,
helpLabel: COMMON_FLAGS,
}),
/* Android specific flags */
type: flags.enum<'apk' | 'aab'>({
description: 'Android archive type',
options: ['apk', 'aab'],
helpLabel: ANDROID_FLAGS,
}),
key: flags.string({
description: 'Path to the JSON key used to authenticate with Google Play',
helpLabel: ANDROID_FLAGS,
}),
'android-package': flags.string({
description: 'Android package name (default: expo.android.package from app config)',
helpLabel: ANDROID_FLAGS,
}),
track: flags.enum({
description: 'The track of the application to use',
default: 'internal',
options: ['production', 'beta', 'alpha', 'internal', 'rollout'],
helpLabel: ANDROID_FLAGS,
}),
'release-status': flags.enum({
description: 'Release status (used when uploading new APKs/AABs)',
default: 'completed',
options: ['completed', 'draft', 'halted', 'inProgress'],
helpLabel: ANDROID_FLAGS,
}),
/* iOS specific flags */
'apple-id': flags.string({
description: 'Your Apple ID username (you can also set EXPO_APPLE_ID env variable)',
helpLabel: IOS_FLAGS,
}),
'asc-app-id': flags.string({
description: `App Store Connect unique application Apple ID number. Providing this param results in skipping app creation step. ${learnMore(
'https://expo.fyi/asc-app-id'
)}`,
helpLabel: IOS_FLAGS,
}),
'apple-team-id': flags.string({
description: 'Your Apple Developer Team ID',
helpLabel: IOS_FLAGS,
}),
'app-name': flags.string({
description:
'The name of your app as it will appear on the App Store (default: expo.name from app config)',
helpLabel: IOS_FLAGS,
}),
'bundle-identifier': flags.string({
description:
'Your iOS Bundle Identifier (default: expo.ios.bundleIdentifier from app config)',
helpLabel: IOS_FLAGS,
}),
sku: flags.string({
description:
'An unique ID for your app that is not visible on the App Store, will be generated unless provided',
helpLabel: IOS_FLAGS,
}),
language: flags.string({
description: 'Primary language (e.g. English, German, ...)',
default: 'en-US',
helpLabel: IOS_FLAGS,
}),
'company-name': flags.string({
description:
'The name of your company, needed only for the first upload of any app to App Store',
helpLabel: IOS_FLAGS,
wait: flags.boolean({
description: 'Wait for submission to complete',
default: true,
allowNo: true,
}),
};
async run(): Promise<void> {
const {
flags: {
// android
'android-package': androidPackage,
'release-status': releaseStatus,
// ios
'apple-id': appleId,
'asc-app-id': ascAppId,
'apple-team-id': appleTeamId,
'app-name': appName,
'bundle-identifier': bundleIdentifier,
'company-name': companyName,
// common
...flags
},
} = this.parse(BuildSubmit);
await ensureLoggedInAsync();
const flags = this.parseFlags();
const platform =
(flags.platform?.toUpperCase() as AppPlatform | undefined) ??
(await promptForPlatformAsync());
(await this.promptForPlatformAsync());
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -176,56 +87,115 @@ export default class BuildSubmit extends Command {
return;
}
// TODO: Make this work outside project dir
if (!projectDir) {
throw new Error("Please run this command inside your project's directory");
const easJsonReader = new EasJsonReader(projectDir);
const submissions: SubmissionFragment[] = [];
let iosSubmitProfile: IosSubmitProfile | null = null;
if (platform === AppPlatform.Android) {
const submitProfile = await easJsonReader.readSubmitProfileAsync(
Platform.ANDROID,
flags.profile
);
const ctx = AndroidSubmitCommand.createContext({
projectDir,
projectId,
profile: submitProfile,
archiveFlags: flags.archiveFlags,
});
const command = new AndroidSubmitCommand(ctx);
submissions.push(await command.runAsync());
} else {
iosSubmitProfile = await easJsonReader.readSubmitProfileAsync(Platform.IOS, flags.profile);
const ctx = IosSubmitCommand.createContext({
projectDir,
projectId,
profile: iosSubmitProfile,
archiveFlags: flags.archiveFlags,
});
const command = new IosSubmitCommand(ctx);
submissions.push(await command.runAsync());
}
if (platform === AppPlatform.Android) {
const options: AndroidSubmitCommandFlags = {
androidPackage,
releaseStatus,
...flags,
};
Log.newLine();
printSubmissionDetailsUrls(submissions);
const ctx = AndroidSubmitCommand.createContext(projectDir, projectId, options);
const command = new AndroidSubmitCommand(ctx);
await command.runAsync();
} else if (platform === AppPlatform.Ios) {
const options: IosSubmitCommandFlags = {
appleId,
ascAppId,
appleTeamId,
appName,
bundleIdentifier,
companyName,
...flags,
};
if (flags.wait) {
Log.newLine();
const completedSubmissions = await waitForSubmissionsEndAsync(submissions);
for (const submission of completedSubmissions) {
if (completedSubmissions.length > 1) {
Log.log(
`${appPlatformEmojis[submission.platform]}${chalk.bold(
`${appPlatformDisplayNames[submission.platform]} submission`
)}`
);
}
if (
submission.platform === AppPlatform.Ios &&
submission.status === SubmissionStatus.Finished
) {
const logMsg = [
chalk.bold('Your binary has been successfully uploaded to App Store Connect!'),
'- It is now being processed by Apple - you will receive an e-mail when the processing finishes.',
'- It usually takes about 5-10 minutes depending on how busy Apple servers are.',
// ascAppIdentifier should be always available for ios submissions but check it anyway
submission.iosConfig?.ascAppIdentifier &&
`- When it’s done, you can see your build here: ${learnMore(
`https://appstoreconnect.apple.com/apps/${submission.iosConfig?.ascAppIdentifier}/appstore/ios`,
{ learnMoreMessage: '' }
)}`,
].join('\n');
Log.addNewLineIfNone();
Log.log(logMsg);
}
await displayLogsAsync(submission, { verbose: flags.verbose });
if (completedSubmissions.length > 1) {
Log.newLine();
}
}
this.exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
}
}
const ctx = IosSubmitCommand.createContext(projectDir, projectId, options);
const command = new IosSubmitCommand(ctx);
await command.runAsync();
} else {
throw new Error(`Unsupported platform: ${platform}!`);
private parseFlags(): Flags {
const {
flags: { platform, profile, verbose, wait, ...archiveFlags },
} = this.parse(BuildSubmit);
return {
platform: platform as Platform,
profile,
verbose,
wait,
archiveFlags,
};
}
private async promptForPlatformAsync(): Promise<AppPlatform> {
const { platform } = await promptAsync({
type: 'select',
message: 'Submit to platform',
name: 'platform',
choices: [
{
title: 'iOS',
value: AppPlatform.Ios,
},
{
title: 'Android',
value: AppPlatform.Android,
},
],
});
return platform;
}
private exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(submissions: SubmissionFragment[]): void {
const nonFinishedSubmissions = submissions.filter(
({ status }) => status !== SubmissionStatus.Finished
);
if (nonFinishedSubmissions.length > 0) {
exit(1);
}
}
}
async function promptForPlatformAsync(): Promise<AppPlatform> {
const { platform } = await promptAsync({
type: 'select',
message: 'Submit to platform',
name: 'platform',
choices: [
{
title: 'iOS',
value: AppPlatform.Ios,
},
{
title: 'Android',
value: AppPlatform.Android,
},
],
});
return platform;
}
@@ -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,7 +0,0 @@
import { getAndroidApiMock } from '../__tests__/fixtures-android';
import { createCtxMock } from '../__tests__/fixtures-context';
export const createCredentialsContextAsync = () =>
createCtxMock({
android: getAndroidApiMock(),
});
@@ -1,77 +0,0 @@
import {
AndroidAppBuildCredentialsFragment,
AndroidKeystoreFragment,
AndroidKeystoreType,
AppFragment,
CommonAndroidAppCredentialsFragment,
} from '../../graphql/generated';
import { testKeystore } from './fixtures-android';
import { testKeystoreBase64 } from './fixtures-base64-data';
export const testAppFragment: AppFragment = {
id: 'test-app-id',
fullName: '@testuser/testapp',
slug: 'testapp',
};
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,
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,
androidAppBuildCredentialsList: [testLegacyAndroidBuildCredentialsFragment],
};
export function getNewAndroidApiMockWithoutCredentials() {
return {
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(),
getAndroidAppBuildCredentialsListAsync: jest.fn(() => []),
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(),
getLegacyAndroidAppBuildCredentialsAsync: jest.fn(),
createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync: jest.fn(),
updateAndroidAppBuildCredentialsAsync: jest.fn(),
createAndroidAppBuildCredentialsAsync: jest.fn(),
getDefaultAndroidAppBuildCredentialsAsync: jest.fn(),
getAndroidAppBuildCredentialsByNameAsync: jest.fn(),
createOrUpdateAndroidAppBuildCredentialsByNameAsync: jest.fn(),
createKeystoreAsync: jest.fn(),
};
}
@@ -1,11 +1,21 @@
import { AndroidCredentials, Keystore } from '../android/credentials';
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';
import { testExperienceName, testJester2ExperienceName } from './fixtures-constants';
const now = new Date();
export const testKeystore: Keystore = {
keystore: testKeystoreBase64,
@@ -39,56 +49,88 @@ export const testPKCS12EmptyPasswordKeystore: Keystore = {
keyAlias: 'test-alias',
};
export const testPushCredentials = {
fcmApiKey: 'examplefcmapikey',
export const testAppFragment: AppFragment = {
id: 'test-app-id',
fullName: '@testuser/testapp',
slug: 'testapp',
};
export const testAppCredentials = {
experienceName: testExperienceName,
keystore: testKeystore,
pushCredentials: testPushCredentials,
export const testLegacyAndroidFcmFragment: AndroidFcmFragment = {
id: 'test-id',
snippet: {
firstFourCharacters: 'abcd',
lastFourCharacters: 'efgh',
},
credential: 'abcdxxxxxxefgh',
version: AndroidFcmVersion.Legacy,
createdAt: now,
updatedAt: now,
};
export const testJester2AppCredentials = {
experienceName: testJester2ExperienceName,
keystore: testKeystore2,
pushCredentials: testPushCredentials,
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: now,
updatedAt: now,
};
export const testAllCredentials: { [key: string]: AndroidCredentials } = {
[testExperienceName]: testAppCredentials,
export const testLegacyAndroidBuildCredentialsFragment: AndroidAppBuildCredentialsFragment = {
id: 'test-id',
isDefault: true,
isLegacy: true,
name: 'legacy',
androidKeystore: testJksAndroidKeystoreFragment,
};
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 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(),
};
}
@@ -1,18 +1,15 @@
import merge from 'lodash/merge';
import { CredentialsManager } from '../CredentialsManager';
import { Context } from '../context';
import { getAndroidApiMockWithoutCredentials } from './fixtures-android';
import { getNewAndroidApiMockWithoutCredentials } from './fixtures-android-new';
import { getNewAndroidApiMock } from './fixtures-android';
import { getAppstoreMock } from './fixtures-appstore';
import { testAppJson, testUsername } from './fixtures-constants';
import { getNewIosApiMockWithoutCredentials } from './fixtures-ios';
import { getNewIosApiMock } from './fixtures-ios';
export function createCtxMock(mockOverride: Record<string, any> = {}): Context {
const defaultMock = {
ios: getNewIosApiMockWithoutCredentials(),
android: getAndroidApiMockWithoutCredentials(),
newAndroid: getNewAndroidApiMockWithoutCredentials(),
ios: getNewIosApiMock(),
android: getNewAndroidApiMock(),
appStore: getAppstoreMock(),
bestEffortAppStoreAuthenticateAsync: jest.fn(),
ensureAppleCtx: jest.fn(),
@@ -29,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,20 +3,17 @@ import {
AppleAppIdentifierFragment,
AppleDistributionCertificateFragment,
AppleProvisioningProfileFragment,
ApplePushKeyFragment,
AppleTeamFragment,
CommonIosAppCredentialsFragment,
IosAppBuildCredentialsFragment,
IosDistributionType,
} from '../../graphql/generated';
import { IosAppCredentialsWithBuildCredentialsQueryResult } from '../ios/api/graphql/queries/IosAppCredentialsQuery';
import {
DistributionCertificate,
ProvisioningProfile,
PushKey,
} 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 now = new Date();
export const testProvisioningProfile: ProvisioningProfile = {
provisioningProfileId: 'test-id',
@@ -40,12 +37,22 @@ export const testAppFragment: AppFragment = {
slug: 'testapp',
};
export const testPushKey: ApplePushKeyFragment = {
id: 'test-push-key-id',
keyIdentifier: 'test-key-identifier',
appleTeam: { ...testAppleTeamFragment },
updatedAt: now,
iosAppCredentialsList: [],
};
export const testTargets: Target[] = [{ targetName: 'testapp', bundleIdentifier: 'foo.bar.com' }];
export const testProvisioningProfileFragment: AppleProvisioningProfileFragment = {
id: 'test-prov-prof-id-1',
expiration: new Date(),
expiration: now,
developerPortalIdentifier: 'test-developer-identifier',
provisioningProfile: testProvisioningProfileBase64,
updatedAt: new Date(),
updatedAt: now,
status: 'Active',
appleTeam: { ...testAppleTeamFragment },
appleDevices: [],
@@ -57,9 +64,9 @@ export const testDistCertFragmentNoDependencies: AppleDistributionCertificateFra
certificatePassword: 'test-password',
serialNumber: 'test-serial',
developerPortalIdentifier: 'test-developer-identifier',
validityNotAfter: new Date(),
validityNotBefore: new Date(),
updatedAt: new Date(),
validityNotAfter: now,
validityNotBefore: now,
updatedAt: now,
appleTeam: { ...testAppleTeamFragment },
iosAppBuildCredentialsList: [],
};
@@ -70,9 +77,9 @@ export const testDistCertFragmentOneDependency: AppleDistributionCertificateFrag
certificatePassword: 'test-password',
serialNumber: 'test-serial',
developerPortalIdentifier: 'test-developer-identifier',
validityNotAfter: new Date(),
validityNotBefore: new Date(),
updatedAt: new Date(),
validityNotAfter: now,
validityNotBefore: now,
updatedAt: now,
appleTeam: { ...testAppleTeamFragment },
iosAppBuildCredentialsList: [
{
@@ -102,11 +109,7 @@ export const testCommonIosAppCredentialsFragment: CommonIosAppCredentialsFragmen
app: testAppFragment,
appleTeam: testAppleTeamFragment,
appleAppIdentifier: testAppleAppIdentifierFragment,
iosAppBuildCredentialsList: [testIosAppBuildCredentialsFragment],
};
export const testIosAppCredentialsWithBuildCredentialsQueryResult: IosAppCredentialsWithBuildCredentialsQueryResult = {
id: 'test-app-credential-id',
pushKey: testPushKey,
iosAppBuildCredentialsList: [testIosAppBuildCredentialsFragment],
};
@@ -114,11 +117,13 @@ export const testAppleTeam = {
id: 'test-team-id',
};
export function getNewIosApiMockWithoutCredentials() {
export function getNewIosApiMock() {
return {
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(),
createOrGetIosAppCredentialsWithCommonFieldsAsync: jest.fn(),
updateIosAppCredentialsAsync: jest.fn(),
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
createOrGetExistingAppleTeamAsync: () => testAppleTeam,
createOrGetExistingAppleAppIdentifierAsync: jest.fn(),
getDevicesForAppleTeamAsync: jest.fn(),
@@ -130,25 +135,9 @@ export function getNewIosApiMockWithoutCredentials() {
getDistributionCertificatesForAccountAsync: jest.fn(),
createDistributionCertificateAsync: jest.fn(),
deleteDistributionCertificateAsync: jest.fn(),
};
}
export function getNewIosApiMockWithCredentials() {
return {
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(),
createPushKeyAsync: jest.fn(),
getPushKeyForAppAsync: jest.fn(),
deletePushKeyAsync: jest.fn(),
};
}
@@ -158,39 +147,3 @@ export const testDistCert: DistributionCertificate = {
distCertSerialNumber: 'test-serial',
teamId: 'test-team-id',
};
export const testIosDistCredential: IosDistCredentials = {
id: 1,
type: 'dist-cert',
...testDistCert,
};
export const testPushKey: PushKey = {
apnsKeyP8: 'test-p8',
apnsKeyId: 'test-key-id',
teamId: 'test-team-id',
};
/**
* Legacy format fixtures
*/
export const testIosPushCredential: IosPushCredentials = {
id: 2,
type: 'push-key',
...testPushKey,
};
export const testAppCredential = {
experienceName: testExperienceName,
bundleIdentifier: testBundleIdentifier,
distCredentialsId: testIosDistCredential.id,
pushCredentialsId: testIosPushCredential.id,
credentials: {
...testProvisioningProfile,
},
};
export const testAllCredentialsForApp = {
...testAppCredential,
pushCredentials: testPushKey,
distCredentials: testDistCert,
};
@@ -1,24 +1,20 @@
import { Platform } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import nullthrows from 'nullthrows';
import { CredentialsManager } from '../CredentialsManager';
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 {
@@ -26,11 +22,6 @@ export default class AndroidCredentialsProvider {
constructor(private ctx: Context, private options: Options) {}
private get projectFullName(): string {
const { projectName, accountName } = this.options.app;
return `@${accountName}/${projectName}`;
}
public async getCredentialsAsync(
src: CredentialsSource.LOCAL | CredentialsSource.REMOTE
): Promise<AndroidCredentials> {
@@ -43,14 +34,22 @@ export default class AndroidCredentialsProvider {
}
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,27 @@
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;
}
}
@@ -1,14 +1,89 @@
import assert from 'assert';
import { nanoid } from 'nanoid';
import ora from 'ora';
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
import { getApplicationIdAsync } 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';
export function getAppLookupParamsFromContext(ctx: Context): AppLookupParams {
/**
* 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 async function getAppLookupParamsFromContextAsync(ctx: Context): Promise<AppLookupParams> {
ctx.ensureProjectContext();
const projectName = ctx.exp.slug;
const accountName = getProjectAccountName(ctx.exp, ctx.user);
@@ -17,7 +92,7 @@ export function getAppLookupParamsFromContext(ctx: Context): AppLookupParams {
throw new Error(`You do not have access to account: ${accountName}`);
}
const androidApplicationIdentifier = ctx.exp.android?.package;
const androidApplicationIdentifier = await getApplicationIdAsync(ctx.projectDir, ctx.exp);
if (!androidApplicationIdentifier) {
throw new Error(
`android.package needs to be defined in your ${getProjectConfigDescription(
@@ -42,15 +117,22 @@ export async function createOrUpdateDefaultAndroidAppBuildCredentialsAsync(
!ctx.nonInteractive,
'createOrUpdateDefaultAndroidAppBuildCredentialsAsync must be run in interactive mode'
);
const existingDefaultBuildCredentials = await ctx.newAndroid.getDefaultAndroidAppBuildCredentialsAsync(
appLookupParams
);
const existingDefaultBuildCredentials =
await ctx.android.getDefaultAndroidAppBuildCredentialsAsync(appLookupParams);
if (existingDefaultBuildCredentials) {
return await ctx.newAndroid.updateAndroidAppBuildCredentialsAsync(
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',
@@ -58,10 +140,23 @@ export async function createOrUpdateDefaultAndroidAppBuildCredentialsAsync(
initial: generateRandomName(),
validate: (input: string) => input !== '',
});
return await ctx.newAndroid.createAndroidAppBuildCredentialsAsync(appLookupParams, {
name: providedName,
isDefault: true,
androidKeystoreId,
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);
});
}
Loaded 100 of 329 files, more files were not shown because too many files have changed in this diff. Show more