Compare commits

..
Author SHA1 Message Date
Quinlan Jung dc0b3b1237 [eas-cli] remove asp prompt 2021-11-05 16:15:33 -07:00
Quinlan Jung 0e6254453a [eas-cli] integrate ascApiKey with submissions workflow (#737)
* [eas-cli] integrate ascApiKey with submissions workflow

* fix failing stuff

* update CHANGELOG.md

* barthap pr feedback

* dominik pr feedback
2021-11-05 13:30:16 -07:00
Quinlan Jung 587a29892f fix changelog 2021-11-05 13:28:03 -07:00
Quinlan Jung db8245d954 [eas-cli] setup -> set up (#735)
* [eas-cli] setup -> set up

* fix stuff

* update CHANGELOG.md

* fix merge errors
2021-11-05 11:59:44 -07:00
Bartłomiej Klocek 0088306bca [eas-cli] Handle Apple servers maintenance in eas submit (#738)
* [eas-cli] Handle Apple servers maintenance in `eas submit`

* update CHANGELOG.md
2021-11-05 11:08:30 +01:00
Wojciech KozyraandDominik Sokal b6fc718713 [eas-cli] improve vcs migration experience (#732)
* [eas-cli] improve vcs migration experience

* update CHANGELOG.md

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-05 10:40:00 +01:00
Quinlan Jung 18e55dafd4 [eas-cli] setup asc api key (#733)
* [eas-cli] setup asc api key

* setup -> set up

* pr feedback

* update CHANGELOG.md
2021-11-04 14:52:25 -07:00
Bartłomiej Klocek 8e013ab793 [eas-cli] Use actor displayName when printing build info (#730)
* [eas-cli] Use actor `displayName` when printing build info

* update CHANGELOG.md
2021-11-04 07:50:03 +01:00
Quinlan JungandDominik Sokal b88e79f0f3 [eas-cli] cleanup up creds prompt method (#728)
* [eas-cli] cleanup up creds prompt method

* update CHANGELOG.md

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-03 13:19:10 -07:00
Ryan Blunden 35b505120d Replace eas:secrets with eas:secret (#727) 2021-11-03 09:44:19 +01:00
Brent VatneandDominik Sokal 6807d3080b [eas-cli] Bump @expo/apple-utils to better handle some error messages (#723)
* [eas-cli] Bump @expo/apple-utils to better handle some error messages

* Update CHANGELOG

* Update CHANGELOG.md

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-03 09:35:11 +01:00
Quinlan JungandDominik Sokal 0d1cfb3286 [eas-cli] support removal of asc api keys (#721)
* [eas-cli] support removal of asc api keys

* update CHANGELOG.md

* Update packages/eas-cli/src/credentials/ios/actions/AscApiKeyUtils.ts

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

* Update packages/eas-cli/src/credentials/ios/actions/RemoveAscApiKey.ts

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-02 21:03:34 -07:00
Quinlan Jung 0b9a5621fe fix changelog 2021-11-02 20:57:46 -07:00
Quinlan Jung 9bd15af02a [eas-cli] assign asc api keys to project (#719)
* [eas-cli] assign asc api keys to project

assign asc test

* fix compilation errors
2021-11-02 20:51:15 -07:00
Quinlan Jung aab3bf1330 [eas-cli] create + validate asc api key, infer issuer ids (#718)
* [eas-cli] create + validate asc api key, infer issuer ids

create asc tests

create asc key:

ascapikeyutils test

* update CHANGELOG.md

* some pr feedback

* API -> Api

* more pr feedbakc

* fix tsc
2021-11-02 20:41:17 -07:00
Expo CI 9e95218523 update CHANGELOG.md 2021-11-02 17:27:32 +00:00
Brent Vatne bb98e6de03 v0.34.1 2021-11-02 10:15:45 -07:00
Dominik Sokal 2604228233 add clean script 2021-11-02 13:41:47 +01:00
Dominik Sokal a7035d3469 clean up package.json 2021-11-02 13:37:00 +01:00
Dominik Sokal f6dce0e17c update CHANGELOG.md 2021-11-02 13:31:56 +01:00
Wojciech KozyraandDominik Sokal ccfa6f9fc3 [eas-cli] check if commit is required after dev client install (#722)
* check if commit is required after dev client install

* update CHANGELOG.md

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-02 12:51:49 +01:00
Dominik Sokal 0ee8e2f967 Update CONTRIBUTING.md 2021-11-02 11:29:27 +01:00
Quinlan Jung 94dc4c6e93 [eas-cli] asc api key graphql type and print support (#717)
* [eas-cli] asc api key graphql type and print support

* gql

* gql

* update CHANGELOG.md
2021-11-01 14:56:07 -07:00
Brent Vatne 4144e387d6 [eas-cli] Add missing period 2021-11-01 11:39:15 -07:00
Expo CI de4e05e1e0 update CHANGELOG.md 2021-11-01 18:30:08 +00:00
Brent Vatne 36118b700f v0.34.0 2021-11-01 11:23:56 -07:00
Dominik Sokal 2fb947b47a rename useDevelopmentClient to developmentClient (#711)
* generate graphql code

* rename useDevelopmentClient to developmentClient

* update CHANGELOG.md

* upgrade @expo/eas-build-job
2021-10-31 18:51:26 +01:00
gregfentonandWojciech Kozyra 0f1156e951 [eas-cli] correct warning message text (#715) (#716)
* [eas-cli] correct warning message text (#715)

* (minor) Incorrect instructions in warning message
Fixes #715

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

Co-authored-by: Wojciech Kozyra <[email protected]>
2021-10-29 10:47:14 +02:00
Quinlan Jung e9f8be92b7 [eas-cli] fix graphql (#713)
* [eas-cli] fix graphql

* update CHANGELOG.md
2021-10-28 11:51:40 -07:00
Quinlan Jung 6360ea9f8e [eas-cli] refactor credentials manager (#712)
* [eas-cli] refactor android manager

* refactor ios

* update CHANGELOG.md
2021-10-28 11:50:08 -07:00
Wojciech Kozyra ad2d8e80f0 add no commit workflow (#695) 2021-10-28 11:40:11 +02:00
Quinlan Jung bc0a5fdaa6 [eas-cli] remove unneeded parameters (#710)
* [eas-cli] remove unneeded parameters

* update CHANGELOG.md
2021-10-27 12:49:56 -07:00
Quinlan Jung d6cfc887c3 [eas-cli] enforce additionalTypenames on graphql queries (#709)
* [eas-cli] enforce additional typenames

* fix stuff

* update CHANGELOG.md
2021-10-26 11:07:26 -07:00
Quinlan Jung ce41c436aa [eas-cli] add additionalTypenames for other queries (#704)
* [eas-cli] add additionalTypenames for other queries

* update CHANGELOG.md
2021-10-25 16:50:32 -07:00
Quinlan Jung ec096eaaa2 [ENG-2143][eas-cli] add additionalTypenames for credentials (#703)
* [eas-cli] add additionalTypenames for credentials

* update CHANGELOG.md
2021-10-25 16:50:10 -07:00
Dominik Sokal 9c32dd5308 don't print logs to stderr (#708)
* don't print logs to stderr

* update CHANGELOG.md
2021-10-25 14:15:54 +02:00
Dominik Sokal 69d7ee9547 fix changelog workflow - use sed for trimming string 2021-10-25 14:03:04 +02:00
Dominik Sokal e2e4cb67ca better error message when eas.json is invalid (#707)
* better error message when eas.json is invalid

* add unit test

* update CHANGELOG.md
2021-10-25 13:19:27 +02:00
Dominik Sokal 4a08ad4997 unify eas.json and build job format (#701)
* unify eas.json and build job format

* update CHANGELOG.md
2021-10-25 09:56:30 +02:00
Expo CI 107a1c12bd update CHANGELOG.md 2021-10-22 18:16:12 +00:00
Brent Vatne 8fe5268a73 v0.33.1 2021-10-22 11:10:45 -07:00
Brent Vatne faba1ad8e3 [eas-cli] Skip validating updates scripts integration, which no longer exist in SDK 43+ (#706) 2021-10-22 11:09:50 -07:00
Dominik Sokal ea4885c710 fix typo 2021-10-21 10:10:47 +02:00
Brent Vatne 4d85002d2b [eas-cli] Update the submission prompt options 2021-10-20 11:43:55 -07:00
jkh 535b17f592 [eas-cli] use config-plugin to compute runtime version (#697) 2021-10-20 10:58:59 -07:00
Expo CI e556b45d4d update CHANGELOG.md 2021-10-20 15:49:25 +00:00
jonsamp 48130dcbdc v0.33.0 2021-10-20 11:43:29 -04:00
1419ca6325 [eas-cli] Make "production" the default profile name (#677)
* build defaults to production, falls back to release

* allow submit to use production profile

* fix typo

* lint

* use a common type

* lint

* adds changelog

* adds test for getDefaultProfilesAsync

* updates default config

* self feedback

* adds channel names

* removes channel from default eas json config

* de-nest getDefaultProfilesAsync

* remove "_" variable name

* fix changelog

* incorporate feedback

* restore readme to main

* lint

* Update CHANGELOG.md

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

* Update CHANGELOG.md

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

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

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

Co-authored-by: Wojciech Kozyra <[email protected]>
Co-authored-by: Dominik Sokal <[email protected]>
2021-10-18 12:26:08 -04:00
Dominik Sokal 8513dc0645 update CHANGELOG.md 2021-10-18 14:59:13 +02:00
Dominik Sokal 70e8f8370b unify reading app.json (#692) 2021-10-18 14:58:08 +02:00
Dominik Sokal 135c90a8b8 eas submit - skip second prompt for apple id when already signed in with apple (#691)
* eas submit - skip second prompt for apple id when already signed in with apple

* update CHANGELOG.md
2021-10-18 14:37:47 +02:00
Dominik Sokal fa588c2d24 don't capitalize credentials.json 2021-10-18 12:33:35 +02:00
Dominik Sokal b4ce7a91c2 rename generic workflow -> bare workflow (#690) 2021-10-18 10:24:24 +02:00
Dominik Sokal 8ca9d66c32 fix typos 2021-10-15 16:04:54 +02:00
Dominik Sokal a86fabde59 re-generate graphql code 2021-10-15 11:44:44 +02:00
Expo CI 40a896675f update CHANGELOG.md 2021-10-15 09:05:59 +00:00
Dominik Sokal cf7848e3c5 v0.32.0 2021-10-15 11:00:07 +02:00
Bartłomiej Klocek 3d4394a9bc [eas-cli][submit] Ability to select a build from the list (#688)
* [eas-cli][submit] Ability to select a build from the list

* Add tests

* update CHANGELOG.md
2021-10-15 10:42:42 +02:00
Quinlan Jung 05f4c08d09 [eas-cli] create/list/revoke asc api keys via baconlane (#687)
* [eas-cli] manage ascApiKeys via baconlane

* expose methods

* update CHANGELOG.md

* pr feedback

* fix tests

* grammar
2021-10-14 13:22:19 -07:00
Quinlan JungandDominik Sokal a4107a86f9 [eas-cli] support ascApiKeys in ios submission (#678)
* [eas-cli] support ascApiKeys in ios submission

* format feedback

* get values from eas.json

* more pr feedback

* changelog stuff

* fyi

* tests

* fix lint

* Update CHANGELOG.md

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

* Update packages/eas-cli/src/submit/ios/IosSubmitCommand.ts

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-10-11 14:49:26 -07:00
Quinlan JungandDominik Sokal a5c695dd88 [eas-json] add asc api key parameters to eas json (#684)
* [eas-json] add asc api key parameters to eas json

* changelog stuff

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-10-11 14:41:13 -07:00
Dominik Sokal 7576d9195d eslint - enable no-underscore-dangle (#686)
* eslint - enable no-underscore-dangle

* update CHANGELOG.md
2021-10-11 11:10:03 +02:00
Brent VatneandDominik Sokal 838c1508be [eas-cli] Fix printing App Store Connect URL after submission (#683)
* [eas-cli] Fix printing App Store Connect URL after submission

* Update CHANGELOG

* Fix comment

* Remove unnecessary import

* Update other place where we pass in empty string to learnMore

* Apply suggestions from code review

Co-authored-by: Dominik Sokal <[email protected]>
2021-10-11 10:12:03 +02:00
Expo CI 7941683435 update CHANGELOG.md 2021-10-08 23:07:10 +00:00
Quinlan Jung 91f2311a50 v0.31.1 2021-10-08 16:01:17 -07:00
Quinlan Jung 45d10d9b83 [eas-cli] fix non interactive bug for service accounts (#682)
* [eas-cli] fix non interactive bug

* changelog stuff
2021-10-08 15:49:52 -07:00
Dominik Sokal f5d5093922 changelog bot: commit as last commit author (#680) 2021-10-08 16:39:47 +02:00
Dominik Sokal fa4687b0f8 implement simple changelog bot (#676)
* implement simple changelog bot

* update PR template
2021-10-08 15:08:02 +02:00
Expo CI e78c475118 update CHANGELOG.md 2021-10-08 12:37:00 +00:00
Dominik Sokal def91af01f v0.31.0 2021-10-08 14:26:59 +02:00
Dominik Sokal 5eb0fb75ac print platform emoji next to build details url when building for multiple platforms 2021-10-08 14:23:11 +02:00
Dominik Sokal de8c69f766 update CHANGELOG.md 2021-10-08 13:58:55 +02:00
Wojciech KozyraandDominik Sokal 5879ac424d [eas-cli] do not set default image (#675)
* [eas-cli] do not set default image

* upgrade @expo/eas-build-job

* remove redundant import

* update CHANGELOG

Co-authored-by: Dominik Sokal <[email protected]>
2021-10-08 13:56:57 +02:00
Dominik Sokal e30f62ec12 add option to review adhoc devices when reusing profile (#673)
* add option to review adhoc devices when reusing profile

* update CHANGELOG

* update snapshot

* address PR feedback
2021-10-08 10:46:37 +02:00
Bartłomiej Klocek dc4e36e8a8 [eas-cli] Improved service account display in submission summary (#674) 2021-10-08 09:17:46 +02:00
Quinlan Jung 12ded8c9a9 [eas-cli] integrated credentials service with android submissions (#664)
* [eas-cli] integrated credentials service with android submissions

* pr feedback

* tests

* changelog

* pr feedback

* dont print path in summary if it doesnt exist

* fix tests
2021-10-06 13:16:02 -07:00
Dominik Sokal 7378c6fd0e change CHANGELOG commit message 2021-10-06 16:23:02 +02:00
Expo CI 77a8b08834 [skip ci] update CHANGELOG.md 2021-10-06 14:15:53 +00:00
Dominik Sokal 79db9eb482 v0.30.1 2021-10-06 16:09:23 +02:00
Dominik Sokal 59f6d36114 don't use PAT for release 2021-10-06 16:07:24 +02:00
Dominik Sokal c36c26f5a9 fix building monorepo projects on Windows (#671)
* fix building monorepo projects on Windows

* update CHANGELOG

* address PR feedback
2021-10-06 15:34:42 +02:00
Dominik Sokal d6692f20b5 volta pin [email protected] and [email protected] 2021-10-06 09:50:33 +02:00
Brent Vatne 6e37f596df [eas-cli] Bump @expo/config @expo/prebuild-config @expo/config-plugins to fix iOS Info.plist warning (#670)
* [eas-cli] Bump @expo/config @expo/prebuild-config @expo/config-plugins

* Update CHANGELOG
2021-10-06 09:39:57 +02:00
Dominik Sokal d326891ef8 fix committing changelog changes from CI 2021-10-05 18:36:52 +02:00
Dominik Sokal 46a34327d9 fix --json flag when running on GitHub Actions (#669)
* fix --json flag when running on GitHub Actions

* update CHANGELOG
2021-10-05 15:38:34 +02:00
Dominik Sokal 97242af30f update CHANGELOG 2021-10-05 13:23:34 +02:00
Dominik Sokal ca0a8f79cd v0.30.0 2021-10-05 12:32:40 +02:00
Dominik Sokal 67eac59585 automate releases (#668)
* automate releases

* update CONTRIBUTING.md

* add copy-pasta alert

* update CHANGELOG

* send slack notif on successful release
2021-10-05 12:28:34 +02:00
Quinlan Jung 8cf1f0dc22 [eas-cli] fix test snapshots 2021-10-04 13:21:29 -07:00
Quinlan JungandDominik Sokal 51702e4bda [eas-cli] fix ios print formats (#657)
* [eas-cli] fix ios print formats

* fix lint

* changelog

* Update CHANGELOG.md

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

* pr feedback

Co-authored-by: Dominik Sokal <[email protected]>
2021-10-01 13:42:44 -07:00
Quinlan Jung 426a3d1ef3 [eas-cli] use google service key detection in credentials service (#660)
* [eas-cli] use google service key detection in credentials service

* pr feedback

* changelog
2021-09-30 12:59:10 -07:00
Quinlan Jung e8d71a52ec [eas-cli] setup google service account key (#659)
* [eas-cli] setup gsa key

* add tests

* lint

* changelog

* pr feedback
2021-09-29 15:25:41 -07:00
Quinlan JungandDominik Sokal ccd0cb19a4 [eas-cli] remove google service account key (#658)
* [eas-cli] remove google service account key

* chnagelog

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

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

* Update packages/eas-cli/src/credentials/android/utils/googleServiceAccountKey.ts

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

* pr feedback

* rebase

* rebase

Co-authored-by: Dominik Sokal <[email protected]>
2021-09-29 15:23:15 -07:00
Quinlan JungandDominik Sokal 9f85178d71 [eas-cli] fix android print formats (#656)
* [eas-cli] fix android print formats

* fix lint:

* changelog

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-09-29 10:27:17 -07:00
jkh 085ee52305 [eas-cli] Enable autoIncrementing on Android (#645) 2021-09-29 05:49:42 -07:00
Dominik Sokal 22a71275b2 update CHANGELOG 2021-09-29 13:35:35 +02:00
Dominik Sokal 5a730d70d6 v0.29.1 2021-09-29 13:21:17 +02:00
Dominik Sokal 41f58e5e52 fix double prompt for apple credentials (#654)
* add credentials context to build context

* add credentials context to submission context

* use credentials context when authenticating with apple

* don't use chalk.italic

* update CHANGELOG
2021-09-29 11:50:07 +02:00
kgc00 34af57f585 upgraded analytics client (#655) 2021-09-29 09:15:17 +02:00
Quinlan Jung 4674cbebc5 [eas-cli] use existing google service account key (#650)
* [eas-cli] use existing google service account key

* changelog

* pr feedback

* update snap
2021-09-28 15:07:10 -07:00
Quinlan Jung 69add6f523 fix changelog 2021-09-28 14:55:45 -07:00
Quinlan Jung b70e874135 [eas-cli] assign google service account key (#649)
* [eas-cli] assign google service account key

* changelog

* pr feedback
2021-09-28 14:54:41 -07:00
Dominik Sokal 03fa29dd5a make eslint disallow use of sync fs functions (#652)
* make eslint disallow use of sync fs functions

* update CHANGELOG
2021-09-28 13:41:38 +02:00
Wojciech Kozyra 9e467b900e Update CHANGELOG 2021-09-28 12:30:37 +02:00
Wojciech Kozyra 6e1c7dd91e v0.29.0 2021-09-28 12:12:28 +02:00
Wojciech KozyraandDominik Sokal 9812406c18 [eas-cli] update eas-build-job + add xcode 13 support (#651)
* update eas-build-job adds xcode 13 support

* update CHANGELOG

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-09-28 12:04:44 +02:00
Quinlan Jung cf11ae93af [eas-cli] create GoogleServiceAccountKey (#642)
* [eas-cli] create GoogleServiceAccountKey

* pr feedback

* changelog stuff

* pr feedback
2021-09-27 17:02:02 -07:00
Wojciech Kozyra 14756ad39b support target specific Info.plist (#635) 2021-09-27 14:08:10 +02:00
Dominik Sokal 1486b481f6 use asMock instead of 'as jest.Mock' 2021-09-27 10:23:51 +02:00
Quinlan Jung 84d82e9a59 [eas-cli] change export style (#647)
* [eas-cli] change export style

* changelog stuff
2021-09-27 09:37:41 +02:00
Quinlan Jung c7ce4a8670 [eas-cli] creds manager: stop prompting every iteration (#641)
* [eas-cli] creds manager: stop prompting every iteration

* changelog stuff
2021-09-24 12:49:40 -07:00
Dominik Sokal fe0a3b4c41 fix experience with validating metro config in non-interactive mode (#644)
* fix experience with validating metro config in non-interactive mode

* update CHANGELOG
2021-09-24 11:35:28 +02:00
Dominik Sokal 0bce278bd4 update CHANGELOG 2021-09-23 11:42:56 +02:00
Dominik Sokal fedfdb3364 v0.28.2 2021-09-23 11:36:29 +02:00
Dominik Sokal 028f0ab74b fix auto-submitting in fresh project (#640)
* fix auto-submitting in fresh project

* update CHANGELOG
2021-09-23 11:34:22 +02:00
Dominik Sokal cd278155e2 clean up tests output - mock console.log 2021-09-23 11:07:54 +02:00
Dominik Sokal 1e419ab49d use log-symbols to unify green ticks style (#639)
* use log-symbols to unify green ticks style

* update CHANGELOG
2021-09-23 10:14:29 +02:00
Dominik Sokal 2f57fd199d update CHANGELOG 2021-09-22 15:40:42 +02:00
Dominik Sokal 5932775cd4 v0.28.1 2021-09-22 15:33:39 +02:00
Dominik Sokal d68d6c38d6 do not fail in case of transient network errors (#638)
* do not fail in case of transient network errors

* update CHANGELOG
2021-09-22 14:29:52 +02:00
Evan BaconandEvan Bacon d690e10caf Forward props (#636)
Co-authored-by: Evan Bacon <[email protected]>
2021-09-22 13:16:33 +02:00
Quinlan JungandDominik Sokal 99ea613b39 [eas-cli] add google service account key fragment (#632)
* [eas-cli] add google service account key fragment

* fix lint

* update changelog

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-09-21 17:02:16 -07:00
Quinlan JungandDominik Sokal 13715317bd [eas-cli] remove graphql support for app specific password (#633)
* [eas-cli] remove graphql support for app specific password

* fix tests

* changelog stuff

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-09-21 17:01:08 -07:00
Dominik Sokal 18d1ed441b add CODEOWNERS 2021-09-21 11:39:54 +02:00
Dominik Sokal db067c4757 rename directory submissions -> submit (#631) 2021-09-20 16:55:46 +02:00
Dominik Sokal 51388f6adb add PR comment with code coverage (#629) 2021-09-20 16:36:42 +02:00
fcbf363538 use custom ora instance (#575)
* use custom ora instance

* Update CHANGELOG.md

* Update ora.ts

* pr feedback

* address PR feedback

* update CHANGELOG

* add eslint rule to prevent using ora directly

Co-authored-by: Evan Bacon <[email protected]>
Co-authored-by: Dominik Sokal <[email protected]>
2021-09-20 16:36:28 +02:00
Dominik Sokal b4d7795006 update CHANGELOG 2021-09-20 11:15:30 +02:00
Dominik Sokal 2364ed74d7 v0.28.0 2021-09-20 10:56:59 +02:00
Dominik Sokal 87fb74dd5a print meaningful error message when not logged in in non-interactive mode (#628)
* print meaningful error message when not logged in in non-interactive mode

* update CHANGELOG

* maybe fix tests
2021-09-20 10:53:55 +02:00
Dominik Sokal eef0c6ae6e [ENG-1792] Validate that project includes expo-dev-client when building with developmentClient flag (#627)
* move reviewAndCommitChangesAsync to utils

* move expo-cli runner to utils

* add expo-cli and expo-dev-client to diagnostics

* check for expo-dev-client when building with developmentClient = true

* update CHANGELOG

* add installation instructions
2021-09-20 10:36:55 +02:00
kgc00 5ee905a977 [eas-cli] Removes Amplitude (#621)
- Deleted references to Amplitude, uninstalled packages.
- Renamed `UserSettings` involving `amplitude...` => `analytics...`
2021-09-17 15:23:25 -07:00
Bartłomiej Klocek ca3e083995 [eas-cli] eas submit: Fix error message not displayed (#626)
* [eas-cli] eas submit: Fix error message not displayed

* Fix error display

* Use `platforDisplayNames`
2021-09-17 13:33:56 +02:00
Dominik Sokal b7ae91892c eas submit: warn about turtle v1 build ids (#624)
* eas submit: warn about turtle v1 build ids

* update CHANGELOG

* id -> ID

* address PR feedback
2021-09-17 10:45:33 +02:00
Dominik Sokal 508ca745c1 [ENG-1915] show syntax errors in eas.json (#618)
* show syntax errors in eas.json

* update CHANGELOG
2021-09-16 12:13:10 +02:00
Dominik Sokal 95e8a38c85 [ENG-1347] eas submit: suggest using build id instead of build details page (#620)
* eas submit: suggest using build id instead of build details page

* update CHANGELOG

* fix lint
2021-09-16 12:05:39 +02:00
Dominik Sokal 441f3eaf4c enforce Async suffix for async functions (#623)
* enforce Async suffix for async functions

* update CHANGELOG

* fix tests
2021-09-16 11:46:15 +02:00
Wojciech Kozyra 9edb10ca55 enforce return type in all functions (#619)
* enforce return type in all functions

* update CHANGLOG
2021-09-15 16:28:09 +02:00
Wojciech Kozyra 7f094bc605 [eas-cli] support multiflavor android projects (#595)
* [eas-cli] support multiflavor android projects

* move utils from expo-cli

* update CHANGELOG

* review feedback
2021-09-15 13:51:28 +02:00
Dominik Sokal 2463e958aa add eslint rule to reduce node-fetch usage 2021-09-14 10:44:31 +02:00
Dominik Sokal 390e6fef9e upgrade eslint-config-universe to 8.0.0 2021-09-13 17:44:24 +02:00
Dominik Sokal 6b416c3dfb use envs from build profile for resolving app config when auto-submiting (#614)
* use envs from build profile for resolving app config when auto-submitting

* update CHANGELOG
2021-09-13 17:34:25 +02:00
dependabot[bot] 60d2daf4c8 Bump handlebars from 4.7.6 to 4.7.7 (#613)
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.7.6 to 4.7.7.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.7.6...v4.7.7)

---
updated-dependencies:
- dependency-name: handlebars
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-13 17:03:33 +02:00
dependabot[bot] 9528f25ff2 Bump hosted-git-info from 2.8.8 to 2.8.9 (#612)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

---
updated-dependencies:
- dependency-name: hosted-git-info
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-13 17:03:17 +02:00
dependabot[bot] 5da03dfca7 Bump glob-parent from 5.1.1 to 5.1.2 (#611)
Bumps [glob-parent](https://github.com/gulpjs/glob-parent) from 5.1.1 to 5.1.2.
- [Release notes](https://github.com/gulpjs/glob-parent/releases)
- [Changelog](https://github.com/gulpjs/glob-parent/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gulpjs/glob-parent/compare/v5.1.1...v5.1.2)

---
updated-dependencies:
- dependency-name: glob-parent
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-13 17:02:34 +02:00
dependabot[bot] 8b3b0376c3 Bump trim-newlines from 3.0.0 to 3.0.1 (#608)
Bumps [trim-newlines](https://github.com/sindresorhus/trim-newlines) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/sindresorhus/trim-newlines/releases)
- [Commits](https://github.com/sindresorhus/trim-newlines/commits)

---
updated-dependencies:
- dependency-name: trim-newlines
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-13 16:53:26 +02:00
dependabot[bot] 5f3c4756d2 Bump path-parse from 1.0.6 to 1.0.7 (#610)
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-13 16:53:05 +02:00
dependabot[bot] 3b34a64893 Bump ws from 5.2.2 to 5.2.3 (#609)
Bumps [ws](https://github.com/websockets/ws) from 5.2.2 to 5.2.3.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/5.2.2...5.2.3)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-13 16:52:36 +02:00
Dominik Sokal 243cf8d630 reduce eas-cli size (#605)
* don't use lodash

* upgrade @expo/json-file

* upgrade @expo/config to 5.0.9

* add tests for utils

* address PR feedback

* more tests

* move tempy to dev dependencies

* replace timeago.js with @expo/timeago.js

* add eslint rule to forbid using lodash

* update CHANGELOG
2021-09-13 16:47:49 +02:00
Juwan Wheatley 671ce30ed4 update CHANGELOG 2021-09-10 19:14:14 -04:00
Juwan Wheatley 9021887072 v0.27.1 2021-09-10 18:59:11 -04:00
Juwan Wheatley 6a80d4c8e9 [eas-cli] fix secrets mutations (#606)
* [eas-cli] fix secrets mutations

* add changelog entry

* fix typo
2021-09-10 18:42:10 -04:00
Dominik Sokal 471e26817a update CHANGELOG 2021-09-10 14:42:07 +02:00
Dominik Sokal 14bbd1ee0f v0.27.0 2021-09-10 14:30:41 +02:00
Dominik Sokal 22b06b444d [eas-json] allow env vars in android.serviceAccountKeyPath (#604)
* [eas-json] allow env vars in android.serviceAccountKeyPath

* update CHANGELOG
2021-09-10 14:28:21 +02:00
Dominik Sokal cebd29e40d [ENG-755] implement auto-submits (#603)
* better experience when building for all platforms

* implement auto-submits

* address PR feedback

* update CHANGELOG

* refactor build command class

* better UX when Android submission completes without error

* address PR feedback

* small ux improvement
2021-09-10 12:32:00 +02:00
Dominik Sokal acf7e55b5b [ENG-755] add --non-interactive flag for eas submit (#600)
* add --non-interactive flag for eas submit

* update CHANGELOG

* add basic tests for --non-interactive mode

* fix long-running tests
2021-09-09 17:12:40 +02:00
Wojciech KozyraandDominik Sokal 7c29aa3b38 [eas-cli] fix google service detection on windows (#602)
* normalize path (C:/path/to/repo -> C:\path\to\repo)

* update CHANGELOG

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-09-09 15:03:00 +02:00
Dominik Sokal dee5b4cf60 mock generateRandomKeystoreAsync to make test execution faster 2021-09-09 11:19:39 +02:00
Dominik Sokal 896e14b1e4 [ENG-755] eas submit - implement --platform all (#598)
* reorder platform prompt options

* move RequestedPlatform enum

* unify selecting platforms in eas build/submit

* implement --platform all

* update CHANGELOG
2021-09-09 09:50:43 +02:00
Dominik Sokal 42c20d6ef5 update CHANGELOG 2021-09-08 10:12:38 +02:00
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
Brent Vatne 30552f134c v0.16.0 2021-05-26 14:49:42 -07:00
Brent Vatne e4beba9a21 Add env var to disable capability sync and extra logs when EXPO_DEBUG is set (#426) 2021-05-26 13:11:42 -07:00
Dominik Sokal b71ab7ec84 implement managed credentials support for multitarget iOS projects (#414)
* ignore --skip-credentials-check flag for ios builds

* account for parent bundle identifier when creating apple app identifier

* implement managed credentials support for multitarget ios projects

* update CHANGELOG.md

* address PR feedback

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

* fix lint warning

* address more PR feedback

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

* fix sending appBuildVersion to api

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

* Update CHANGELOG.md

* Updated getAppBuildGradle -> getAppBuildGradleFilePath

* Update RemoveProvisioningProfile.ts

* Update view.ts

* Update applicationId.ts

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

* detect unencrypted pem

* much sad

* much sad 2

* much sad

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

* add buildNumber and versionCode to metadata

* read proper app version for generic projects

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

* address PR feedback

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

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

* remove disableIosBundleIdentifierValidation from eas.json

* update CHANGELOG.md

* add unit tests

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

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

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

* add tests

* change error message

* fix test description

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

* Update CHANGELOG.md

* Added support for native projects

* Update ensureAppExists-test.ts

* Validate entitlements from JSON

* removed unneeded tests

* Update bundleIdCapabilities.ts

* use Workflow

* Forward props everywhere

* refactor

* Added return types

* Update entitlements.ts

* Update bundleIdCapabilities.ts

* Update bundleIdCapabilities.ts

* Update bundleIdCapabilities.ts

* cleanup

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

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

* Update CHANGELOG.md

* Address PR comments.

* Update CHANGELOG.md

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

* Update new features in changelog.

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

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

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

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

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

validate using pem decryption instead

* 0: some pr feedback

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

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

* [eas-cli] update changelog

* [eas-cli] sort imports

* [eas-cli] sort imports

* [eas-cli] improve getInstallUrl typing

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

* [eas-cli] fix typing

* [eas-cli] clean code

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

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

ctx.newIos -> ctx.ios

fix lint

unused fixtures

remove unused fixtures

merge fixtures

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

delete old inner class

remove uneeded dist cert utils

merge dist cert utils

remove unused functions of pprofile utils

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

* nits:

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

* tweak name

* pr feedback

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

* fix broken tests from switching b64 pprofile test fixture

* fix lint

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

* update CHANGELOG.md

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

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

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

* update CHANGELOG.md

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

* update changelog

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

* pr feedback

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

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

* update changelog

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

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

* refactor SetupAdhocProvisioningProfile

* update CHANGELOG

* address PR feedback
2021-04-19 12:29:37 +02:00
Dominik Sokal db0b6bfcf5 remove redundant finally block 2021-04-16 15:54:44 +02:00
Dominik Sokal da81c1c6df remove incorrect usage example from secret:list command 2021-04-16 14:16:41 +02:00
Dominik Sokal 42d3296f07 update CHANGELOG 2021-04-16 14:13:09 +02:00
Dominik Sokal f9b4b1a6e8 v0.10.0 2021-04-16 13:56:25 +02:00
Will Schurman d03603a75f Use special Expo SDK runtime version for managed projects (#336) 2021-04-15 11:15:02 -07:00
Juwan Wheatley ffa9e5ef66 [eas-cli] make secrets commands follow styleguide (#315)
* [eas-cli] make CLI more styleguide-compliant

* add --json flag

* update changelog

* json flag is a new feature, not a breaking change

* remove secrets JSON flag since we still show non-result output (and same for other commands)

* remove extra import
2021-04-15 13:05:28 -04:00
Dominik Sokal e7f3cf9c74 Pass --preserveWatchOutput to tsc --watch
This makes the 'yarn start' output preserve the output for previous tsc runs.
2021-04-15 16:42:04 +02:00
Dominik Sokal e82a4a0a57 replace all iOS -> Ios (#337) 2021-04-15 12:23:14 +02:00
Bartłomiej Klocek 175f07fac9 [eas cli][submit] Use GraphQL instead of REST endpoints (#333)
* Autogenerated gql code

* Add Submission Query

* Add Submission Mutation

* Use GQL generated types

* Remove Submission types

* Replace SubmissionPlatform with AppPlatform

* Update Fix tests
2021-04-15 11:43:02 +02:00
Dominik Sokal 2869744a5d add support for adhoc and universal distribution within enterprise teams (#335)
* [eas-json] add enterpriseProvisioning prop

* implement generating enterprise universal credentials

* fix tests
2021-04-15 10:42:59 +02:00
Dominik Sokal 99f898a483 refactor IosCredentialsProvider to use graphql (#329)
* refactor IosCredentialsProvider to use graphql

* add tests for IosCredentialsProvider

* address PR feedback

* fix tests
2021-04-15 10:23:42 +02:00
Dominik Sokal 1448db96e6 don't update capabilities if there's nothing to change (#334)
* don't update capabilities if there's nothing to change

* add tests

* update CHANGELOG
2021-04-15 10:19:30 +02:00
Quinlan Jung e02036a46a [eas-cli] ilovecats deletion 2021-04-14 14:31:04 -07:00
Quinlan JungandDominik Sokal 09f33263cb [eas-cli] use new graphql api in SetupBuildCredentialsFromCredentials… (#328)
* [eas-cli] use new graphql api in SetupBuildCredentialsFromCredentialsJson

* pr feedback

* tests

* Update packages/eas-cli/src/credentials/ios/actions/new/BuildCredentialsUtils.ts

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

* pr feedback

Co-authored-by: Dominik Sokal <[email protected]>
2021-04-14 14:28:06 -07:00
jkh 412b1ec74c [eas-cli] add update:delete command (#332) 2021-04-14 12:04:56 -07:00
jkh 9a1538fb4d add eas update:view [ID] (#327) 2021-04-14 11:04:58 -07:00
jkh 0ad9b31ee3 [eas-cli] update channel:list to handle rollouts (#331) 2021-04-14 11:04:49 -07:00
Wojciech Kozyra 4cbb7bb2f2 [eas-cli] Support for local builds (#305)
* [eas-cli] Support for local builds

* add CHANGELOG + mark as experimental

* review feedback
2021-04-14 09:08:06 +02:00
jkh ae109cd8c6 [eas-cli] handle branch with no updates gracefully (#330) 2021-04-13 07:11:54 -07:00
Dominik Sokal fbaba774a6 enable type checking for tests and mocks (#325)
* use separate tsconfig files for development and release

* fix type errors in tests

* add typecheck script

* address PR feedback
2021-04-13 11:49:07 +02:00
jkh 38bc8062a4 [eas-cli] make branch name a positional instead of a named arg (#326) 2021-04-12 10:50:41 -07:00
Dominik Sokal d41d184b55 add start script (#323) 2021-04-12 16:36:56 +02:00
jkh d6d32a7a7d [eas-cli] clean up formatting of list/view commands (#322) 2021-04-12 07:19:37 -07:00
Dominik Sokal 15a9532fa7 refactor credentials actions (#321)
* make SetupAdhocProvisioningProfile.runAsync return result

* make SetupDistributionCertificate.runAsync return result

* print ios credentials using new display function

* generate graphql code

* update CHANGELOG

* address PR feedback

* updates changes after running generate-graphql-code

* address more PR feedback

* fix lint warning
2021-04-12 11:38:06 +02:00
Dominik Sokal 3b9fb724fe update CONTRIBUTING.md 2021-04-12 10:41:36 +02:00
Dominik Sokal 33767b5635 update CHANGELOG 2021-04-12 10:18:27 +02:00
Test User 175fc2b30e v0.9.1 2021-04-09 14:43:36 -07:00
Quinlan Jung aec9347d69 [eas-cli] fix validation check of provisioning profile (#318)
* [eas-cli] fix validation check of provisioning profile

* changelog and stuff

* quin not dominik

* move the changelog thing
2021-04-09 13:05:25 -07:00
Quinlan Jung 91500d07c0 [eas-cli] replace SetupProvisioningProfile (#317)
* [eas-cli] replace SetupProvisioningProfile

* changelog

* more changelog
2021-04-09 13:02:12 -07:00
Test User 62ca14b504 Commit README updates 2021-04-09 10:13:28 -07:00
Test User 9158a95d57 v0.9.0 2021-04-09 09:59:29 -07:00
Test User d0f3e387e7 Bump @expo/config-plugins to fix eas build:configure on Windows 2021-04-09 09:58:43 -07:00
jkh 158856f8ec [eas-cli] upgrade @urql/core and add retry (#320) 2021-04-09 07:19:04 -07:00
Dominik Sokal 1e59957753 Update CHANGELOG.md 2021-04-09 11:50:36 +02:00
Dominik Sokal f97d46040c add commands for managing webhooks (#314)
* add webhook:create command

* add webhook:list command

* add webhook:view command

* add webhook:update command

* add webhook:delete command

* generate graphql code

* update CHANGELOG

* address PR feedback
2021-04-09 11:50:07 +02:00
Dominik Sokal d7485cb5d2 fix first ios build for a project without ios.bundleIdentifier in app.json (#319)
* fix first ios build for a project without ios.bundleIdentifier in app.json

* update CHANGELOG

* address PR feedback

* address more PR feedback
2021-04-09 11:31:17 +02:00
071fdb7f4a [eas submit] Added new common error messages (#311)
* [eas submit] Added new common error messages

* Update CHANGELOG

* Apply suggestions from code review

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

Co-authored-by: James Ide <[email protected]>
Co-authored-by: Brent Vatne <[email protected]>
2021-04-09 08:17:33 +02:00
jkh 6dd96ffb20 [eas-cli] add more info to transient errors (#316) 2021-04-08 16:26:34 -04:00
Quinlan Jung f56fb0f7de [eas-cli] setupProvisioningProfile using graphql (#301)
* [eas-cli] setupProvisioningProfile using graphql

refactor

fix lint

setup provisioning profile

* refactor stuff

* add comments for ppl

* simplify logic

* tests

* pr feedback
2021-04-08 11:19:14 -07:00
Wojciech Kozyra 7d28f6add9 add LICENSE (#313) 2021-04-08 12:35:50 +02:00
Juwan Wheatley eebef3e461 [eas-cli] use autocomplete list for secrets:delete (#309)
* [eas-cli] use autocomplete list for secrets:delete

* update changelog

* check if secret is account-wide and tell user that it could affect multiple apps
2021-04-07 13:08:00 -04:00
Dominik Sokal 8336f630ea update CHANGELOG 2021-04-06 12:55:17 +02:00
Dominik Sokal 7d6bdbb6ed v0.8.1 2021-04-06 12:30:31 +02:00
Dominik Sokal 403b5c237f don't use graphql cache for polling builds (#310)
* don't use graphql cache for polling builds

* update CHANGELOG
2021-04-06 12:29:41 +02:00
Dominik Sokal 2367ed7911 update CHANGELOG.md 2021-04-06 11:21:13 +02:00
Dominik Sokal b650f76979 v0.8.0 2021-04-06 11:12:43 +02:00
Dominik Sokal 96e59c198d update CHANGELOG.md 2021-04-06 11:11:47 +02:00
Dominik Sokal bf1704d268 use graphql for generating upload sessions (#307)
* use graphql for generating upload sessions

* address PR feedback

* fix tests
2021-04-06 11:06:47 +02:00
Dominik Sokal 279dfd00c8 eas build - use GraphQL instead of REST endpoints for build queries (#306)
* use graphql for printing pending builds

* use graphql for polling build status

* remove some unused code
2021-04-02 12:10:59 +02:00
Dominik Sokal 4d2577865a eas build - use GraphQL instead of REST endpoints (#303)
* regenerate graphql schema

* use graphql mutations for creating builds
2021-04-02 12:06:10 +02:00
Quinlan Jung 9d88fba40e [eas-cli] configure provisioning profile using new api (#299)
* [eas-cli] configure provisioning profile using new api

change positional args

* configure prov prof: pr feedback

* fix lint
2021-04-01 23:16:34 -07:00
Juwan Wheatley 9d2353b4f7 [eas-cli] use flags for secrets:create and rename target to scope (#300)
* use flags instead of args for secrets:create

* rename target to scope

* show secret ID before timestamp

* add changelog entries and note about name validation source of truth
2021-04-01 16:13:44 -04:00
Dominik Sokal 44b338d50a unhide secrets:create and secrets:delete (#302) 2021-04-01 19:06:23 +02:00
Bartłomiej Klocek 62114cab3b [Submit] Skip tar extraction logic in CLI (#297)
* [submit] Skip tar extraction logic for URL

* Remove tar extraction utils

* Update CHANGELOG

* Simplified path handling code

* New unit tests

* Add URL validation
2021-04-01 18:11:23 +02:00
Dominik Sokal d426fb0355 fix string interpolation 2021-04-01 11:34:32 +02:00
Quinlan Jung 2a8d6683ff [eas-cli] fix typo in tests 2021-03-31 17:57:46 -07:00
Juwan WheatleyandDominik Sokal 7b18165390 [eas-cli] fix secret name validation when prompted (#298)
* [eas-cli] fix secret name validation when prompted

* add changelog entry

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-03-31 19:43:46 -04:00
Dominik Sokal 814a44ac27 change cache object schema + rename secretEnvs -> environmentSecrets (#295)
* change cache object schema

* update CHANGELOG.md

* rename secretEnvs -> environmentSecrets

* update @expo/eas-build-job to 0.2.22
2021-03-31 10:20:04 +02:00
jkh f5483c8996 [eas-cli] add command eas channel:rollout (#290) 2021-03-30 13:43:37 -07:00
Juwan Wheatley acdbed4762 [eas-cli] fix "irreversible" typo (#282) 2021-03-30 16:24:19 +02:00
Dominik Sokal f09e4b54cf add distribution/release channel to graphql formatter 2021-03-30 16:23:41 +02:00
Vincent Thibault 859fdf5e46 Add distribution/channel informations in build:view/list (#284)
* Add new informations in build:view/list

* Update CHANGELOG.md

* Add nullish coalescing operator
2021-03-30 16:17:16 +02:00
Wojciech Kozyra 6aa72d5b7d [eas-cli] add analytics support (#162) 2021-03-29 11:58:05 +02:00
Quinlan Jung 886900df29 [eas-cli] make dist cert setup generic with graphql api (#294)
* [eas-cli] make dist cert setup generic

* fix tsc
2021-03-26 13:17:07 -07:00
Quinlan Jung a450fed1f8 [eas-cli] create provisioning profile using graphql api (#293) 2021-03-26 13:16:37 -07:00
Dominik Sokal 4c78100adb use graphql to create unpublished app (#288) 2021-03-26 09:01:42 +01:00
Dominik Sokal 0317e6fe71 refactor build:list and build:view to use graphql (#286) 2021-03-26 08:56:04 +01:00
jkh 6825877344 [eas-cli] add support for rollouts to channel:view command (#289) 2021-03-24 07:04:07 -07:00
Dominik Sokal a47ea88d28 Update CHANGELOG.md 2021-03-23 10:37:57 +01:00
Wojciech Kozyra a9e7f304ec Update CHANGELOG.md 2021-03-22 13:36:10 +01:00
Wojciech Kozyra d92a2ae3f4 v0.7.0 2021-03-22 13:33:34 +01:00
Dominik Sokal b7dea35da4 better message when build was canceled 2021-03-22 12:02:08 +01:00
Wojciech Kozyra 5a5f11752d Update @expo/eas-build-job 2021-03-22 12:02:08 +01:00
Wojciech Kozyra b6c300b5c2 [eas-json] Detect dependency cycle for build profiles (#283)
* [eas-json] Detect dependency cycle for build profiles

* Update CHANGELOG.md

* review feedback
2021-03-22 11:15:38 +01:00
Wojciech Kozyra 0117dbfa5f [eas-cli] verify that "name" in app.sjon can be used as iOS target name (#280) 2021-03-18 12:15:52 +01:00
Dominik Sokal c106f8a1a4 fix project utils calls after merge 2021-03-18 09:55:55 +01:00
Dominik Sokal 8b9741a70c refactor project utils (#278) 2021-03-18 09:51:30 +01:00
Juwan Wheatley 271b85fd74 [eas-cli] create commands for managing environment secrets (#275)
* set up graphql queries and mutations

* add secrets: list command

* add create command

* list all secrets

* add deletion command, add ID to list, add EAS check to create

* update changelog

* fix typo

* use target enum

* use US cancel

* move AccountResolver to generic place

* secret isn't reassigned

* check for eas in delete

* make mutation inputs required

* update generated graphql files

* use findAccountByName directly instead of AccountResolver

* update list format

* move accountresolver back into device manager file

* restore old devicemanager source

* lint
2021-03-17 14:50:40 -04:00
Dominik Sokal 21a481790b [ENG-438] allow for installing custom bundler version (#277)
* allow for installing custom bundler version

* update CHANGELOG.md

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

* fix printing docs url
2021-03-17 13:52:52 +01:00
Wojciech Kozyra ab5d9237f1 [eas-cli] update external errors logic (#276) 2021-03-16 17:10:37 +01:00
Wojciech Kozyra 756853f2f3 [eas-cli] autmatically commit when EAS_BUILD_AUTOCOMMIT is set (#271) 2021-03-15 09:23:30 +01:00
Bartłomiej Klocek 5eb80d15f6 [ENG-509][submit] Fix local archive prompt (#273) 2021-03-12 11:34:31 +01:00
Dominik Sokal a98043b4ad print common error messages when builds fail (#272)
* print common error messages when builds fail

* update CHANGELOG

* address PR feedback

* address more PR feedback
2021-03-12 10:29:33 +01:00
jkh d0818932e7 [eas-cli] add --republish and --group flags to branch:publish command. (#253) 2021-03-11 05:37:18 -08:00
Dominik Sokal 23b60ed385 fix typo 2021-03-09 11:08:39 +01:00
Dominik Sokal 5d2811bf21 update CHANGELOG 2021-03-09 11:05:14 +01:00
Dominik Sokal 7b202f2aa7 v0.6.0 2021-03-09 10:42:59 +01:00
Dominik SokalandWojciech Kozyra fa6e9c1735 [ENG-444] support internal distribution in non-interactive builds (#269)
* support internal distribution in non-interactive builds

* update CHANGELOG.md

* address PR feedback

* Update packages/eas-cli/src/credentials/ios/actions/new/SetupAdhocProvisioningProfile.ts

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

* address more PR feedback

* update error message

Co-authored-by: Wojciech Kozyra <[email protected]>
2021-03-09 10:40:52 +01:00
Wojciech KozyraandDominik Sokal c3300036d8 [eas-cli] handle unknown team name in device list (#268)
* [eas-cli] handle unknown team name in device list

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-03-05 14:05:06 +01:00
Dominik Sokal 5efcdb2940 [eas-cli] use Metadata type from @expo/eas-build-job (#267) 2021-03-05 09:43:23 +01:00
Wojciech Kozyra 8113450ea4 [eas-json] add experimental.disableIosBundleIdentifierValidation flag (#263) 2021-03-04 15:12:45 +01:00
Dominik Sokal d10b0c3c65 iOS builds: default to build configuration 'Release' (#266)
* iOS builds: default to build configuration 'Release'

* update CHANGELOG.md

* fix tests
2021-03-04 10:25:59 +01:00
Bartosz Kaszubowski e8de0e8712 add installation section to eas-cli readme (#255) 2021-03-03 16:53:50 -08:00
Bartłomiej Klocek e1da1099da [eas submit] UX improvements (#144)
* Add finish message for iOS Submissions

* Improve summary table

* Improve table look

* Update CHANGELOG
2021-03-03 16:57:08 +01:00
Dominik Sokal 264aa8fefa [ENG-326] send more build metadata: release channel, build profile, git commit hash (#265)
* send more build metadata: release channel, build profile, git commit hash

* update CHANGELOG.md

* address PR feedback

* upgrade @expo/config-plugins 1.0.19-alpha.0 -> 1.0.21-alpha.0

* address PR feedback
2021-03-03 16:00:59 +01:00
jkh 941d0294b7 [eas-cli] use generated graphql types (#262) 2021-03-03 06:53:54 -08:00
Wojciech Kozyra 366f4e8914 [eas-cli] display size of the archive (#264) 2021-03-03 14:11:31 +01:00
Dominik Sokal 1460c3c5a2 add first item to the PR checklist (#260) 2021-03-03 09:16:37 +01:00
jkh 6271472827 [eas-cli] add branch:delete command (#248) 2021-03-02 09:43:53 -08:00
Dominik Sokal 3bc4a054ae update CHANGELOG.md 2021-03-02 11:35:19 +01:00
Dominik Sokal dd85d559d7 v0.5.0 2021-03-02 11:14:43 +01:00
Wojciech Kozyra ab10b082e5 [eas-cli] fix submit with tar.gz files (#257)
* [eas-cli] fix submit with tar.gz files

* [eas-cli] update CHANGELOG.md
2021-03-02 10:52:19 +01:00
Dominik Sokal 60b60066f9 enable internal distribution support for self-managed credentials (#256)
* enable internal distribution support for self-managed credentials

* update CHANGELOG.md

* address PR feedback

* fix typo
2021-03-02 10:49:59 +01:00
Wojciech Kozyra 9e9e517698 [eas-cli] detect untracked files when checking for changes in repository (#259)
* [eas-cli] detect untracked files when checking for changes in repository

* Update CHANGELOG.md
2021-03-02 10:49:27 +01:00
Wojciech Kozyra 020c0f2b98 [eas-cli] commit all files before build (#251)
* [eas-cli] commit all files before build

* Update CHANGELOG.md
2021-03-01 10:08:25 +01:00
Wojciech Kozyra dd74ebdc51 [eas-cli] use error.expoApiV2ErrorCode to check error code (#252) 2021-03-01 09:24:22 +01:00
Wojciech KozyraandDominik Sokal ddf7381efb [eas-cli] EAS Build - build cache support (#247)
* [eas-cli] EAS Build - build cache support

* Update CHANGELOG

* Update CHANGELOG.md

Co-authored-by: Dominik Sokal <[email protected]>
2021-03-01 09:24:05 +01:00
Cedric van Puttenandjkh 3b67b912e4 [eas-cli] Add channel edit list and view commands (#249)
* [eas-cli] add channel:view command

* [eas-cli] add channel:list and channel:edit commands

* [eas-cli] Fix styling issue in channel edit command

* [eas-cli] Assume single branch and update when listing

* [eas-cli] Move graphql queries back to command files

* [eas-cli] Cleam up graphql limits

* [eas-cli] Show current branch when editing channel

* [eas-cli] Reword channel:edit description

Co-authored-by: jkh <[email protected]>

* [eas-cli] Reword channel:edit question

Co-authored-by: jkh <[email protected]>

* [eas-cli] Update table output of channel commands

* [eas-cli] Remove limit from properties and hardcode limit

Co-authored-by: jkh <[email protected]>

* [eas-cli] Reformat todos to fit repo better

Co-authored-by: jkh <[email protected]>

* [eas-cli] Remove limit from channel list query

Co-authored-by: jkh <[email protected]>
2021-02-27 14:54:02 +01:00
Cedric van Putten 37d7fa4510 [eas-cli] Fix json output for branch and channel commands (#250) 2021-02-27 14:30:03 +01:00
jkh 096601473a [eas-cli] fix tsc in prompt.ts (#254) 2021-02-26 07:42:28 -08:00
jkh 2e25ed81ca [eas-cli] rename fileHash to fileSHA256 (#244) 2021-02-23 04:35:48 -08:00
jkh 4e11ea1d77 [eas-cli] add default bundler for branch:publish (#237) 2021-02-23 04:28:37 -08:00
Dominik Sokal d9d470426b upgrade @expo/eas-build-job 0.2.12 -> 0.2.13 (#245)
* upgrade @expo/eas-build-job 0.2.12 -> 0.2.13

* fix tests

* update CHANGELOG.md
2021-02-23 12:52:24 +01:00
Dominik Sokal 119b4ad8b2 update CHANGELOG.md 2021-02-23 11:26:26 +01:00
Dominik Sokal ae1e9f000d update README.md 2021-02-23 11:25:46 +01:00
Dominik Sokal eb0505f8f8 v0.4.3 2021-02-23 11:12:02 +01:00
Dominik Sokal c8f7800ab5 update CHANGELOG.md 2021-02-23 11:10:15 +01:00
Wojciech Kozyra bc4e7becdd [eas-cli] use fixed version of @expo/eas-json (#243) 2021-02-22 15:03:06 +01:00
jkh 71ece19c40 [eas-cli] rename updateGroup, updateMessage, branchName, channelName (#242) 2021-02-19 12:07:39 -08:00
Dominik Sokal e9053c0d15 add support for ios simulator builds (#240)
* add support for ios simulator builds

* update CHANGELOG.md

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

* fix lint errors
2021-02-19 13:35:06 +01:00
Dominik Sokal cf76296142 update CHANGELOG.md 2021-02-18 11:43:33 +01:00
Dominik Sokal 5e5d3626f4 v0.4.2 2021-02-18 11:36:14 +01:00
Dominik Sokal 5808c5b412 add CHANGELOG.md (#239) 2021-02-18 11:30:13 +01:00
Dominik Sokal cd64392d44 [eas-cli] bump @expo/config-plugins to 1.0.19-alpha.0 (#238) 2021-02-18 11:20:39 +01:00
Wojciech Kozyra 79f889e16c v0.4.1 2021-02-16 19:17:33 +01:00
Wojciech Kozyra 595bfecf1c [eas-cli] fix "buildType" is not allowed error 2021-02-16 19:15:10 +01:00
Dominik Sokal ced2e192eb v0.4.0 2021-02-16 15:08:05 +01:00
Dominik Sokal ee91612c07 [eas-cli][eas-json] add schemeBuildConfiguration to eas.json (#234)
* [eas-cli] use newer @expo/config-plugins

* [eas-cli][eas-json] add buildConfiguration to eas.json

* address PR feedback

* upgrade @expo/eas-build-job and @expo/config-plugins

* [eas-json] reuse type from @expo/eas-build-job

* [eas-json] fix exports
2021-02-16 14:40:57 +01:00
Quinlan Jung 4d815c1fd9 [eas-cli] tests for credential removal (#236)
* [eas-cli] tests for credential removal

* fix tsc
2021-02-15 18:11:51 -08:00
Quinlan Jung d2687a55b7 [eas-cli] basic RemoveDistCert (and RemoveProvisioningProfile) (#221)
* [www] remove dist cert and prov profile

* pr feedback

* fix tests

* cleanup removal code

* fix tsc
2021-02-15 17:50:24 -08:00
jkh 41142d33dc [eas-cli] remove trailing whitespace from default branch name (#235) 2021-02-15 13:12:52 -08:00
jkh 52ab6107c5 [eas-cli] rename EAS release to branch (#233) 2021-02-15 12:41:12 -08:00
Wojciech Kozyra 56ac348ed4 [eas-cli] configurable builder environment (#230) 2021-02-15 08:55:07 +01:00
Dominik Sokal 1ac0af53ab [ENG-9] [eas-cli] implement automatic version bumping for ios builds (#231)
* [eas-cli] implement automatic version bumping for ios builds

* [eas-cli] autoincrement -> autoIncrement

* [eas-cli] code style fix

* [eas-cli][eas-json] address PR feedback

* more pr feedback
2021-02-12 14:37:19 +01:00
Wojciech Kozyra f9361b471c [eas-cli] fix running builds from subdirectory (#229) 2021-02-09 11:08:38 +01:00
Paul Ridgway 8f39e0dcde Allow wait to be set to false for eas build #225 (#226) 2021-02-08 13:23:31 +01:00
Wojciech Kozyra 81de402d2d [eas-cli] add build:cancel command (#219)
* [eas-cli] add build:cancel command

* review feedback
2021-02-08 13:23:08 +01:00
Wojciech Kozyra 9d6e3f52d8 [eas-cli] Fix missing android.package error (#224) 2021-02-08 13:20:22 +01:00
Will Schurman 6a0a9bbaaa Fix potential broken this binding when passing logger function as argument (#227) 2021-02-04 09:38:02 -08:00
Will Schurman 24bc29b3cd Refactor log module so that weird __esModule mocks are not needed (#223) 2021-02-03 16:26:58 -08:00
Brent Vatne aac5ebff73 v0.3.1 2021-01-25 12:05:30 -08:00
Wojciech KozyraandDominik Sokal 61cc5b0a4a [eas-cli] support canceled build status (#217)
* [eas-cli] support `canceled` build status

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

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-01-22 11:00:56 +01:00
Wojciech Kozyra 1383783c33 [eas-cli] check if tty is atched on prompt (#216) 2021-01-21 15:24:25 +01:00
Dominik Sokal 971129be6b [eas-cli] improve monorepo support - fix configuring expo-updates (#214)
* [eas-cli] improve monorepo support - fix configuring expo-updates

* [eas-cli] bump @expo/config-plugins to 1.0.15-alpha.0
2021-01-21 12:08:30 +01:00
Quinlan Jung f4fb10483a [eas-cli] filter stale profiles (#215)
* [eas-cli] filter stale profiles

* pr feedback
2021-01-20 15:30:54 -08:00
Wojciech Kozyra 256a0e1f22 [eas-cli] return non-zero exit code if prompt is displayed on CI (#208) 2021-01-20 14:57:08 +01:00
563 changed files with 51305 additions and 21385 deletions

No files matched your search

+40 -12
View File
@@ -1,15 +1,9 @@
module.exports = {
root: true,
extends: ['universe/node'],
overrides: [
{
files: ['**/__tests__/*'],
},
],
globals: {
jasmine: false,
},
plugins: ['async-protect', 'node'],
rules: {
'no-console': 'warn',
'no-constant-condition': ['warn', { checkLoops: false }],
'sort-imports': [
'warn',
@@ -18,10 +12,44 @@ module.exports = {
},
],
'flowtype/no-types-missing-file-annotation': 'off',
curly: 'warn',
'import/no-cycle': 'error',
'import/no-extraneous-dependencies': [
'error',
{ devDependencies: ['**/__tests__/**/*', '**/__mocks__/**/*'] },
],
'import/no-relative-packages': 'error',
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'lodash',
message: "Don't use lodash, it's heavy!",
},
{
name: 'node-fetch',
message: 'Use got instead, node-fetch is used only for the GraphQL client.',
},
],
},
],
'no-underscore-dangle': ['error', { allow: ['__typename'] }],
'@typescript-eslint/explicit-function-return-type': [
'warn',
{
allowExpressions: true,
},
],
'async-protect/async-suffix': 'error',
'node/no-sync': 'error',
},
settings: {
react: {
version: 'detect',
overrides: [
{
files: ['**/__tests__/**/*.ts', '**/__mocks__/**/*.ts'],
rules: {
'async-protect/async-suffix': 'off',
},
},
},
],
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

+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'
+7
View File
@@ -1,3 +1,10 @@
<!-- Thanks for contributing to _EAS CLI_! Before you submit your pull request, please make sure to check the following boxes by putting an x in the [ ] (don't: [x ], [ x], do: [x]) -->
# Checklist
- [ ] I've added an entry to [CHANGELOG.md](https://github.com/expo/eas-cli/blob/main/CHANGELOG.md#main) if necessary. You can comment this pull request with `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]` and CHANGELOG.md will be updated automatically.
- [ ] I've tagged the changelog entry with `[EAS BUILD API]` if it's a part of a breaking change to EAS Build API (only possible when updating `@expo/eas-build-job` package).
# Why
Please describe the motivation for this PR, and link to relevant GitHub issues, forums posts, or feature requests.
+46
View File
@@ -0,0 +1,46 @@
on:
issue_comment:
types: [created]
name: Changelog
jobs:
pr-commented:
runs-on: ubuntu-latest
if: ${{ github.event.issue.pull_request && github.event.issue.user.login == github.event.comment.user.login && startsWith(github.event.comment.body, '/changelog-entry ') }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '16'
- name: Checkout Pull Request
run: hub pr checkout ${{ github.event.issue.number }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
working-directory: ./scripts
run: yarn install --frozen-lockfile --check-files
- name: Add changelog entry
working-directory: ./scripts
run: |
PARAMS=${PR_COMMENT_BODY#/changelog-entry }
if [[ ! "$PARAMS" =~ " " ]]; then
echo "Invalid /changelog-entry command"
exit 1
fi
CATEGORY=$( cut -d ' ' -f 1 <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
yarn changelog-entry "$CATEGORY" "$MESSAGE"
env:
GITHUB_PR_AUTHOR: ${{ github.event.issue.user.login }}
GITHUB_PR_NUMBER: ${{ github.event.issue.number }}
PR_COMMENT_BODY: ${{ github.event.comment.body }}
- name: Commit and push
run: |
# use the last commit author name and email
git config --global user.name "$(git --no-pager log --format=format:'%an' -n 1)"
git config --global user.email "$(git --no-pager log --format=format:'%ae' -n 1)"
git add CHANGELOG.md
git commit -m "update CHANGELOG.md"
git push
+72 -5
View File
@@ -15,7 +15,7 @@ jobs:
- id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
draft: true
@@ -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,71 @@ 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
publish-to-npm:
name: Publish to npm
runs-on: ubuntu-latest
needs: [build-linux, build-mac, build-windows]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
registry-url: 'https://registry.npmjs.org/'
scope: 'expo'
node-version: '16'
- name: Install dependencies
run: yarn install --frozen-lockfile --check-files
- name: Build
run: yarn build
- name: Publish packages to npm
run: yarn lerna publish from-package --yes --no-verify-access
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
release-changelog:
name: Update changelog and publish release
runs-on: ubuntu-latest
needs: publish-to-npm
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '16'
- name: Install dependencies
working-directory: ./scripts
run: yarn install --frozen-lockfile --check-files
- name: Resolve version
run: echo "EAS_CLI_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Update changelog
working-directory: ./scripts
run: yarn --silent release-changelog $EAS_CLI_VERSION > /tmp/current-changelog
- name: Publish release
run: hub release edit v$EAS_CLI_VERSION --draft=false -F /tmp/current-changelog
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Commit and push
run: |
git fetch
git checkout main
git config --global user.email "[email protected]"
git config --global user.name "Expo CI"
git add CHANGELOG.md
git commit -m "update CHANGELOG.md"
git push
- name: Prepare changelog for Slack
run: |
tail -n +3 /tmp/current-changelog > /tmp/changelog.md
npx slackmd /tmp/changelog.md
echo 'SLACK_CHANGELOG<<EOF' >> $GITHUB_ENV
cat /tmp/changelog.slack.md >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Slack Notification
uses: rtCamp/action-slack-notify@v2
env:
SLACK_CHANNEL: eas-cli
SLACK_COLOR: ${{ job.status }}
SLACK_ICON: https://github.com/expo-ci.png?size=48
SLACK_TITLE: 'Released eas-cli@${{ env.EAS_CLI_VERSION }}'
SLACK_MESSAGE: ${{ env.SLACK_CHANGELOG }}
SLACK_USERNAME: 'Expo CI'
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_FOOTER: ''
+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'
+3 -3
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:
@@ -22,7 +22,7 @@ jobs:
with:
node-version: ${{ matrix.node }}
- run: yarn install --frozen-lockfile --check-files
- run: yarn lerna run build
- run: yarn typecheck
- run: yarn test
if: ${{ !matrix.coverage }}
- run: yarn test --coverage
+1
View File
@@ -11,3 +11,4 @@ tmp
node_modules
coverage
.history
.DS_Store
+1
View File
@@ -0,0 +1 @@
save-prefix ""
+763
View File
@@ -0,0 +1,763 @@
# Changelog
This is the log of notable changes to EAS CLI and related packages.
## main
### 🛠 Breaking changes
### 🎉 New features
- Add ASC Api Key generation workflow. ([#718](https://github.com/expo/eas-cli/pull/718) by [@quinlanj](https://github.com/quinlanj))
- Add support for removal of ASC Api Keys. ([#721](https://github.com/expo/eas-cli/pull/721) by [@quinlanj](https://github.com/quinlanj))
- Allow users to assign an ASC Api Key to their project. ([#719](https://github.com/expo/eas-cli/pull/719) by [@quinlanj](https://github.com/quinlanj))
- Add setup support for ASC Api Keys. ([#733](https://github.com/expo/eas-cli/pull/733) by [@quinlanj](https://github.com/quinlanj))
- Show initiating user display name when selecting a build to submit. ([#730](https://github.com/expo/eas-cli/pull/730) by [@barthap](https://github.com/barthap))
- Handle Apple servers maintenance error in `eas submit`. ([#738](https://github.com/expo/eas-cli/pull/738) by [@barthap](https://github.com/barthap))
- Integrate ASC Api Key with submissions workflow. ([#737](https://github.com/expo/eas-cli/pull/737) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
### 🧹 Chores
- Clean up credentials prompt method. ([#728](https://github.com/expo/eas-cli/pull/728) by [@quinlanj](https://github.com/quinlanj))
- Bump @expo/apple-utils to 0.0.0-alpha.26. ([#723](https://github.com/expo/eas-cli/pull/723] by [@brentvatne](https://github.com/brentvatne))
- Grammar: replace setup with set up. ([#735](https://github.com/expo/eas-cli/pull/735) by [@quinlanj](https://github.com/quinlanj))
- Improve VCS workflow migration experience. ([#732](https://github.com/expo/eas-cli/pull/732) by [@wkozyra95](https://github.com/wkozyra95))
## [0.34.1](https://github.com/expo/eas-cli/releases/tag/0.34.1) - 2021-11-02
### 🐛 Bug fixes
- Don't show commit prompt in no-commit workflow after installing `expo-dev-client`. ([#722](https://github.com/expo/eas-cli/pull/722) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Add App Store Connect Api Key graphql type and print support. ([#717](https://github.com/expo/eas-cli/pull/717) by [@quinlanj](https://github.com/quinlanj))
## [0.34.0](https://github.com/expo/eas-cli/releases/tag/0.34.0) - 2021-11-01
### 🛠 Breaking changes
- `[EAS BUILD API]` Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701) and [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal))
- Don't print logs to `stderr`. ([#708](https://github.com/expo/eas-cli/pull/708) by [@dsokal](https://github.com/dsokal))
- Remove automatic migration for legacy `eas.json` format introduced in [v0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2). ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
- Add required `cli` field in root of `eas.json` ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Better error message when eas.json is invalid. ([#707](https://github.com/expo/eas-cli/pull/707) by [@dsokal](https://github.com/dsokal))
- Fix credentials workflow for new users: add additionalTypenames for credentials. ([#703](https://github.com/expo/eas-cli/pull/703) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Add additionalTypenames for other queries. ([#704](https://github.com/expo/eas-cli/pull/704) by [@quinlanj](https://github.com/quinlanj))
- Fix errors after pulling down most recent graphql schema changes. ([#713](https://github.com/expo/eas-cli/pull/713) by [@quinlanj](https://github.com/quinlanj))
- Refactor credentials manager. ([#712](https://github.com/expo/eas-cli/pull/712) by [@quinlanj](https://github.com/quinlanj))
- Remove unneeded parameters. ([#710](https://github.com/expo/eas-cli/pull/710) by [@quinlanj](https://github.com/quinlanj))
- Enforce additionalTypenames for graphql queries. ([#709](https://github.com/expo/eas-cli/pull/709) by [@quinlanj](https://github.com/quinlanj))
## [0.33.1](https://github.com/expo/eas-cli/releases/tag/0.33.1) - 2021-10-22
### 🎉 New features
- Compute runtime version using config-plugins. ([#697](https://github.com/expo/eas-cli/pull/697) by [@jkhales](https://github.com/jkhales))
### 🐛 Bug fixes
- [eas-cli] Skip validating updates scripts integration, which no longer exist in SDK 43+ ([#706](https://github.com/expo/eas-cli/pull/706) by [@brentvatne](https://github.com/brentvatne))
## [0.33.0](https://github.com/expo/eas-cli/releases/tag/0.33.0) - 2021-10-20
### 🎉 New features
- Make "production" the default profile for building and submitting. ([#677](https://github.com/expo/eas-cli/pull/677)) by [@jonsamp](https://github.com/jonsamp)
- Previously, the default profile when running `eas build` or `eas submit` was "release". We're changing it to a more recognizable name that is consistent with our docs, which is now "production". You can always specify a profile manually with the `--profile` flag. For this major version, if you do not have a profile named "production", and still have a profile named "release", we will fall back to the "release" profile as the default, however you'll see a warning as we're going to remove that behavior in the next major release of EAS CLI.
- To upgrade, update **eas.json** to have a "production" profile in both the `build` and `submit` objects. If you already have a project set up, this will replace the existing "release" profile. After the change, eas.json should have the following profiles:
```json
{
"build": {
"production": { ... }
},
"submit": {
"production": { ... }
}
}
```
### 🐛 Bug fixes
- Skip the second prompt for Apple ID if the user is already signed in with Apple. ([#691](https://github.com/expo/eas-cli/pull/691) by [@dsokal](https://github.com/dsokal))
- Unify reading `app.json`. ([#692](https://github.com/expo/eas-cli/pull/692) by [@dsokal](https://github.com/dsokal))
## [0.32.0](https://github.com/expo/eas-cli/releases/tag/0.32.0) - 2021-10-15
### 🎉 New features
- Add App Store Connect Api Key support to iOS submissions. ([#678](https://github.com/expo/eas-cli/pull/678) by [@quinlanj](https://github.com/quinlanj))
- Create/list/revoke App Store Connect Api keys via Apple apis. ([#687](https://github.com/expo/eas-cli/pull/687) by [@quinlanj](https://github.com/quinlanj))
- Add ability to select a build from a list in `eas submit` interactive mode. ([#688](https://github.com/expo/eas-cli/pull/688) by [@barthap](https://github.com/barthap))
### 🐛 Bug fixes
- Fix printing App Store Connect URL after submission. ([#683](https://github.com/expo/eas-cli/pull/683) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
- Add App Store Connect Api Key fields to `eas.json`. ([#684](https://github.com/expo/eas-cli/pull/684) by [@quinlanj](https://github.com/quinlanj))
- Enable no-underscore-dangle eslint rule. ([#686](https://github.com/expo/eas-cli/pull/686) by [@dsokal](https://github.com/dsokal))
## [0.31.1](https://github.com/expo/eas-cli/releases/tag/0.31.1) - 2021-10-08
### 🐛 Bug fixes
- Google Service Account Keys: Fix non-interactive bug in credentials service workflow ([#682](https://github.com/expo/eas-cli/pull/682) by [@quinlanj](https://github.com/quinlanj))
## [0.31.0](https://github.com/expo/eas-cli/releases/tag/0.31.0) - 2021-10-08
### 🛠 Breaking changes
- Don't resolve the iOS builder image on the client side. EAS Build will use the appropriate iOS image for a given Expo SDK version unless the `image` is defined. This only applies to managed projects. ([#675](https://github.com/expo/eas-cli/pull/675) by [@wkozyra95](https://github.com/wkozyra95) + [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Integrate credentials service with Android submissions. ([#664](https://github.com/expo/eas-cli/pull/664) by [@quinlanj](https://github.com/quinlanj))
- Add option to review ad-hoc devices when reusing provisioning profile. ([#673](https://github.com/expo/eas-cli/pull/673) by [@dsokal](https://github.com/dsokal))
## [0.30.1](https://github.com/expo/eas-cli/releases/tag/0.30.1) - 2021-10-06
### 🐛 Bug fixes
- Fix `--json` flag when running EAS CLI on GitHub actions. ([#669](https://github.com/expo/eas-cli/pull/669) by [@dsokal](https://github.com/dsokal))
- Fix `"ios: mods.ios.infoPlist: Failed to find Info.plist linked to Xcode project."` warning when running `eas build` in a managed project.([#670](https://github.com/expo/eas-cli/pull/670) by [@brentvatne](https://github.com/brentvatne))
- Fix building monorepo projects on Windows. ([#671](https://github.com/expo/eas-cli/pull/671) by [@dsokal](https://github.com/dsokal))
## [0.30.0](https://github.com/expo/eas-cli/releases/tag/v0.30.0) - 2021-10-05
### 🛠 Breaking changes
- Enable auto-incrementing on Android. ([#645](https://github.com/expo/eas-cli/pull/645) by [@jkhales](https://github.com/jkhales))
### 🧹 Chores
- Ports detection for Google Service Account Keys into the credentials service. ([#660](https://github.com/expo/eas-cli/pull/660) by [@quinlanj](https://github.com/quinlanj))
- Improve iOS credentials printing. ([#657](https://github.com/expo/eas-cli/pull/657) by [@quinlanj](https://github.com/quinlanj))
- Automate `eas-cli` releases. ([#668](https://github.com/expo/eas-cli/pull/668) by [@dsokal](https://github.com/dsokal))
## [0.29.1](https://github.com/expo/eas-cli/releases/tag/v0.29.1) - 2021-09-29
### 🎉 New features
- More upload support for Google Service Account Keys. ([#649](https://github.com/expo/eas-cli/pull/649) by [@quinlanj](https://github.com/quinlanj))
- Allow the user to assign an existing Google Service Account Key to their project. ([#650](https://github.com/expo/eas-cli/pull/650) by [@quinlanj](https://github.com/quinlanj))
- Allow the user to remove a Google Service Account Key from their account. ([#658](https://github.com/expo/eas-cli/pull/658) by [@quinlanj](https://github.com/quinlanj))
- Adds setup support for Google Service Account Keys. ([#659](https://github.com/expo/eas-cli/pull/659) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Fix double prompt for Apple credentials when running `eas build --auto-submit`. ([#654](https://github.com/expo/eas-cli/pull/654) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Always use async `fs` functions. ([#652](https://github.com/expo/eas-cli/pull/652) by [@dsokal](https://github.com/dsokal))
- Improve Android credentials printing. ([#656](https://github.com/expo/eas-cli/pull/656) by [@quinlanj](https://github.com/quinlanj))
## [0.29.0](https://github.com/expo/eas-cli/releases/tag/v0.29.0) - 2021-09-28
### 🎉 New features
- Added upload support for Google Service Account Keys. ([#642](https://github.com/expo/eas-cli/pull/642) by [@quinlanj](https://github.com/quinlanj))
- Add Xcode 13 image. ([#651](https://github.com/expo/eas-cli/pull/651) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Credentials manager: stop prompting for an Android build profile every iteration. ([#641](https://github.com/expo/eas-cli/pull/641) by [@quinlanj](https://github.com/quinlanj))
- Don't display prompt in non-interactive mode when the metro config seems invalid. ([#644](https://github.com/expo/eas-cli/pull/644) by [@dsokal](https://github.com/dsokal))
- Read versions from correct `Info.plist`. ([#635](https://github.com/expo/eas-cli/pull/635) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Move away from weird convention of exports. ([#647](https://github.com/expo/eas-cli/pull/647) by [@quinlanj](https://github.com/quinlanj))
## [0.28.2](https://github.com/expo/eas-cli/releases/tag/v0.28.2) - 2021-09-23
### 🐛 Bug fixes
- Use `log-symbols` to unify green ticks style. ([#639](https://github.com/expo/eas-cli/pull/639) by [@dsokal](https://github.com/dsokal))
- Fix `eas build --auto-submit` in fresh projects. ([#640](https://github.com/expo/eas-cli/pull/640) by [@dsokal](https://github.com/dsokal))
## [0.28.1](https://github.com/expo/eas-cli/releases/tag/v0.28.1) - 2021-09-22
### 🐛 Bug fixes
- Prevent ora spinners breaking when debug logs are enabled. ([#575](https://github.com/expo/eas-cli/pull/575) by [@EvanBacon](https://github.com/EvanBacon))
- Do not fail `eas build` when transient network errors occur. ([#638](https://github.com/expo/eas-cli/pull/638) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Add GraphQL support for GoogleServiceAccountKey. ([#632](https://github.com/expo/eas-cli/pull/632) by [@quinlanj](https://github.com/quinlanj))
- Remove GraphQL support for the App Specific Password. ([#633](https://github.com/expo/eas-cli/pull/633) by [@quinlanj](https://github.com/quinlanj))
## [0.28.0](https://github.com/expo/eas-cli/releases/tag/v0.28.0) - 2021-09-20
### 🎉 New features
- Use envs from build profile to resolve app config when auto-submitting. ([#614](https://github.com/expo/eas-cli/pull/614) by [@dsokal](https://github.com/dsokal))
- Support multi flavor Android projects. ([#595](https://github.com/expo/eas-cli/pull/595) by [@wkozyra95](https://github.com/wkozyra95))
- Improve experience when using the build details page as a build artifact URL in `eas submit`. ([#620](https://github.com/expo/eas-cli/pull/620) by [@dsokal](https://github.com/dsokal))
- Better error message when eas.json is invalid JSON. ([#618](https://github.com/expo/eas-cli/pull/618) by [@dsokal](https://github.com/dsokal))
- Add warning about the legacy build service IDs in `eas submit`. ([#624](https://github.com/expo/eas-cli/pull/624) by [@dsokal](https://github.com/dsokal))
- Validate that project includes `expo-dev-client` when building with `developmentClient` flag. ([#627](https://github.com/expo/eas-cli/pull/627) by [@dsokal](https://github.com/dsokal))
- Better experience when not logged in in non-interactive mode. ([#628](https://github.com/expo/eas-cli/pull/628) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Reduce `eas-cli` size by almost getting rid of `lodash` from dependencies. ([#605](https://github.com/expo/eas-cli/pull/605) by [@dsokal](https://github.com/dsokal))
- Enforce explicit return type in all functions. ([#619](https://github.com/expo/eas-cli/pull/619) by [@wkozyra95](https://github.com/wkozyra95))
- Enforce `Async` suffix for async functions. ([#623](https://github.com/expo/eas-cli/pull/623) by [@dsokal](https://github.com/dsokal))
## [0.27.1](https://github.com/expo/eas-cli/releases/tag/v0.27.1) - 2021-09-10
### 🐛 Bug fixes
- Fixed force-creation, app secret creation, and secret deletion mutations. ([#606](https://github.com/expo/eas-cli/pull/606) by [@fiberjw](https://github.com/fiberjw))
## [0.27.0](https://github.com/expo/eas-cli/releases/tag/v0.27.0) - 2021-09-10
### 🎉 New features
- Implement support for `--platform all` in `eas submit`. ([#598](https://github.com/expo/eas-cli/pull/598) by [@dsokal](https://github.com/dsokal))
- Implement support for `--non-interactive` in `eas submit`. ([#600](https://github.com/expo/eas-cli/pull/600) by [@dsokal](https://github.com/dsokal))
- Add auto-submit feature. Run `eas build --auto-submit` to submit automatically on build complete. ([#603](https://github.com/expo/eas-cli/pull/603) by [@dsokal](https://github.com/dsokal))
- Allow using env var for `android.serviceAccountKeyPath` in submit profiles. ([#604](https://github.com/expo/eas-cli/pull/604) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix detecting `googleServicesFile` on Windows. ([#602](https://github.com/expo/eas-cli/pull/602) by [@wkozyra95](https://github.com/wkozyra95))
## [0.26.0](https://github.com/expo/eas-cli/releases/tag/v0.26.0) - 2021-09-08
### 🛠 Breaking changes
- Require `eas submit` to be configured with eas.json submit profiles (see [https://docs.expo.dev/submit/eas-json/](https://docs.expo.dev/submit/eas-json/) for details). 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))
## [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 multi-target iOS projects. ([#414](https://github.com/expo/eas-cli/pull/414) by [@dsokal](https://github.com/dsokal))
## [0.15.1](https://github.com/expo/eas-cli/releases/tag/v0.15.1) - 2021-05-25
### 🎉 New features
- Support auto capabilities in managed workflow using Expo config plugin introspection. ([#419](https://github.com/expo/eas-cli/pull/419) by [@EvanBacon](https://github.com/EvanBacon))
### 🐛 Bug fixes
- Fix sending `appBuildVersion` as part of the build metadata. ([#423](https://github.com/expo/eas-cli/pull/423) by [@dsokal](https://github.com/dsokal))
## [0.15.0](https://github.com/expo/eas-cli/releases/tag/v0.15.0) - 2021-05-20
### 🛠 Breaking changes
- `[EAS BUILD API]` Remove "Auto" option for `schemeBuildConfiguration` and make the old "Auto" behaviour the default. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
- Remove `experimental.disableIosBundleIdentifierValidation` flag from eas.json. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
- Deprecate `android.package` and `ios.bundleIdentifier` in app config for generic projects. EAS CLI depends on the values in native code now. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
- Remove application id synchronization (`android.package` and `ios.bundleIdentifier`) between app.json and native code when running builds. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Auto sync associated domains capability before building. ([#384](https://github.com/expo/eas-cli/pull/384) by [@EvanBacon](https://github.com/EvanBacon))
- Add eas init command. [#402](https://github.com/expo/eas-cli/pull/402) by [@jkhales](https://github.com/jkhales))
- Allow for arbitrary string values in `schemeBuildConfiguration`. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
- Allow for installing custom `expo-cli` version on EAS Build. ([#409](https://github.com/expo/eas-cli/pull/409) by [@randomhajile](https://github.com/randomhajile))
- Support PKCS kesytores for Android. ([#398](https://github.com/expo/eas-cli/pull/398) by [@wkozyra95](https://github.com/wkozyra95))
- Support empty passwords in Android and iOS keystores. ([#398](https://github.com/expo/eas-cli/pull/398) by [@wkozyra95](https://github.com/wkozyra95))
- Add more build metadata - `appBuildVersion`. ([#413](https://github.com/expo/eas-cli/pull/413) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix failure when publishing without the platform flag. ([#415](https://github.com/expo/eas-cli/pull/415) by [@jkhales](https://github.com/jkhales))
- Pin versions in package.json. ([#399](https://github.com/expo/eas-cli/pull/399) by [@dsokal](https://github.com/dsokal))
- Revert [0ac2f](https://github.com/expo/eas-cli/commit/0ac2fb77a118df609381c4e350aa68609340c3cd) as the root cause of the issue has been fixed in [#399](https://github.com/expo/eas-cli/pull/399).
- Include development-client in valid buildType for internal distribution. ([#410](https://github.com/expo/eas-cli/pull/410) by [@brentvatne](https://github.com/brentvatne))
## [0.14.1](https://github.com/expo/eas-cli/releases/tag/v0.14.1) - 2021-05-11
### 🐛 Bug fixes
- Make the default urql requestPolicy 'network-only'. ([#397](https://github.com/expo/eas-cli/pull/397) by [@jkhales](https://github.com/jkhales))
## [0.14.0](https://github.com/expo/eas-cli/releases/tag/v0.14.0) - 2021-05-10
### 🛠 Breaking changes
- Make eas branch:publish work with expo-cli >= 4.4.3 ([#366](https://github.com/expo/eas-cli/pull/366) by [@jkhales](https://github.com/jkhales))
- Create asset keys without an extension. ([#366](https://github.com/expo/eas-cli/pull/366) by [@jkhales](https://github.com/jkhales))
### 🎉 New features
- Display platforms supported by an update group ([#391](https://github.com/expo/eas-cli/pull/391) by [@jkhales](https://github.com/jkhales))
- Add a --platform flag to branch:publish command ([#389](https://github.com/expo/eas-cli/pull/389) by [@jkhales](https://github.com/jkhales))
- Release new credentials manager. ([#363](https://github.com/expo/eas-cli/pull/363) by [@quinlanj](https://github.com/quinlanj))
- Add QR code to install internal distribution build on device. ([#371](https://github.com/expo/eas-cli/pull/371) by [@axeldelafosse](https://github.com/axeldelafosse))
### 🧹 Chores
- Cleanup unused code. ([#364](https://github.com/expo/eas-cli/pull/364), [#365](https://github.com/expo/eas-cli/pull/365) by [@quinlanj](https://github.com/quinlanj))
- Redesign UX in beta credentials manager. ([#360](https://github.com/expo/eas-cli/pull/360) by [@quinlanj](https://github.com/quinlanj))
- Port more options to the beta credentials manager. ([#352](https://github.com/expo/eas-cli/pull/352), [357](https://github.com/expo/eas-cli/pull/357), [361](https://github.com/expo/eas-cli/pull/361) by [@quinlanj](https://github.com/quinlanj))
- Add getBuildProfileNamesAsync helper to EasJsonReader. ([#351](https://github.com/expo/eas-cli/pull/351) by [@quinlanj](https://github.com/quinlanj))
- Increase build timeout to 1 hour. ([#370](https://github.com/expo/eas-cli/pull/370) by [@wkozyra95](https://github.com/wkozyra95))
- Remove check for pending builds. ([#373](https://github.com/expo/eas-cli/pull/373) by [@wkozyra95](https://github.com/wkozyra95))
## [0.13.0](https://github.com/expo/eas-cli/releases/tag/v0.13.0) - 2021-04-22
### 🎉 New features
- Implement offline distribution certificate validation when running a build in non-interactive mode. ([#344](https://github.com/expo/eas-cli/pull/344) by [@dsokal](https://github.com/dsokal))
- Add support for building internal distribution apps for Apple Enterprise Teams. ([#344](https://github.com/expo/eas-cli/pull/344) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Display descriptive error message when API for EAS Build changes. ([#359](https://github.com/expo/eas-cli/pull/359) by [@wkozyra95](https://github.com/wkozyra95))
## [0.12.0](https://github.com/expo/eas-cli/releases/tag/v0.12.0) - 2021-04-22
### 🎉 New features
- Add the initiating user's username to the build metadata. ([#354](https://github.com/expo/eas-cli/pull/354) by [@dsokal](https://github.com/dsokal))
- Add `--clear-cache` flag for `eas build`. ([#355](https://github.com/expo/eas-cli/pull/355) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Fix the bug where the account name was used as the username. ([#354](https://github.com/expo/eas-cli/pull/354) by [@dsokal](https://github.com/dsokal))
## [0.11.1](https://github.com/expo/eas-cli/releases/tag/v0.11.1) - 2021-04-20
### 🎉 New features
- Resolve ios distribution types. ([#348](https://github.com/expo/eas-cli/pull/348) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Bump version of `@expo/apple-utils` to fix capabilities sync immediately after initial identifier registration. ([593c4](https://github.com/expo/eas-cli/commit/593c42f601396deba1751489343ec07b0a8876da) by [@brentvatne](https://github.com/brentvatne))
## [0.11.0](https://github.com/expo/eas-cli/releases/tag/v0.11.0) - 2021-04-20
### 🛠 Breaking changes
- `"credentialsSource": "auto"` is now deprecated. The option will use `"remote"` as the default value. ([#345](https://github.com/expo/eas-cli/pull/345) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Add validation when setting up ad-hoc provisioning profile in non-interactive mode. ([#338](https://github.com/expo/eas-cli/pull/338) by [@dsokal](https://github.com/dsokal))
- Allow for registering devices when running an ad-hoc build for the first time. ([#340](https://github.com/expo/eas-cli/pull/340) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix the bug where all Android managed builds produced AAB archives. The `buildType` property from `eas.json` is not ignored now. ([#349](https://github.com/expo/eas-cli/pull/349) by [@dsokal](https://github.com/dsokal))
## [0.10.0](https://github.com/expo/eas-cli/releases/tag/v0.10.0) - 2021-04-16
### 🛠 Breaking changes
- `secrets:<COMMAND>` is renamed to `secret:<COMMAND>` ([#315](https://github.com/expo/eas-cli/pull/315) by [@fiberjw](https://github.com/fiberjw))
- `secret:delete` now accepts `id` through a flag rather than an argument ([#315](https://github.com/expo/eas-cli/pull/315) by [@fiberjw](https://github.com/fiberjw))
### 🎉 New features
- Use special Expo SDK runtime version for managed projects ([#336](https://github.com/expo/eas-cli/pull/336)) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
- Fix the behavior where the provisioning profile was invalidated after syncing bundle id capabilities. ([#334](https://github.com/expo/eas-cli/pull/334) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Change build credentials summary format. ([#321](https://github.com/expo/eas-cli/pull/321) by [@dsokal](https://github.com/dsokal))
## [0.9.1](https://github.com/expo/eas-cli/releases/tag/v0.9.1) - 2021-04-09
### 🐛 Bug fixes
- Fix provisioning profile validation to actually check status. ([#318](https://github.com/expo/eas-cli/pull/318) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Replace Credentials REST API calls with GraphQL counterparts, also improved DX. ([#293](https://github.com/expo/eas-cli/pull/293), [#299](https://github.com/expo/eas-cli/pull/299), [#301](https://github.com/expo/eas-cli/pull/301), [#317](https://github.com/expo/eas-cli/pull/317) by [@quinlanj](https://github.com/quinlanj))
## [0.9.0](https://github.com/expo/eas-cli/releases/tag/v0.9.0) - 2021-04-09
### 🎉 New features
- Retry graphQL call on network or transient server errors. ([#320](https://github.com/expo/eas-cli/pull/320) by [@jkhales](https://github.com/jkhales))
- Display more friendly error messages when `eas submit` fails. ([#311](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
- Add support for managing webhooks (new commands: `webhook:create`, `webhook:view`, `webhook:list`, `webhook:update`, and `webhook:delete`). ([#314](https://github.com/expo/eas-cli/pull/314) by [@dsokal](https://github.com/dsokal))
- Support for local builds [experimental]. ([#305](https://github.com/expo/eas-cli/pull/305) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Fix the issue where the first iOS build fails for a project without `ios.bundleIdentifier` set in `app.json`. ([#319](https://github.com/expo/eas-cli/pull/319) by [@dsokal](https://github.com/dsokal))
- Bump @expo/config-plugins to fix `eas build:configure` on Windows. ([d0f3e](https://github.com/expo/eas-cli/commit/d0f3e387e7f31dbce4c3b93b8d083c7d20d9e30d) by [@brentvatne](https://github.com/brentvatne))
- Replace REST API calls with GraphQL counterparts. ([#333](https://github.com/expo/eas-cli/pull/333) by [@barthap](https://github.com/barthap))
## [0.8.1](https://github.com/expo/eas-cli/releases/tag/v0.8.1) - 2021-04-06
### 🐛 Bug fixes
- Fix the issue where the build status never got updated when running `eas build`. ([#310](https://github.com/expo/eas-cli/pull/310) by [@dsokal](https://github.com/dsokal))
## [0.8.0](https://github.com/expo/eas-cli/releases/tag/v0.8.0) - 2021-04-06
### 🛠 Breaking changes
- Change the way of disabling cache - add `cache.disabled` field. ([#295](https://github.com/expo/eas-cli/pull/295) by [@dsokal](https://github.com/dsokal))
- `secrets:create` now uses flags rather than positional arguments ([#300](https://github.com/expo/eas-cli/pull/300) by [@fiberjw](https://github.com/fiberjw))
- `secrets:create`'s `target` arg is now called `scope` ([#300](https://github.com/expo/eas-cli/pull/300) by [@fiberjw](https://github.com/fiberjw))
- `secrets:list`'s `target` property is now called `scope` ([#300](https://github.com/expo/eas-cli/pull/300) by [@fiberjw](https://github.com/fiberjw))
- `secrets:delete`'s `ID` arg is now optional ([#309](https://github.com/expo/eas-cli/pull/309) by [@fiberjw](https://github.com/fiberjw))
- `secrets:delete`'s now allows users to choose secrets from a list ([#309](https://github.com/expo/eas-cli/pull/309) by [@fiberjw](https://github.com/fiberjw))
### 🎉 New features
- `build:view` and `build:list` now showing the distribution type (store / internal) and release channel. ([#284](https://github.com/expo/eas-cli/pull/284) by [@vthibault](https://github.com/vthibault))
- Add analytics to EAS Build. ([#162](https://github.com/expo/eas-cli/pull/162) by [@wkozyra95](https://github.com/wkozyra95))
- Improve tar archive support in EAS Submit. ([#297](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
### 🐛 Bug fixes
- Fix environment secret creation prompt. ([#298](https://github.com/expo/eas-cli/pull/298) by [@fiberjw](https://github.com/fiberjw))
### 🧹 Chores
- Replace REST API calls with GraphQL counterparts. ([#286](https://github.com/expo/eas-cli/pull/286), [#288](https://github.com/expo/eas-cli/pull/288), [#303](https://github.com/expo/eas-cli/pull/303), [#306](https://github.com/expo/eas-cli/pull/306), [#307](https://github.com/expo/eas-cli/pull/307) by [@dsokal](https://github.com/dsokal))
## [0.7.0](https://github.com/expo/eas-cli/releases/tag/v0.7.0) - 2021-03-22
### 🎉 New features
- Print common error messages when builds fail. ([#272](https://github.com/expo/eas-cli/pull/272) by [@dsokal](https://github.com/dsokal))
- Commit automatically if `EAS_BUILD_AUTOCOMMIT` is set. ([#271](https://github.com/expo/eas-cli/pull/271) by [@wkozyra95](https://github.com/wkozyra95))
- Allow for installing custom `bundler` version on EAS Build. ([#277](https://github.com/expo/eas-cli/pull/277) by [@dsokal](https://github.com/dsokal))
- Add support for managing environment secrets. ([#275](https://github.com/expo/eas-cli/pull/275) by [@fiberjw](https://github.com/fiberjw))
### 🐛 Bug fixes
- Fix `eas submit` local archive prompt for `.aab` files when submitting for iOS. ([#273](https://github.com/expo/eas-cli/pull/273) by [@barthap](https://github.com/barthap))
- Verify whether "name" field in app.json contains any alphanumeric characters. ([#280](https://github.com/expo/eas-cli/pull/280) by [@wkozyra95](https://github.com/wkozyra95))
- Detect dependency cycles in eas.json build profiles. ([#283](https://github.com/expo/eas-cli/pull/283) by [@wkozyra95](https://github.com/wkozyra95))
## [0.6.0](https://github.com/expo/eas-cli/releases/tag/v0.6.0) - 2021-03-09
### 🛠 Breaking changes
- Generic iOS projects: build release builds by default. ([#266](https://github.com/expo/eas-cli/pull/266) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Log the size of the archived project when uploading. ([#264](https://github.com/expo/eas-cli/pull/264) by [@wkozyra95](https://github.com/wkozyra95))
- Add more build metadata (release channel, build profile name, git commit hash). ([#265](https://github.com/expo/eas-cli/pull/265) by [@dsokal](https://github.com/dsokal))
- Display App Store link after successful submission. ([#144](https://github.com/expo/eas-cli/pull/144) by [@barthap](https://github.com/barthap))
- Add `experimental.disableIosBundleIdentifierValidation` flag to eas.json. ([#263](https://github.com/expo/eas-cli/pull/263) by [@wkozyra95](https://github.com/wkozyra95))
- Support internal distribution in non-interactive builds. ([#269](https://github.com/expo/eas-cli/pull/269) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Print Apple Team ID in the output of `device:list` when the team name is unknown. ([#268](https://github.com/expo/eas-cli/pull/268) by [@wkozyra95](https://github.com/wkozyra95))
## [0.5.0](https://github.com/expo/eas-cli/releases/tag/v0.5.0) - 2021-03-02
### 🎉 New features
- Add support for build cache. ([#247](https://github.com/expo/eas-cli/pull/247) by [@wkozyra95](https://github.com/wkozyra95))
- Enable internal distribution support for self-managed credentials. ([#256](https://github.com/expo/eas-cli/pull/256) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Make sure all files are committed before build. ([#251](https://github.com/expo/eas-cli/pull/251) by [@wkozyra95](https://github.com/wkozyra95))
- Fix `eas submit` support for tar.gz files. ([#257](https://github.com/expo/eas-cli/pull/257)) by [@wkozyra95](https://github.com/wkozyra95))
- Show untracked files when checking `git status`. ([#259](https://github.com/expo/eas-cli/pull/259) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Upgrade `@expo/eas-build-job` from `0.2.12` to `0.2.13`. ([#245](https://github.com/expo/eas-cli/pull/245) by [@dsokal](https://github.com/dsokal))
## [0.4.3](https://github.com/expo/eas-cli/releases/tag/v0.4.3) - 2021-02-23
### 🎉 New features
- Add support for iOS simulator builds. ([#240](https://github.com/expo/eas-cli/pull/240) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Use fixed version of `@expo/eas-json`. ([#243](https://github.com/expo/eas-cli/pull/243) by [@wkozyra95](https://github.com/wkozyra95))
## [0.4.2](https://github.com/expo/eas-cli/releases/tag/v0.4.2) - 2021-02-18
### 🐛 Bug fixes
- Fix detecting application target (iOS builds). ([#238](https://github.com/expo/eas-cli/pull/238) by [@dsokal](https://github.com/dsokal))
## [0.4.1](https://github.com/expo/eas-cli/releases/tag/v0.4.1) - 2021-02-16
### 🐛 Bug fixes
- Fix `"buildType" is not allowed` error. ([#595bf](https://github.com/expo/eas-cli/commit/595bfecf1cbff0f76e7fd2049fe16f6f38bbe150) by [@wkozyra95](https://github.com/wkozyra95))
## [0.4.0](https://github.com/expo/eas-cli/releases/tag/v0.4.0) - 2021-02-16
### 🎉 New features
- Add build:cancel command. ([#219](https://github.com/expo/eas-cli/pull/219) by [@wkozyra95](https://github.com/wkozyra95))
- Implement version auto increment for iOS builds. ([#231](https://github.com/expo/eas-cli/pull/231) by [@dsokal](https://github.com/dsokal))
- Add support for builder environment customizations. ([#230](https://github.com/expo/eas-cli/pull/230) by [@wkozyra95](https://github.com/wkozyra95))
- Add `schemeBuildConfiguration` option for generic iOS builds. ([#234](https://github.com/expo/eas-cli/pull/234) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix `--no-wait` flag for `eas build`. ([#226](https://github.com/expo/eas-cli/pull/226) by [@paul-ridgway](https://github.com/paul-ridgway))
- Fix running builds from project subdirectories. ([#229](https://github.com/expo/eas-cli/pull/229) by [@wkozyra95](https://github.com/wkozyra95))
+7
View File
@@ -0,0 +1,7 @@
packages/eas-cli/src/build @dsokal @wkozyra95
packages/eas-cli/src/commands/build @dsokal @wkozyra95
packages/eas-cli/src/submit @barthap @dsokal
packages/eas-cli/src/commands/submit.ts @barthap @dsokal
packages/eas-json @dsokal @wkozyra95
+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
+8 -7
View File
@@ -1,9 +1,10 @@
## Releasing a new version
1. Add release notes to `CHANGELOG.md`.
1. Run `yarn release` in the repository root folder. This will create a new tag
like `v1.0.0`.
1. Wait until the `release` GitHub workflow has finished.
1. Once the release ready to go, you can go to
https://github.com/expo/eas-cli/releases and mark it as not draft.
1. Run `yarn lerna publish from-git` in the repository root folder. This will publish the packages to npm.
1. Run `yarn release` in the repository root folder.
2. Wait, the `release` workflow on GitHub Actions is going to take care of the rest (including publishing packages to npm and updating the changelog).
3. Check the changelog. If any of the entries have been tagged with `[EAS BUILD API]`
- Open `package.json` and note the version of `@expo/eas-build-job` used in the eas-cli.
- Find the `Publish` commit in [expo/eas-build](https://github.com/expo/eas-build/commits) for that version. In most cases, it will be the latest `Publish` commit.
- Find the `eas-cli-local-build-plugin` version published in that commit.
![example publish commit](./.gh-assets/eas-build-publish-commit.png)
- Run `npm dist-tags add eas-cli-local-build-plugin@VERSION latest` with the version from the previous step.
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2020-present 650 Industries, Inc. (aka Expo)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-1
View File
@@ -1,4 +1,3 @@
comment: false
coverage:
round: up
precision: 2
-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.3.0"
"version": "0.34.1"
}
+23 -13
View File
@@ -1,25 +1,35 @@
{
"private": true,
"workspaces": [
"packages/*"
],
"workspaces": ["packages/*"],
"scripts": {
"build": "lerna run build",
"build:tarballs:linux": "yarn build && yarn workspace eas-cli oclif-dev pack --targets linux-x64",
"typecheck": "lerna run typecheck",
"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"
"test": "jest",
"clean": "lerna run clean && rm -rf node_modules coverage"
},
"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": "8.0.0",
"eslint-plugin-async-protect": "1.1.0",
"eslint-plugin-node": "11.1.0",
"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.10.0",
"yarn": "1.22.15"
}
}
+12 -3
View File
@@ -1,9 +1,18 @@
const rootEslintrc = require('../../.eslintrc.js');
module.exports = {
rules: {
'import/no-cycle': 'error',
'import/no-extraneous-dependencies': [
'no-restricted-imports': [
'error',
{ devDependencies: ['**/__tests__/**/*', '**/__mocks__/**/*'] },
{
paths: [
...rootEslintrc.rules['no-restricted-imports'][1].paths,
{
name: 'ora',
message: 'Import ora from src/ora.ts instead.',
},
],
},
],
'graphql/template-strings': [
'error',
+2
View File
@@ -0,0 +1,2 @@
graphql.schema.json linguist-generated
src/graphql/generated.ts linguist-generated
+292 -89
View File
@@ -7,27 +7,28 @@ EAS command line tool
[![Downloads/week](https://img.shields.io/npm/dw/eas-cli.svg)](https://npmjs.org/package/eas-cli)
[![License](https://img.shields.io/npm/l/eas-cli.svg)](https://github.com/expo/eas-cli/blob/main/package.json)
<!-- toc -->
* [eas-cli](#eas-cli)
* [Installation](#installation)
* [Usage](#usage)
* [Commands](#commands)
<!-- tocstop -->
# Installation
```sh
npm install -g eas-cli
# or
yarn global add eas-cli
```
# Usage
<!-- usage -->
```sh-session
$ npm install -g eas-cli
$ eas COMMAND
running command...
$ eas (-v|--version|version)
eas-cli/0.3.0 darwin-x64 node-v14.13.1
$ eas --help [COMMAND]
USAGE
$ eas COMMAND
...
```sh
eas COMMAND
# runs the command
eas (-v|--version|version)
# prints the version
eas --help COMMAND
# outputs help for specific command
```
<!-- usagestop -->
# Commands
@@ -35,16 +36,30 @@ USAGE
* [`eas account:login`](#eas-accountlogin)
* [`eas account:logout`](#eas-accountlogout)
* [`eas account:view`](#eas-accountview)
* [`eas analytics [STATUS]`](#eas-analytics-status)
* [`eas build`](#eas-build)
* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
* [`eas build:configure`](#eas-buildconfigure)
* [`eas build:list`](#eas-buildlist)
* [`eas build:view [BUILDID]`](#eas-buildview-buildid)
* [`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 submit --platform=(android|ios)`](#eas-submit---platformandroidios)
* [`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`](#eas-submit)
* [`eas webhook:create`](#eas-webhookcreate)
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
* [`eas webhook:list`](#eas-webhooklist)
* [`eas webhook:update`](#eas-webhookupdate)
* [`eas webhook:view ID`](#eas-webhookview-id)
## `eas account:login`
@@ -58,7 +73,7 @@ ALIASES
$ eas login
```
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.3.0/build/commands/account/login.ts)_
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/login.js)_
## `eas account:logout`
@@ -72,7 +87,7 @@ ALIASES
$ eas logout
```
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.3.0/build/commands/account/logout.ts)_
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/logout.js)_
## `eas account:view`
@@ -86,11 +101,22 @@ ALIASES
$ eas whoami
```
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.3.0/build/commands/account/view.ts)_
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/account/view.js)_
## `eas analytics [STATUS]`
view or change analytics settings
```
USAGE
$ eas analytics [STATUS]
```
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/analytics.js)_
## `eas build`
Start a build
start a build
```
USAGE
@@ -98,14 +124,40 @@ USAGE
OPTIONS
-p, --platform=(android|ios|all)
--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
--wait Wait for build(s) to complete
--auto-submit Submit on build complete using the submit profile with the same name as the
build profile
--auto-submit-with-profile=PROFILE_NAME Submit on build complete using the submit profile with provided name
--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_NAME Name of the build profile from eas.json. Defaults to "production" if defined
in eas.json.
--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.3.0/build/commands/build/index.ts)_
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/index.js)_
## `eas build:cancel [BUILD_ID]`
Cancel a build.
```
USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/cancel.js)_
## `eas build:configure`
@@ -117,10 +169,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.3.0/build/commands/build/configure.ts)_
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/configure.js)_
## `eas build:list`
@@ -131,34 +182,69 @@ 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)
--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.3.0/build/commands/build/list.ts)_
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/build/list.js)_
## `eas build:view [BUILDID]`
## `eas build:view [BUILD_ID]`
view a build for your project
```
USAGE
$ eas build:view [BUILDID]
$ 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.3.0/build/commands/build/view.ts)_
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/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.34.1/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.3.0/build/commands/credentials.ts)_
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/credentials.js)_
## `eas device:create`
@@ -169,7 +255,7 @@ USAGE
$ eas device:create
```
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.3.0/build/commands/device/create.ts)_
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/create.js)_
## `eas device:list`
@@ -183,7 +269,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.3.0/build/commands/device/list.ts)_
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/device/list.js)_
## `eas device:view [UDID]`
@@ -194,7 +280,18 @@ USAGE
$ eas device:view [UDID]
```
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.3.0/build/commands/device/view.ts)_
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/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.34.1/packages/eas-cli/src/commands/diagnostics.js)_
## `eas help [COMMAND]`
@@ -211,84 +308,190 @@ 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 submit --platform=(android|ios)`
## `eas project:info`
Submits build artifact to app store
information about the current project
```
USAGE
$ eas submit --platform=(android|ios)
$ eas project:info
```
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.34.1/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.34.1/packages/eas-cli/src/commands/project/init.js)_
## `eas secret:create`
Create an environment secret on the current project or owner account.
```
USAGE
$ eas secret:create
OPTIONS
-p, --platform=(android|ios) For which platform you want to submit a build
--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
```
Android specific options=android-package Android package name (default: expo.android.package
from app config)
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/create.js)_
iOS specific options=app-name The name of your app as it will appear on the App
Store (default: expo.name from app config)
## `eas secret:delete`
iOS specific options=apple-id Your Apple ID username (you can also set
EXPO_APPLE_ID env variable)
Delete an environment secret by ID.
iOS specific options=apple-team-id Your Apple Developer Team ID
```
USAGE
$ eas secret:delete
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.
OPTIONS
--id=id ID of the secret to delete
iOS specific options=bundle-identifier Your iOS Bundle Identifier (default:
expo.ios.bundleIdentifier from app config)
DESCRIPTION
Unsure where to find the secret's ID? Run eas secret:list
```
iOS specific options=company-name The name of your company, needed only for the first
upload of any app to App Store
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/delete.js)_
--id=id ID of the build to submit
## `eas secret:list`
Android specific options=key Path to the JSON key used to authenticate with
Google Play
Lists environment secrets available for your current app
iOS specific options=language [default: en-US] Primary language (e.g. English,
German, ...)
```
USAGE
$ eas secret:list
```
--latest Submit the latest build for specified platform
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/list.js)_
--path=path Path to the .apk/.aab file
## `eas submit`
Android specific options=(completed|draft|halted|inProgress) [default: completed] Release status (used when
uploading new APKs/AABs)
submit build archive to app store
iOS specific options=sku An unique ID for your app that is not visible on
the App Store, will be generated unless provided
```
USAGE
$ eas submit
Android specific options=(production|beta|alpha|internal|rollout) [default: internal] The track of the application to
use
OPTIONS
-p, --platform=(android|ios|all)
--id=id ID of the build to submit
--latest Submit the latest build for specified platform
--non-interactive Run command in non-interactive mode
--path=path Path to the .apk/.aab/.ipa file
Android specific options=(apk|aab) Android archive type
--profile=profile Name of the submit profile from eas.json. Defaults to "production" if defined in
eas.json.
--url=url App archive url
--url=url App archive url
--verbose Always print logs from Submission Service
--verbose Always print logs from Submission Service
--[no-]wait Wait for submission to complete
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.3.0/build/commands/submit.ts)_
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/submit.js)_
## `eas webhook:create`
Create a webhook on the current project.
```
USAGE
$ eas webhook:create
OPTIONS
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
--secret=secret Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature'
header.
--url=url Webhook URL
```
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/create.js)_
## `eas webhook:delete [ID]`
Delete a webhook on the current project.
```
USAGE
$ eas webhook:delete [ID]
ARGUMENTS
ID ID of the webhook to delete
```
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/delete.js)_
## `eas webhook:list`
List webhooks on the current project.
```
USAGE
$ eas webhook:list
OPTIONS
--event=(BUILD) Event type that triggers the webhook
```
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/list.js)_
## `eas webhook:update`
Create a webhook on the current project.
```
USAGE
$ eas webhook:update
OPTIONS
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
--id=id (required) Webhook ID
--secret=secret Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature'
header.
--url=url Webhook URL
```
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/update.js)_
## `eas webhook:view ID`
View a webhook on the current project.
```
USAGE
$ eas webhook:view ID
ARGUMENTS
ID ID of the webhook to view
```
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/webhook/view.js)_
<!-- commandsstop -->
@@ -1,3 +0,0 @@
const getConfig = jest.fn(() => ({ exp: {} }));
export { getConfig };
+12
View File
@@ -1,2 +1,14 @@
import { fs } from 'memfs';
// needed because of a weird bug with tempy (dependency of @expo/config-plugins)
// eslint-disable-next-line node/no-sync
fs.mkdirSync('/tmp');
if (process.env.TMPDIR) {
// eslint-disable-next-line node/no-sync
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';
};
+2
View File
@@ -3,6 +3,8 @@ schema: 'https://staging.exp.host/--/graphql'
documents:
- 'src/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/commands/**/*.ts'
generates:
src/graphql/generated.ts:
plugins:
+10641 -3204
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();
});
+99 -85
View File
@@ -1,103 +1,105 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.3.0",
"author": "Expo <support@expo.io>",
"version": "0.34.1",
"author": "Expo <support@expo.dev>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.17",
"@expo/config": "~3.3.19",
"@expo/config-plugins": "^1.0.14",
"@expo/eas-build-job": "^0.2.0",
"@expo/eas-json": "^0.3.0",
"@expo/json-file": "^8.2.24",
"@expo/plist": "^0.0.11",
"@expo/plugin-warn-if-update-available": "^1.7.0",
"@expo/results": "^1.0.0",
"@expo/spawn-async": "^1.5.0",
"@hapi/joi": "^17.1.1",
"@oclif/command": "^1",
"@oclif/config": "^1",
"@oclif/errors": "^1.3.3",
"@oclif/plugin-help": "^3",
"@urql/core": "^1.13.0",
"chalk": "^4.1.0",
"cli-table3": "^0.6.0",
"dateformat": "^3.0.3",
"env-paths": "^2.2.0",
"fast-glob": "^3.2.4",
"figures": "^3.2.0",
"form-data": "^3.0.0",
"fs-extra": "^9.0.1",
"getenv": "^1.0.0",
"got": "^11.5.2",
"graphql": "^15.3.0",
"graphql-tag": "^2.11.0",
"indent-string": "^4.0.0",
"keychain": "^1.3.0",
"lodash": "^4.17.20",
"md5-file": "^5.0.0",
"mime": "^2.4.7",
"minimatch": "^3.0.4",
"node-fetch": "^2.6.1",
"node-forge": "^0.10.0",
"nullthrows": "^1.1.1",
"ora": "^5.1.0",
"pkg-dir": "^4.2.0",
"progress": "^2.0.3",
"prompts": "^2.3.2",
"qrcode-terminal": "^0.12.0",
"strip-ansi": "^6.0.0",
"tar": "^6.0.5",
"tempy": "^0.7.0",
"terminal-link": "^2.1.1",
"timeago.js": "^4.0.2",
"tslib": "^1",
"turndown": "^7.0.0",
"untildify": "^4.0.0",
"uuid": "^8.3.0",
"wrap-ansi": "^7.0.0"
"@expo/apple-utils": "0.0.0-alpha.26",
"@expo/config": "6.0.3",
"@expo/config-plugins": "4.0.3",
"@expo/eas-build-job": "0.2.57",
"@expo/eas-json": "^0.34.1",
"@expo/json-file": "8.2.33",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.14",
"@expo/plugin-warn-if-update-available": "1.7.0",
"@expo/prebuild-config": "3.0.3",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.0",
"@expo/sdk-runtime-versions": "1.0.0",
"@expo/spawn-async": "1.5.0",
"@expo/timeago.js": "1.0.0",
"@oclif/command": "1.8.0",
"@oclif/config": "1.17.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": "4.5.1",
"env-paths": "2.2.0",
"envinfo": "7.8.1",
"fast-glob": "3.2.7",
"figures": "3.2.0",
"form-data": "3.0.0",
"fs-extra": "10.0.0",
"getenv": "1.0.0",
"got": "11.8.2",
"gradle-to-js": "2.0.0",
"graphql": "15.5.1",
"graphql-tag": "2.12.5",
"ignore": "5.1.8",
"indent-string": "4.0.0",
"jks-js": "1.0.1",
"joi": "17.4.2",
"keychain": "1.3.0",
"log-symbols": "4.1.0",
"mime": "2.5.2",
"minimatch": "3.0.4",
"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",
"prompts": "2.3.2",
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
"semver": "7.3.5",
"slash": "3.0.0",
"strip-ansi": "6.0.0",
"tar": "6.1.11",
"terminal-link": "2.1.1",
"tslib": "2.3.1",
"turndown": "7.1.1",
"untildify": "4.0.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/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/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"
"tempy": "0.7.0",
"ts-deepmerge": "1.0.8"
},
"engines": {
"node": ">=8.0.0"
"node": ">=12.0.0"
},
"files": [
"/bin",
@@ -111,7 +113,6 @@
"cli"
],
"license": "MIT",
"main": "build/index.js",
"oclif": {
"bin": "eas",
"commands": "./build/commands",
@@ -119,17 +120,28 @@
"@oclif/plugin-help",
"@expo/plugin-warn-if-update-available"
],
"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"
},
@@ -158,11 +170,13 @@
"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",
"build": "tsc",
"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",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml"
},
"types": "build/index.d.ts"
"version": "yarn oclif-dev readme && node patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
"clean": "rm -rf dist build node_modules yarn-error.log"
}
}
+12
View File
@@ -0,0 +1,12 @@
const { promises: fs } = require('fs');
(async () => {
// Patch `oclif-dev readme` path and link generation
let readmeContent = await fs.readFile('README.md', 'utf8');
readmeContent = readmeContent.replace(/\[build\//g, '[src/');
readmeContent = readmeContent.replace(/build\/commands/g, 'packages/eas-cli/src/commands');
await fs.writeFile('README.md', readmeContent);
// eslint-disable-next-line no-console
console.log('Patched README path generation');
})();
@@ -1,7 +1,9 @@
export default jest.fn().mockReturnValue({
export const ora = jest.fn().mockReturnValue({
start: jest.fn().mockReturnValue({
fail: jest.fn(),
succeed: jest.fn(),
}),
stop: jest.fn(),
stopAndPersist: jest.fn(),
succeed: jest.fn(),
});
+4 -4
View File
@@ -21,10 +21,10 @@ describe('apiClient', () => {
],
});
let error: Error;
let error: Error | null = null;
try {
await apiClient.post('test');
} catch (e) {
} catch (e: any) {
error = e;
}
@@ -41,10 +41,10 @@ describe('apiClient', () => {
it('does not convert non-APIv2 error to ApiV2Error', async () => {
nock(getExpoApiBaseUrl()).post('/--/api/v2/test').reply(500, 'Something went wrong');
let error: Error;
let error: Error | null = null;
try {
await apiClient.post('test');
} catch (e) {
} catch (e: any) {
error = e;
}
expect(error).toBeInstanceOf(RequestError);
@@ -25,7 +25,7 @@ interface AppJSON {
};
}
function createTestProject(
export function createTestProject(
user: Actor,
appJSONExtraData?: Record<string, object | string>
): MockProject {
@@ -58,5 +58,3 @@ function createTestProject(
},
};
}
export { createTestProject };
+129
View File
@@ -0,0 +1,129 @@
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';
import { easCliVersion } from './utils/easCli';
const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
darwin: 'Mac',
win32: 'Windows',
linux: 'Linux',
};
let rudderstackClient: RudderAnalytics | null = null;
let userIdentified = false;
let identifyData: {
userId: string;
deviceId: string;
traits: Record<string, any>;
} | null = null;
export async function initAsync(): Promise<void> {
// TODO: remove after some time
const amplitudeEnabled = await UserSettings.getAsync('amplitudeEnabled', null);
if (amplitudeEnabled !== null) {
await UserSettings.setAsync('analyticsEnabled', amplitudeEnabled);
await UserSettings.deleteKeyAsync('amplitudeEnabled');
}
const amplitudeDeviceId = await UserSettings.getAsync('amplitudeDeviceId', null);
if (amplitudeDeviceId !== null) {
await UserSettings.setAsync('analyticsDeviceId', amplitudeDeviceId);
await UserSettings.deleteKeyAsync('amplitudeDeviceId');
}
// TODO: cut here
if (process.env.DISABLE_EAS_ANALYTICS) {
await UserSettings.setAsync('analyticsEnabled', false);
}
const analyticsEnabled = await UserSettings.getAsync('analyticsEnabled', true);
if (analyticsEnabled) {
const config =
process.env.EXPO_STAGING || process.env.EXPO_LOCAL
? {
// staging environment
rudderstackWriteKey: '1wpX20Da4ltFGSXbPFYUL00Chb7',
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
}
: {
// prod environment
rudderstackWriteKey: '1wpXLFxmujq86etH6G6cc90hPcC',
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
};
rudderstackClient = new RudderAnalytics(
config.rudderstackWriteKey,
new URL('/v1/batch', config.rudderstackDataPlaneURL).toString(),
{
flushInterval: 300,
}
);
}
}
export async function setUserDataAsync(userId: string, traits: Record<string, any>): Promise<void> {
const savedDeviceId = await UserSettings.getAsync('analyticsDeviceId', null);
const deviceId = savedDeviceId ?? uuidv4();
if (!savedDeviceId) {
await UserSettings.setAsync('analyticsDeviceId', deviceId);
}
identifyData = {
userId,
deviceId,
traits,
};
ensureUserIdentified();
}
export async function flushAsync(): Promise<void> {
if (rudderstackClient) {
await rudderstackClient.flush();
}
}
export function logEvent(name: string, properties: Record<string, any> = {}): void {
if (!rudderstackClient) {
return;
}
ensureUserIdentified();
const { userId, deviceId } = identifyData ?? {};
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
rudderstackClient.track({
event: name,
properties: { ...properties, ...commonEventProperties },
...identity,
context: getRudderStackContext(),
});
}
function ensureUserIdentified(): void {
if (!rudderstackClient || userIdentified || !identifyData) {
return;
}
rudderstackClient.identify({
userId: identifyData.userId,
anonymousId: identifyData.deviceId,
traits: identifyData.traits,
});
userIdentified = true;
}
function getRudderStackContext(): Record<string, any> {
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: easCliVersion ?? 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,294 +0,0 @@
import { Workflow } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import prompts from 'prompts';
import { asMock } from '../../__tests__/utils';
import { CredentialsProvider } from '../../credentials/CredentialsProvider';
import { ensureCredentialsAsync } from '../credentials';
jest.mock('prompts');
function createMockCredentialsProvider({
hasRemote,
hasLocal,
isLocalSynced,
}: any): CredentialsProvider {
return {
platform: 'android',
hasRemoteAsync: jest.fn().mockImplementation(() => hasRemote || false),
hasLocalAsync: jest.fn().mockImplementation(() => hasLocal || false),
isLocalSyncedAsync: jest.fn().mockImplementation(() => isLocalSynced || false),
} as CredentialsProvider;
}
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
beforeEach(() => {
asMock(prompts).mockReset();
asMock(prompts).mockImplementation(() => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
});
describe(ensureCredentialsAsync, () => {
describe('for generic builds', () => {
it("should use credentials.json if it's the only available credentials", async () => {
const provider = createMockCredentialsProvider({
hasRemote: false,
hasLocal: true,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Generic,
CredentialsSource.AUTO,
false
);
expect(src).toBe('local');
});
it("should use credentials from server if it's the only available credentials", async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: false,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Generic,
CredentialsSource.AUTO,
false
);
expect(src).toBe('remote');
});
it('should use credentials.json if credentials are the same in credentials.json and on server', async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: true,
isLocalSynced: true,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Generic,
CredentialsSource.AUTO,
false
);
expect(prompts).toHaveBeenCalledTimes(0);
expect(src).toBe('local');
});
it('should ask which credentials to use when local and remote crednitals have different values', async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: true,
isLocalSynced: false,
});
asMock(prompts).mockImplementationOnce(() => {
return { select: 'local' };
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Generic,
CredentialsSource.AUTO,
false
);
expect(prompts).toHaveBeenCalledTimes(1);
expect(src).toBe('local');
});
it('should ask when local and remote are not the same (select remote)', async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: true,
isLocalSynced: false,
});
asMock(prompts).mockImplementationOnce(() => {
return { select: 'remote' };
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Generic,
CredentialsSource.AUTO,
false
);
expect(prompts).toHaveBeenCalledTimes(1);
expect(src).toBe('remote');
});
it('should should throw an error when local and remote are not the same (in non interactive mode)', async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: true,
isLocalSynced: false,
});
try {
await ensureCredentialsAsync(provider, Workflow.Generic, CredentialsSource.AUTO, true);
throw new Error('ensureCredentialsAsync should throw an Error');
} catch (e) {
expect(e.message).toMatch(
'Contents of your local credentials.json for Android are not the same as credentials on EAS servers'
);
}
expect(prompts).toHaveBeenCalledTimes(0);
});
it('should ask when no credentials are present and return remote if confirm=true', async () => {
const provider = createMockCredentialsProvider({
hasRemote: false,
hasLocal: false,
isLocalSynced: false,
});
asMock(prompts).mockImplementationOnce(() => {
return { value: true }; //confirm
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Generic,
CredentialsSource.AUTO,
false
);
expect(prompts).toHaveBeenCalledTimes(1);
expect(src).toBe('remote');
});
it('should ask when no credentials are present and abort if confimr=false', async () => {
const provider = createMockCredentialsProvider({
hasRemote: false,
hasLocal: false,
isLocalSynced: false,
});
asMock(prompts).mockImplementationOnce(() => {
return { value: false }; //confirm
});
try {
await ensureCredentialsAsync(provider, Workflow.Generic, CredentialsSource.AUTO, false);
throw new Error('ensureCredentialsAsync should throw an Error');
} catch (e) {
expect(e.message).toMatch(
'Aborting build process, credentials are not configured for Android'
);
}
expect(prompts).toHaveBeenCalledTimes(1);
});
it('should throw an error when no credentials are present in non interactive mode', async () => {
const provider = createMockCredentialsProvider({
hasRemote: false,
hasLocal: false,
isLocalSynced: false,
});
asMock(prompts).mockImplementation(() => {
return { value: true }; // confirm
});
try {
await ensureCredentialsAsync(provider, Workflow.Generic, CredentialsSource.AUTO, true);
throw new Error('ensureCredentialsAsync should throw an Error');
} catch (e) {
expect(e.message).toMatch('Credentials for this app are not configured');
}
expect(prompts).toHaveBeenCalledTimes(0);
});
});
describe('for managed builds', () => {
it("should use credentials.json if it's the only available credentials", async () => {
const provider = createMockCredentialsProvider({
hasRemote: false,
hasLocal: true,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Managed,
CredentialsSource.AUTO,
false
);
expect(src).toBe('local');
});
it("should use credentials from server if it's the only available credentials", async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: false,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Managed,
CredentialsSource.AUTO,
false
);
expect(src).toBe('remote');
});
it('should use credentials.json if credentials are the same in credentials.json and on server', async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: true,
isLocalSynced: true,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Managed,
CredentialsSource.AUTO,
false
);
expect(prompts).toHaveBeenCalledTimes(0);
expect(src).toBe('local');
});
it('should use credentials.json even if credentials on server are different', async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: true,
isLocalSynced: false,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Managed,
CredentialsSource.AUTO,
false
);
expect(prompts).toHaveBeenCalledTimes(0);
expect(src).toBe('local');
});
});
describe('generic builds with credentialsSource set to local', () => {
it('should use credentials.json without asking even if credentials on server are different', async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: true,
isLocalSynced: false,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Generic,
CredentialsSource.LOCAL,
false
);
expect(prompts).toHaveBeenCalledTimes(0);
expect(src).toBe('local');
});
});
describe('generic builds with credentialsSource set to remote', () => {
it('should use credentials from server even if credentials.json exists and have different values', async () => {
const provider = createMockCredentialsProvider({
hasRemote: true,
hasLocal: true,
isLocalSynced: false,
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Generic,
CredentialsSource.REMOTE,
false
);
expect(prompts).toHaveBeenCalledTimes(0);
expect(src).toBe('remote');
});
});
});
@@ -1,8 +1,7 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import fs from 'fs-extra';
import { AndroidConfig, AndroidManifest } from '@expo/config-plugins';
import log from '../../log';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { ensureValidVersions } from '../utils/updates';
@@ -10,28 +9,12 @@ import { ensureValidVersions } from '../utils/updates';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
const buildGradleContent = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(buildGradleContent, projectDir, exp)) {
const gradleScriptApply = AndroidConfig.Updates.formatApplyLineForBuildGradle(projectDir, exp);
await fs.writeFile(
buildGradlePath,
`${buildGradleContent}\n// Integration with Expo updates\n${gradleScriptApply}\n`
);
}
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 = await AndroidConfig.Updates.setUpdatesConfig(exp, androidManifest, accountName);
const result = AndroidConfig.Updates.setUpdatesConfig(exp, androidManifest, accountName);
await AndroidConfig.Manifest.writeAndroidManifestAsync(androidManifestPath, result);
}
@@ -44,19 +27,16 @@ export async function syncUpdatesConfigurationAsync(
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
try {
await ensureUpdatesConfiguredAsync(projectDir, exp);
await ensureUpdatesConfiguredAsync(projectDir);
} catch (error) {
log.error(
Log.error(
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
);
throw error;
}
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);
@@ -64,30 +44,54 @@ export async function syncUpdatesConfigurationAsync(
}
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, accountName)) {
log.warn(
Log.warn(
'Native project configuration is not synced with values present in your app.json, run "eas build:configure" to make sure all values are applied in the native project'
);
}
}
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
const buildGradleContent = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(buildGradleContent, projectDir, exp)) {
const gradleScriptApply = AndroidConfig.Updates.formatApplyLineForBuildGradle(projectDir, exp);
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
);
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in AndroidManifest.xml
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
const androidManifest = await getAndroidManifestAsync(projectDir);
if (!AndroidConfig.Updates.isMainApplicationMetaDataSet(androidManifest)) {
throw new Error('Missing values in AndroidManifest.xml');
}
}
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
const androidManifest = await getAndroidManifestAsync(projectDir);
return AndroidConfig.Manifest.getMainApplicationMetaDataValue(
androidManifest,
AndroidConfig.Updates.Config.RELEASE_CHANNEL
);
} catch (err) {
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(async () => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.remove(os.tmpdir());
});
beforeEach(async () => {
vol.reset();
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.mkdirp(os.tmpdir());
});
const testChannel = 'test-channel';
describe(readChannelSafelyAsync, () => {
it('retrieves the channel when it is defined natively', async () => {
vol.fromJSON(
{
'./android/app/src/main/AndroidManifest.xml': AndroidManifestWithChannel,
},
'/expo-project'
);
const channel = await readChannelSafelyAsync('/expo-project');
expect(channel).toBe(testChannel);
});
it('returns null if the channel is not defined natively', async () => {
vol.fromJSON(
{
'./android/app/src/main/AndroidManifest.xml': AndroidManifestNoChannel,
},
'/expo-project'
);
const channel = await readChannelSafelyAsync('/expo-project');
expect(channel).toBeNull();
});
});
const AndroidManifestWithChannel = `
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.expo.mycoolapp">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="true"
android:theme="@style/AppTheme">
<meta-data android:name="expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY" android:value='{"expo-channel-name":"${testChannel}"}'/>
<activity
android:name=".MainActivity"
android:launchMode="singleTask"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
</manifest>
`;
const AndroidManifestNoChannel = `
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.expo.mycoolapp">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:launchMode="singleTask"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
</manifest>
`;
@@ -0,0 +1,280 @@
import { ExpoConfig } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import assert from 'assert';
import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import path from 'path';
import { getAppBuildGradleAsync, resolveConfigValue } from '../../../project/android/gradleUtils';
import {
BumpStrategy,
bumpVersionAsync,
bumpVersionInAppJsonAsync,
maybeResolveVersionsAsync,
} from '../version';
const fsReal = jest.requireActual('fs').promises as typeof fs;
jest.mock('fs');
afterAll(async () => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.remove(os.tmpdir());
});
beforeEach(async () => {
vol.reset();
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.mkdirp(os.tmpdir());
});
// bare workflow
describe(bumpVersionAsync, () => {
it('throws an informative error when multiple flavors are detected in the android project', async () => {
await initProjectWithGradleFileAsync(
/*buildGradlePath*/ path.join(
__dirname,
'../../../project/android/__tests__/fixtures/multiflavor-build.gradle'
)
);
await expect(
bumpVersionAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/multiflavor',
exp: {} as any,
})
).rejects.toThrow(
'Automatic version bumping is not supported for multi-flavor Android projects.'
);
});
it('throws an informative error when multiple flavor dimensions are defined in an android project', async () => {
await initProjectWithGradleFileAsync(
/*buildGradlePath*/ path.join(
__dirname,
'../../../project/android/__tests__/fixtures/multiflavor-with-dimensions-build.gradle'
)
);
await expect(
bumpVersionAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/multiflavor',
exp: {} as any,
})
).rejects.toThrow(
'Automatic version bumping is not supported for multi-flavor Android projects.'
);
});
it('bumps expo.android.versionCode and buildGradle versionCode when strategy = BumpStrategy.VERSION_CODE', async () => {
const nativeVersionCode = 100; // this should be overwritten by bumpVersionAsync
const fakeExp = initBareWorkflowProject({ versionCode: nativeVersionCode });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('3.0.0');
expect(fakeExp.android?.versionCode).toBe(124);
expect(appJSON.expo.version).toBe('3.0.0');
expect(appJSON.expo.android.versionCode).toBe(124);
const buildGradle = await getAppBuildGradleAsync('/repo');
assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('124');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
});
it('bumps expo.version and gradle versionCode when strategy = BumpStrategy.SHORT_VERSION', async () => {
const nativeVersionName = '1.0.0'; // this should be overwritten by bumpVersionAsync
const fakeExp = initBareWorkflowProject({ versionName: nativeVersionName });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('3.0.1');
expect(fakeExp.android?.versionCode).toBe(123);
expect(appJSON.expo.version).toBe('3.0.1');
expect(appJSON.expo.android.versionCode).toBe(123);
const buildGradle = await getAppBuildGradleAsync('/repo');
assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.1');
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initBareWorkflowProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('3.0.0');
expect(fakeExp.android?.versionCode).toBe(123);
expect(appJSON.expo.version).toBe('3.0.0');
expect(appJSON.expo.android.versionCode).toBe(123);
const buildGradle = await getAppBuildGradleAsync('/repo');
assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
});
});
// managed workflow
describe(bumpVersionInAppJsonAsync, () => {
it('bumps expo.android.versionCode when strategy = BumpStrategy.VERSION_CODE', async () => {
const fakeExp = initManagedProject();
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('5.0.0');
expect(fakeExp.android?.versionCode).toBe(127);
expect(appJSON.expo.version).toBe('5.0.0');
expect(appJSON.expo.android.versionCode).toBe(127);
});
it('bumps expo.version when strategy = BumpStrategy.SHORT_VERSION', async () => {
const fakeExp = initManagedProject();
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('5.0.1');
expect(fakeExp.android?.versionCode).toBe(126);
expect(appJSON.expo.version).toBe('5.0.1');
expect(appJSON.expo.android.versionCode).toBe(126);
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initManagedProject();
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.NOOP,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('5.0.0');
expect(fakeExp.android?.versionCode).toBe(126);
expect(appJSON.expo.version).toBe('5.0.0');
expect(appJSON.expo.android.versionCode).toBe(126);
});
});
describe(maybeResolveVersionsAsync, () => {
describe('bare project', () => {
it('reads the versions from native code', async () => {
const exp = initBareWorkflowProject();
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/repo',
exp,
{} as BuildProfile<Platform.ANDROID>
);
expect(appVersion).toBe('3.0.0');
expect(appBuildVersion).toBe('123');
});
});
describe('managed project', () => {
it('reads the versions from expo config', async () => {
const exp = initManagedProject();
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/repo',
exp,
{} as BuildProfile<Platform.ANDROID>
);
expect(appVersion).toBe('5.0.0');
expect(appBuildVersion).toBe('126');
});
});
});
function initBareWorkflowProject({
versionCode = 123,
versionName = '3.0.0',
}: {
versionCode?: number;
versionName?: string;
} = {}): ExpoConfig {
const fakeExp: ExpoConfig = {
name: 'myproject',
slug: 'myproject',
version: '3.0.0',
android: {
versionCode: 123,
},
};
vol.fromJSON(
{
'./app.json': JSON.stringify({
expo: fakeExp,
}),
'./android/app/build.gradle': `android {
defaultConfig {
applicationId "com.expo.testapp"
versionCode ${versionCode}
versionName "${versionName}"
}
}`,
'./android/app/src/main/AndroidManifest.xml': 'fake',
},
'/repo'
);
return fakeExp;
}
function initManagedProject(): ExpoConfig {
const fakeExp: ExpoConfig = {
name: 'myproject',
slug: 'myproject',
version: '5.0.0',
android: {
versionCode: 126,
},
};
vol.fromJSON(
{
'./app.json': JSON.stringify({
expo: fakeExp,
}),
},
'/repo'
);
return fakeExp;
}
async function initProjectWithGradleFileAsync(gradleFilePath: string): Promise<void> {
vol.fromJSON(
{
'android/app/build.gradle': await fsReal.readFile(gradleFilePath, 'utf-8'),
'./app.json': JSON.stringify({
expo: {} as any,
}),
},
'/multiflavor'
);
}
@@ -1,187 +0,0 @@
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import assert from 'assert';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import log from '../../log';
import {
ensureAppIdentifierIsDefinedAsync,
getAndroidApplicationIdAsync,
getProjectConfigDescription,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { gitAddAsync } from '../../utils/git';
import { Platform } from '../types';
enum ApplicationIdSource {
AndroidProject,
AppJson,
}
function isApplicationIdValid(applicationId: string): boolean {
return /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(applicationId);
}
export async function ensureApplicationIdIsValidAsync(projectDir: string) {
const applicationId = await ensureAppIdentifierIsDefinedAsync(projectDir, Platform.Android);
if (!isApplicationIdValid(applicationId)) {
const configDescription = getProjectConfigDescription(projectDir);
log.error(
`Invalid format of Android applicationId. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`
);
log.error(`Update "android.package" in ${configDescription} and run this command again.`);
throw new Error('Invalid applicationId');
}
}
export async function configureApplicationIdAsync(
projectDir: string,
exp: ExpoConfig,
allowExperimental: boolean
): Promise<void> {
const configDescription = getProjectConfigDescription(projectDir);
const applicationIdFromConfig = AndroidConfig.Package.getPackage(exp);
const applicationIdFromAndroidProject = await getAndroidApplicationIdAsync(projectDir);
if (applicationIdFromAndroidProject && applicationIdFromConfig) {
if (applicationIdFromConfig !== applicationIdFromAndroidProject) {
log.newLine();
log.warn(
`We detected that your Android project is configured with a different application id than the one defined in ${configDescription}.`
);
const hasApplicationIdInStaticConfig = await hasApplicationIdInStaticConfigAsync(
projectDir,
exp
);
if (!hasApplicationIdInStaticConfig) {
log(`If you choose the one defined in ${configDescription} we'll automatically configure your Android project with it.
However, if you choose the one defined in the Android project you'll have to update ${configDescription} on your own.`);
}
log.newLine();
const { applicationIdSource } = await promptAsync({
type: 'select',
name: 'applicationIdSource',
message: 'Which application id should we use?',
choices: [
{
title: `${chalk.bold(
applicationIdFromAndroidProject
)} - In build.gradle in Android project`,
value: ApplicationIdSource.AndroidProject,
},
{
title: `${chalk.bold(applicationIdFromConfig)} - In your ${configDescription}`,
value: ApplicationIdSource.AppJson,
},
],
});
switch (applicationIdSource) {
case ApplicationIdSource.AppJson: {
await updateApplicationIdInBuildGradleAsync(projectDir, exp);
break;
}
case ApplicationIdSource.AndroidProject: {
if (hasApplicationIdInStaticConfig) {
await updateAppJsonConfigAsync(projectDir, exp, applicationIdFromAndroidProject);
} else {
throw new Error(missingPackageMessage(configDescription));
}
break;
}
}
}
} else if (!applicationIdFromAndroidProject && !applicationIdFromConfig) {
throw new Error(missingPackageMessage(configDescription));
} else if (applicationIdFromAndroidProject && !applicationIdFromConfig) {
if (getConfigFilePaths(projectDir).staticConfigPath) {
await updateAppJsonConfigAsync(projectDir, exp, applicationIdFromAndroidProject);
} else {
throw new Error(missingPackageMessage(configDescription));
}
} else if (!applicationIdFromAndroidProject && applicationIdFromConfig) {
// This should never happen, adding warning just in case
log.warn(
'applicationId is not specified in your ./android/app/build.gradle file. Make sure your project is configured correctly before building.'
);
return;
}
if (allowExperimental) {
// this step is optional, the Play store is using applicationId value from build.gradle
// to identify the app, so package name does not need to be updated
await updatePackageNameAsync(projectDir, exp);
await gitAddAsync(path.join(projectDir, 'android'), { intentToAdd: true });
}
}
function missingPackageMessage(configDescription: string): string {
return `Please define "android.package" in ${configDescription} and run "eas build:configure" again.`;
}
async function updateAppJsonConfigAsync(
projectDir: string,
exp: ExpoConfig,
newApplicationId: string
): Promise<void> {
const paths = getConfigFilePaths(projectDir);
assert(paths.staticConfigPath, "can't update dynamic configs");
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
rawStaticConfig.expo = {
...rawStaticConfig.expo,
android: { ...rawStaticConfig.expo?.android, package: newApplicationId },
};
await fs.writeJson(paths.staticConfigPath, rawStaticConfig, { spaces: 2 });
exp.android = { ...exp.android, package: newApplicationId };
}
/**
* Check if static config exists and if expo.android.package is defined there.
* It will return false if the value in static config is different than "android.package" in ExpoConfig
*/
async function hasApplicationIdInStaticConfigAsync(
projectDir: string,
exp: ExpoConfig
): Promise<boolean> {
if (!exp.android?.package) {
return false;
}
const paths = getConfigFilePaths(projectDir);
if (!paths.staticConfigPath) {
return false;
}
const rawStaticConfig = await fs.readJson(paths.staticConfigPath);
return rawStaticConfig?.expo?.android?.package === exp.android.package;
}
async function updateApplicationIdInBuildGradleAsync(projectDir: string, exp: ExpoConfig) {
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
const updatedBuildGradleContents = AndroidConfig.Package.setPackageInBuildGradle(
exp,
buildGradleContents
);
await fs.writeFile(buildGradlePath, updatedBuildGradleContents);
}
async function updatePackageNameAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
await AndroidConfig.Package.renamePackageOnDisk(exp, projectDir);
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
if (!androidManifestPath) {
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
}
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
androidManifestPath
);
const updatedAndroidManifest = AndroidConfig.Package.setPackageInAndroidManifest(
exp,
androidManifest
);
await AndroidConfig.Manifest.writeAndroidManifestAsync(
androidManifestPath,
updatedAndroidManifest
);
}
+88 -71
View File
@@ -1,111 +1,128 @@
import { Workflow } from '@expo/eas-build-job';
import { EasConfig } from '@expo/eas-json';
import { Android, Job, 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,
} from '../../credentials/android/AndroidCredentialsProvider';
import { createCredentialsContextAsync } from '../../credentials/context';
import log from '../../log';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import Log from '../../log';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
getApplicationIdAsync,
} from '../../project/android/applicationId';
import { GradleBuildContext, resolveGradleBuildContextAsync } from '../../project/android/gradle';
import { toggleConfirmAsync } from '../../prompts';
import { CredentialsResult, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommandContext, createBuildContext } from '../context';
import { ensureCredentialsAsync } from '../credentials';
import { Platform } from '../types';
import { ensureApplicationIdIsValidAsync } from './applicationId';
import { findAccountByName } from '../../user/Account';
import {
BuildRequestSender,
CredentialsResult,
JobData,
prepareBuildRequestForPlatformAsync,
} from '../build';
import { BuildContext } from '../context';
import { transformMetadata } from '../graphql';
import { logCredentialsSource } from '../utils/credentials';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
export async function prepareAndroidBuildAsync(
commandCtx: CommandContext,
easConfig: EasConfig
): Promise<() => Promise<string>> {
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/)
) {
log.addNewLineIfNone();
log.warn(
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(
buildProfile.gradleCommand
)}) includes string 'bundle'.
This means that it will most likely produce an AAB and you will not be able to install it on your Android devices straight from the Expo website.`
);
log.newLine();
Log.newLine();
const confirmed = await toggleConfirmAsync({ message: 'Would you like to proceed?' });
if (!confirmed) {
log.error('Please update eas.json and come back again.');
Log.error('Please update eas.json and come back again.');
process.exit(1);
}
}
await ensureApplicationIdIsValidAsync(commandCtx.projectDir);
checkNodeEnvVariable(ctx);
await checkGoogleServicesFileAsync(ctx);
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
if (ctx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
}
return await prepareBuildRequestForPlatformAsync({
ctx: buildCtx,
projectConfiguration: {},
ensureCredentialsAsync: ensureAndroidCredentialsAsync,
ctx,
ensureCredentialsAsync: async (ctx: BuildContext<Platform.ANDROID>) => {
return await ensureAndroidCredentialsAsync(ctx, gradleContext);
},
ensureProjectConfiguredAsync: async () => {
if (buildCtx.buildProfile.workflow === Workflow.Generic) {
await validateAndSyncProjectConfigurationAsync(commandCtx.projectDir, commandCtx.exp);
}
await validateAndSyncProjectConfigurationAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile,
});
},
getMetadataContext: () => ({
gradleContext,
}),
prepareJobAsync: async (
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData<AndroidCredentials>
): Promise<Job> => {
return await prepareJobAsync(ctx, jobData);
},
sendBuildRequestAsync: async (
appId: string,
job: Android.Job,
metadata: Metadata
): Promise<BuildResult> => {
const graphqlMetadata = transformMetadata(metadata);
const graphqlJob = transformJob(job);
return await BuildMutation.createAndroidBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
},
prepareJobAsync,
});
}
function shouldProvideCredentials(ctx: BuildContext<Platform.Android>): boolean {
return (
ctx.buildProfile.workflow === Workflow.Managed ||
(ctx.buildProfile.workflow === Workflow.Generic && !ctx.buildProfile.withoutCredentials)
);
function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean {
return !ctx.buildProfile.withoutCredentials;
}
async function ensureAndroidCredentialsAsync(
ctx: BuildContext<Platform.Android>
ctx: BuildContext<Platform.ANDROID>,
gradleContext?: GradleBuildContext
): Promise<CredentialsResult<AndroidCredentials> | undefined> {
if (!shouldProvideCredentials(ctx)) {
return;
}
const provider = new AndroidCredentialsProvider(
await createCredentialsContextAsync(ctx.commandCtx.projectDir, {
nonInteractive: ctx.commandCtx.nonInteractive,
}),
{
app: {
projectName: ctx.commandCtx.projectName,
accountName: ctx.commandCtx.accountName,
},
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
}
);
const credentialsSource = await ensureCredentialsAsync(
provider,
ctx.buildProfile.workflow,
ctx.buildProfile.credentialsSource,
ctx.commandCtx.nonInteractive
const androidApplicationIdentifier = await getApplicationIdAsync(
ctx.projectDir,
ctx.exp,
gradleContext
);
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
app: {
account: nullthrows(
findAccountByName(ctx.user.accounts, ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.projectName,
androidApplicationIdentifier,
},
});
const { credentialsSource } = ctx.buildProfile;
logCredentialsSource(credentialsSource, Platform.ANDROID);
return {
credentials: await provider.getCredentialsAsync(credentialsSource),
source: credentialsSource,
+50 -25
View File
@@ -1,50 +1,75 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { AndroidBuildProfile, AndroidVersionAutoIncrement } from '@expo/eas-json';
import log from '../../log';
import { getAndroidApplicationIdAsync } from '../../project/projectUtils';
import { gitAddAsync } from '../../utils/git';
import Log from '../../log';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject,
} from '../../project/android/applicationId';
import { resolveWorkflowAsync } from '../../project/workflow';
import { getVcsClient } from '../../vcs';
import { ConfigureContext } from '../context';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
import { configureApplicationIdAsync, ensureApplicationIdIsValidAsync } from './applicationId';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void> {
if (!ctx.hasAndroidNativeProject) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
return;
}
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
await configureApplicationIdAsync(ctx.projectDir, ctx.exp, ctx.allowExperimental);
await ensureApplicationIdIsValidAsync(ctx.projectDir);
const easGradlePath = AndroidConfig.EasBuild.getEasBuildGradlePath(ctx.projectDir);
await gitAddAsync(easGradlePath, { intentToAdd: true });
await getVcsClient().trackFileAsync(easGradlePath);
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
}
log.withTick('Android project configured');
Log.withTick('Android project configured');
}
export async function validateAndSyncProjectConfigurationAsync(
projectDir: string,
exp: ExpoConfig
): Promise<void> {
const applicationIdFromAndroidProject = await getAndroidApplicationIdAsync(projectDir);
const packageNameFromConfig = AndroidConfig.Package.getPackage(exp);
const isApplicationIdConfigured =
applicationIdFromAndroidProject && applicationIdFromAndroidProject === packageNameFromConfig;
export async function validateAndSyncProjectConfigurationAsync({
projectDir,
exp,
buildProfile,
}: {
projectDir: string;
exp: ExpoConfig;
buildProfile: AndroidBuildProfile;
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const { autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
const isEasBuildGradleConfigured = await AndroidConfig.EasBuild.isEasBuildGradleConfiguredAsync(
projectDir
);
if (!isEasBuildGradleConfigured || !isApplicationIdConfigured) {
throw new Error(
'Project is not configured. Please run "eas build:configure" to configure the project.'
);
if (workflow === Workflow.GENERIC) {
if (!(await AndroidConfig.EasBuild.isEasBuildGradleConfiguredAsync(projectDir))) {
throw new Error(
'Project is not configured. Please run "eas build:configure" to configure the project.'
);
}
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
} else {
await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
}
}
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
function resolveVersionBumpStrategy(autoIncrement: AndroidVersionAutoIncrement): BumpStrategy {
if (autoIncrement === true) {
return BumpStrategy.VERSION_CODE;
} else if (autoIncrement === false) {
return BumpStrategy.NOOP;
} else if (autoIncrement === 'versionCode') {
return BumpStrategy.VERSION_CODE;
} else {
return BumpStrategy.APP_VERSION;
}
}
@@ -0,0 +1,30 @@
import { Android } from '@expo/eas-build-job';
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
import { transformProjectArchive, transformWorkflow } from '../graphql';
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,
username: job.username,
buildType: job.buildType && transformBuildType(job.buildType),
developmentClient: job.developmentClient,
};
}
function transformBuildType(buildType: Android.BuildType): AndroidBuildType {
if (buildType === Android.BuildType.APK) {
return AndroidBuildType.Apk;
} else {
return AndroidBuildType.AppBundle;
}
}
@@ -1,58 +1,34 @@
import {
Android,
ArchiveSource,
ArchiveSourceType,
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 slash from 'slash';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { readSecretEnvsAsync } from '../../credentials/credentialsJson/read';
import { getProjectAccountNameAsync } from '../../project/projectUtils';
import { gitRootDirectoryAsync } from '../../utils/git';
import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
import { Platform } from '../types';
interface JobData {
archiveBucketKey: string;
projectArchive: ArchiveSource;
credentials?: AndroidCredentials;
}
const cacheDefaults = {
disabled: false,
customPaths: [],
cacheDefaultPaths: true,
};
export async function prepareJobAsync(
ctx: BuildContext<Platform.Android>,
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;
secrets: {
buildCredentials?: {
keystore: Android.Keystore;
};
secretEnvs?: Record<string, string>;
};
}
async function prepareJobCommonAsync(
ctx: BuildContext<Platform.Android>,
jobData: JobData
): Promise<Partial<CommonJobProperties>> {
const secretEnvs = await readSecretEnvsAsync(ctx.commandCtx.projectDir);
const credentials = jobData.credentials;
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildProfile: AndroidBuildProfile = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const { credentials } = jobData;
const buildCredentials = credentials
? {
buildCredentials: {
@@ -66,48 +42,40 @@ async function prepareJobCommonAsync(
}
: {};
return {
platform: Platform.Android,
projectArchive: {
type: ArchiveSourceType.S3,
bucketKey: jobData.archiveBucketKey,
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: buildProfile.image,
node: buildProfile.node,
yarn: buildProfile.yarn,
ndk: buildProfile.ndk,
expoCli: buildProfile.expoCli,
env: buildProfile.env,
},
cache: {
...cacheDefaults,
...buildProfile.cache,
clear: ctx.clearCache,
},
secrets: {
...(secretEnvs ? { secretEnvs } : {}),
...buildCredentials,
},
};
}
async function prepareGenericJobAsync(
ctx: BuildContext<Platform.Android>,
jobData: JobData,
buildProfile: AndroidGenericBuildProfile
): Promise<Partial<Android.GenericJob>> {
const projectRootDirectory = path.relative(await gitRootDirectoryAsync(), process.cwd()) || '.';
return {
...(await prepareJobCommonAsync(ctx, jobData)),
type: Workflow.Generic,
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
developmentClient: buildProfile.developmentClient,
gradleCommand: buildProfile.gradleCommand,
artifactPath: buildProfile.artifactPath,
releaseChannel: buildProfile.releaseChannel,
projectRootDirectory,
buildType,
username,
};
}
async function prepareManagedJobAsync(
ctx: BuildContext<Platform.Android>,
jobData: JobData,
buildProfile: AndroidManagedBuildProfile
): Promise<Partial<Android.ManagedJob>> {
const projectRootDirectory = path.relative(await gitRootDirectoryAsync(), process.cwd()) || '.';
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.projectDir);
return {
...(await prepareJobCommonAsync(ctx, jobData)),
type: Workflow.Managed,
username: accountName,
buildType: buildProfile.buildType,
releaseChannel: buildProfile.releaseChannel,
projectRootDirectory,
};
return sanitizeJob(job);
}
@@ -0,0 +1,153 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import Log from '../../log';
import {
getAppBuildGradleAsync,
parseGradleCommand,
resolveConfigValue,
} from '../../project/android/gradleUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
export enum BumpStrategy {
APP_VERSION,
VERSION_CODE,
NOOP,
}
export async function bumpVersionAsync({
bumpStrategy,
projectDir,
exp,
}: {
projectDir: string;
exp: ExpoConfig;
bumpStrategy: BumpStrategy;
}): Promise<void> {
if (bumpStrategy === BumpStrategy.NOOP) {
return;
}
ensureStaticConfigExists(projectDir);
const buildGradle = await getAppBuildGradleAsync(projectDir);
const isMultiFlavor =
buildGradle.android?.productFlavors || buildGradle.android?.flavorDimensions;
if (isMultiFlavor) {
throw new Error(
'Automatic version bumping is not supported for multi-flavor Android projects.'
);
}
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
Log.log('Updated versions in app.json');
await writeVersionsToBuildGradleAsync({
projectDir,
exp,
});
Log.log('Synchronized versions with build gradle');
}
export async function bumpVersionInAppJsonAsync({
bumpStrategy,
projectDir,
exp,
}: {
bumpStrategy: BumpStrategy;
projectDir: string;
exp: ExpoConfig;
}): Promise<void> {
if (bumpStrategy === BumpStrategy.NOOP) {
return;
}
ensureStaticConfigExists(projectDir);
Log.addNewLineIfNone();
if (bumpStrategy === BumpStrategy.APP_VERSION) {
const appVersion = AndroidConfig.Version.getVersionName(exp) ?? '1.0.0';
await bumpAppVersionAsync({ appVersion, projectDir, exp });
} else {
const versionCode = AndroidConfig.Version.getVersionCode(exp);
const bumpedVersionCode = versionCode + 1;
Log.log(
`Bumping ${chalk.bold('expo.android.versionCode')} from ${chalk.bold(
versionCode
)} to ${chalk.bold(bumpedVersionCode)}`
);
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.android = { ...config.android, versionCode: bumpedVersionCode };
});
}
}
export async function maybeResolveVersionsAsync(
projectDir: string,
exp: ExpoConfig,
buildProfile: BuildProfile<Platform.ANDROID>
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
const buildGradle = await getAppBuildGradleAsync(projectDir);
try {
const parsedGradleCommand = buildProfile.gradleCommand
? parseGradleCommand(buildProfile.gradleCommand, buildGradle)
: undefined;
return {
appVersion:
resolveConfigValue(buildGradle, 'versionName', parsedGradleCommand?.flavor) ?? '1.0.0',
appBuildVersion:
resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1',
};
} catch (err: any) {
return {};
}
} else {
return {
appBuildVersion: String(AndroidConfig.Version.getVersionCode(exp)),
appVersion: exp.version,
};
}
}
async function writeVersionsToBuildGradleAsync({
projectDir,
exp,
}: {
projectDir: string;
exp: ExpoConfig;
}): Promise<string> {
const buildGradle = await readBuildGradleAsync(projectDir);
if (!buildGradle) {
throw new Error('This project is missing a build.gradle file.');
}
let updatedBuildGradle = AndroidConfig.Version.setVersionName(exp, buildGradle);
updatedBuildGradle = AndroidConfig.Version.setVersionCode(exp, updatedBuildGradle);
await writeBuildGradleAsync({ projectDir, buildGradle: updatedBuildGradle });
return updatedBuildGradle;
}
async function readBuildGradleAsync(projectDir: string): Promise<string | undefined> {
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
if (!(await fs.pathExists(buildGradlePath))) {
return undefined;
}
return await fs.readFile(buildGradlePath, 'utf8');
}
async function writeBuildGradleAsync({
projectDir,
buildGradle,
}: {
projectDir: string;
buildGradle: string;
}): Promise<void> {
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
await fs.writeFile(buildGradlePath, buildGradle);
}
+238 -128
View File
@@ -1,122 +1,139 @@
import { Job } 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 { apiClient } from '../api';
import log from '../log';
import { promptAsync } from '../prompts';
import { UploadType, uploadAsync } from '../uploads';
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
import { BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log, { learnMore } from '../log';
import { ora } from '../ora';
import { requestedPlatformDisplayNames } from '../platform';
import { uploadAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
import { platformDisplayNames } from './constants';
import { sleepAsync } from '../utils/promise';
import { getVcsClient } from '../vcs';
import { BuildContext } from './context';
import { collectMetadata } from './metadata';
import { AnalyticsEvent, Platform, TrackingContext } from './types';
import Analytics from './utils/analytics';
import { runLocalBuildAsync } from './local';
import { MetadataContext, collectMetadataAsync } from './metadata';
import { TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
import { printDeprecationWarnings } from './utils/printBuildInfo';
import {
commitPromptAsync,
isGitStatusCleanAsync,
makeProjectTarballAsync,
showDiffAsync,
} from './utils/repository';
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
export interface CredentialsResult<Credentials> {
source: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
credentials: Credentials;
}
interface Builder<TPlatform extends Platform, Credentials, ProjectConfiguration> {
export interface JobData<Credentials> {
credentials?: Credentials;
projectArchive: ArchiveSource;
}
interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
ctx: BuildContext<TPlatform>;
projectConfiguration: ProjectConfiguration;
ensureCredentialsAsync(
ctx: BuildContext<TPlatform>
): Promise<CredentialsResult<Credentials> | undefined>;
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
prepareJobAsync(
ctx: BuildContext<TPlatform>,
jobData: {
archiveBucketKey: string;
credentials?: Credentials;
projectConfiguration?: ProjectConfiguration;
}
): Promise<Job>;
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,
ProjectConfiguration
>(builder: Builder<TPlatform, Credentials, ProjectConfiguration>): Promise<() => Promise<string>> {
TJob extends Job
>(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: AnalyticsEvent.GATHER_CREDENTIALS_SUCCESS,
failureEvent: AnalyticsEvent.GATHER_CREDENTIALS_FAIL,
trackingCtx: builder.ctx.trackingCtx,
successEvent: Event.GATHER_CREDENTIALS_SUCCESS,
failureEvent: Event.GATHER_CREDENTIALS_FAIL,
trackingCtx: ctx.trackingCtx,
}
);
if (!builder.ctx.commandCtx.skipProjectConfiguration) {
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(builder.ctx), {
successEvent: AnalyticsEvent.CONFIGURE_PROJECT_SUCCESS,
failureEvent: AnalyticsEvent.CONFIGURE_PROJECT_FAIL,
trackingCtx: builder.ctx.trackingCtx,
if (!ctx.skipProjectConfiguration) {
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(ctx), {
successEvent: Event.CONFIGURE_PROJECT_SUCCESS,
failureEvent: Event.CONFIGURE_PROJECT_FAIL,
trackingCtx: ctx.trackingCtx,
});
}
if (!(await isGitStatusCleanAsync())) {
log.addNewLineIfNone();
const projectType = builder.ctx.platform === Platform.Android ? 'Android' : 'Xcode';
// Currently we are only updaing runtime version durring build, but if it changes in a future
// this message should also contain more info on that (or be more generic)
await reviewAndCommitChangesAsync(`Update runtime version in the ${projectType} project`, {
nonInteractive: builder.ctx.commandCtx.nonInteractive,
});
if (await getVcsClient().isCommitRequiredAsync()) {
Log.addNewLineIfNone();
await reviewAndCommitChangesAsync(
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
{ nonInteractive: ctx.nonInteractive }
);
}
const archiveBucketKey = await uploadProjectAsync(builder.ctx);
const projectArchive = ctx.local
? ({
type: ArchiveSourceType.PATH,
path: (await makeProjectTarballAsync()).path,
} as const)
: ({
type: ArchiveSourceType.S3,
bucketKey: await uploadProjectAsync(ctx),
} as const);
const metadata = await collectMetadata(builder.ctx, {
credentialsSource: credentialsResult?.source,
});
const job = await builder.prepareJobAsync(builder.ctx, {
archiveBucketKey,
const metadataContext = builder.getMetadataContext();
const metadata = await collectMetadataAsync(ctx, metadataContext);
const job = await builder.prepareJobAsync(ctx, {
projectArchive,
credentials: credentialsResult?.credentials,
projectConfiguration: builder.projectConfiguration,
});
return async () => {
try {
return await withAnalyticsAsync(
async () => {
if (log.isDebug) {
log(`Starting ${platformDisplayNames[job.platform]} build`);
}
const {
data: { buildId, deprecationInfo },
} = await apiClient
.post(`projects/${builder.ctx.commandCtx.projectId}/builds`, {
json: {
job,
metadata,
},
})
.json();
printDeprecationWarnings(deprecationInfo);
return buildId;
},
{
successEvent: AnalyticsEvent.BUILD_REQUEST_SUCCESS,
failureEvent: AnalyticsEvent.BUILD_REQUEST_FAIL,
trackingCtx: builder.ctx.trackingCtx,
if (ctx.local) {
await runLocalBuildAsync(job);
return undefined;
} else {
try {
return await sendBuildRequestAsync(builder, job, metadata);
} 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'
) {
Log.error(
`You have already reached the maximum number of pending ${
requestedPlatformDisplayNames[job.platform]
} builds for your account. Try again later.`
);
throw new Error('Build request failed.');
} else if (error?.graphQLErrors) {
Log.error(
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, please report the issue.'
);
}
);
} catch (error) {
const body = error?.response?.body;
if (body && JSON.parse(body)?.errors?.[0]?.code === 'TURTLE_DEPRECATED_JOB_FORMAT') {
log.error('EAS Build API has changed, please upgrade to the latest eas-cli');
throw error;
}
throw error;
}
};
}
@@ -132,19 +149,25 @@ async function uploadProjectAsync<TPlatform extends Platform>(
projectTarballPath = projectTarball.path;
const { bucketKey } = await uploadAsync(
UploadType.TURTLE_PROJECT_SOURCES,
UploadSessionType.EasBuildProjectSources,
projectTarball.path,
createProgressTracker({
total: projectTarball.size,
message: 'Uploading to EAS Build',
completedMessage: `Uploaded to EAS`,
message: ratio =>
`Uploading to EAS Build (${formatBytes(projectTarball.size * ratio)} / ${formatBytes(
projectTarball.size
)})`,
completedMessage: (duration: string) =>
`Uploaded to EAS ${chalk.dim(duration)} ${learnMore(
'https://expo.fyi/eas-build-archive'
)}`,
})
);
return bucketKey;
},
{
successEvent: AnalyticsEvent.PROJECT_UPLOAD_SUCCESS,
failureEvent: AnalyticsEvent.PROJECT_UPLOAD_FAIL,
successEvent: Event.PROJECT_UPLOAD_SUCCESS,
failureEvent: Event.PROJECT_UPLOAD_FAIL,
trackingCtx: ctx.trackingCtx,
}
);
@@ -155,11 +178,40 @@ async function uploadProjectAsync<TPlatform extends Platform>(
}
}
async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJob extends Job>(
builder: Builder<TPlatform, Credentials, TJob>,
job: TJob,
metadata: Metadata
): Promise<BuildFragment> {
const { ctx } = builder;
return await withAnalyticsAsync(
async () => {
if (Log.isDebug) {
Log.log(`Starting ${requestedPlatformDisplayNames[job.platform]} build`);
}
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
ctx.projectId,
job,
metadata
);
printDeprecationWarnings(deprecationInfo);
return build;
},
{
successEvent: Event.BUILD_REQUEST_SUCCESS,
failureEvent: Event.BUILD_REQUEST_FAIL,
trackingCtx: ctx.trackingCtx,
}
);
}
async function withAnalyticsAsync<Result>(
fn: () => Promise<Result>,
analytics: {
successEvent: AnalyticsEvent;
failureEvent: AnalyticsEvent;
successEvent: Event;
failureEvent: Event;
trackingCtx: TrackingContext;
}
): Promise<Result> {
@@ -167,7 +219,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,
@@ -176,46 +228,104 @@ async function withAnalyticsAsync<Result>(
}
}
enum ShouldCommitChanges {
Yes,
ShowDiffFirst,
Abort,
}
async function reviewAndCommitChangesAsync(
commitMessage: string,
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
): Promise<void> {
if (nonInteractive) {
throw new Error(
'Cannot commit changes when --non-interactive is specified. Run the command in interactive mode to review and commit changes.'
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) {
Log.debug('Failed to fetch the build status', err);
return null;
}
})
);
if (builds.length === 1) {
const [build] = builds;
if (build !== null) {
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 build status: ${build.status} - aborting!`);
}
} else {
if (!spinner.text) {
spinner.text = 'Could not fetch the build status. Check your network connection.';
}
}
} 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 unknown = 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}`),
unknown && chalk.red(`Builds with unknown status: ${unknown}`),
]
.filter(i => i)
.join('\t');
}
}
time = new Date().getTime();
await sleepAsync(intervalSec * 1000);
}
const { selected } = await promptAsync({
type: 'select',
name: 'selected',
message: 'Can we commit these changes to git for you?',
choices: [
{ title: 'Yes', value: ShouldCommitChanges.Yes },
...(askedFirstTime
? [{ title: 'Show the diff and ask me again', value: ShouldCommitChanges.ShowDiffFirst }]
: []),
{
title: 'Abort build process',
value: ShouldCommitChanges.Abort,
},
],
});
if (selected === ShouldCommitChanges.Abort) {
throw new Error(
"Aborting, run the command again once you're ready. Make sure to commit any changes you've made."
);
} else if (selected === ShouldCommitChanges.Yes) {
await commitPromptAsync(commitMessage);
log.withTick('Committed changes.');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await showDiffAsync();
await reviewAndCommitChangesAsync(commitMessage, { nonInteractive, askedFirstTime: false });
}
spinner.warn('Timed out');
throw new Error(
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
);
}
+115 -80
View File
@@ -1,37 +1,65 @@
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 { RequestedPlatform } from '../platform';
import { resolveWorkflowAsync } from '../project/workflow';
import { confirmAsync, promptAsync } from '../prompts';
import { ensureLoggedInAsync } from '../user/actions';
import { gitAddAsync } from '../utils/git';
import { easCliVersion } from '../utils/easCli';
import { getVcsClient } 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',
[RequestedPlatform.iOS]: 'Configure EAS Build for iOS',
[RequestedPlatform.Ios]: 'Configure EAS Build for iOS',
[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 getVcsClient().isCommitRequiredAsync()) {
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 getVcsClient().ensureRepoExistsAsync();
await maybeBailOnRepoStatusAsync();
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
@@ -39,17 +67,18 @@ 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')),
shouldConfigureIos: [RequestedPlatform.All, RequestedPlatform.Ios].includes(options.platform),
hasAndroidNativeProject:
(await resolveWorkflowAsync(options.projectDir, Platform.ANDROID)) === Workflow.GENERIC,
hasIosNativeProject:
(await resolveWorkflowAsync(options.projectDir, Platform.IOS)) === Workflow.GENERIC,
};
log.newLine();
Log.newLine();
await ensureEasJsonExistsAsync(ctx);
if (ctx.shouldConfigureAndroid) {
await configureAndroidAsync(ctx);
@@ -58,71 +87,77 @@ export async function configureAsync(options: {
await configureIosAsync(ctx);
}
if (!(await isGitStatusCleanAsync())) {
log.newLine();
if (await getVcsClient().isCommitRequiredAsync()) {
Log.newLine();
await reviewAndCommitChangesAsync(configureCommitMessage[options.platform]);
} else {
log.newLine();
log.withTick('No changes were necessary, the project is already configured correctly.');
} else if (!(await getVcsClient().hasUncommittedChangesAsync())) {
Log.newLine();
Log.withTick('No changes were necessary, the project is already configured correctly.');
}
}
const EAS_JSON_MANAGED_DEFAULT: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
},
build: {
development: {
developmentClient: true,
distribution: 'internal',
},
preview: {
distribution: 'internal',
},
production: {},
},
submit: {
production: {},
},
};
const EAS_JSON_BARE_DEFAULT: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
},
build: {
development: {
distribution: 'internal',
android: {
gradleCommand: ':app:assembleDebug',
},
ios: {
buildConfiguration: 'Debug',
},
},
preview: {
distribution: 'internal',
},
production: {},
},
submit: {
production: {},
},
};
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;
}
Log.withTick('Validated eas.json');
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_BARE_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 getVcsClient().trackFileAsync(easJsonPath);
Log.withTick('Generated eas.json');
}
enum ShouldCommitChanges {
@@ -132,7 +167,7 @@ enum ShouldCommitChanges {
}
async function reviewAndCommitChangesAsync(
commitMessage: string,
initialCommitMessage: string,
askedFirstTime: boolean = true
): Promise<void> {
const { selected } = await promptAsync({
@@ -152,10 +187,10 @@ async function reviewAndCommitChangesAsync(
});
if (selected === ShouldCommitChanges.Yes) {
await commitPromptAsync(commitMessage);
log.withTick('Committed changes');
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await showDiffAsync();
await reviewAndCommitChangesAsync(commitMessage, false);
await getVcsClient().showDiffAsync();
await reviewAndCommitChangesAsync(initialCommitMessage, false);
}
}
-12
View File
@@ -1,12 +0,0 @@
import { Platform, RequestedPlatform } from './types';
export const platformDisplayNames = {
[RequestedPlatform.iOS]: 'iOS',
[RequestedPlatform.Android]: 'Android',
[RequestedPlatform.All]: 'Android and iOS',
};
export const platformEmojis = {
[Platform.iOS]: '🍎',
[Platform.Android]: '🤖',
};
+129 -88
View File
@@ -1,75 +1,25 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { AndroidBuildProfile, EasConfig, iOSBuildProfile } from '@expo/eas-json';
import { ExpoConfig } from '@expo/config';
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 { CredentialsContext } from '../credentials/context';
import { RequestedPlatform } from '../platform';
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';
export interface CommandContext {
requestedPlatform: RequestedPlatform;
profile: string;
projectDir: string;
projectId: string;
user: Actor;
accountName: string;
projectName: string;
exp: ExpoConfig;
trackingCtx: TrackingContext;
nonInteractive: boolean;
skipCredentialsCheck: boolean;
skipProjectConfiguration: boolean;
waitForBuildEnd: boolean;
}
export async function createCommandContextAsync({
requestedPlatform,
profile,
projectDir,
projectId,
trackingCtx,
nonInteractive = false,
skipCredentialsCheck = false,
skipProjectConfiguration = false,
waitForBuildEnd,
}: {
requestedPlatform: RequestedPlatform;
profile: string;
projectId: string;
projectDir: string;
trackingCtx: TrackingContext;
nonInteractive: boolean;
skipCredentialsCheck: boolean;
skipProjectConfiguration: boolean;
waitForBuildEnd: boolean;
}): Promise<CommandContext> {
const user = await ensureLoggedInAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = getProjectAccountName(exp, user);
const projectName = exp.slug;
return {
requestedPlatform,
profile,
projectDir,
projectId,
user,
accountName,
projectName,
exp,
trackingCtx,
nonInteractive,
skipCredentialsCheck,
skipProjectConfiguration,
waitForBuildEnd,
};
}
import { TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
export interface ConfigureContext {
user: Actor;
projectDir: string;
exp: ExpoConfig;
allowExperimental: boolean;
requestedPlatform: RequestedPlatform;
shouldConfigureAndroid: boolean;
shouldConfigureIos: boolean;
@@ -77,38 +27,129 @@ 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;
credentialsCtx: CredentialsContext;
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`);
}
const builderTrackingCtx = {
...commandCtx.trackingCtx,
platform,
};
return {
commandCtx,
trackingCtx: builderTrackingCtx,
projectDir: string;
skipProjectConfiguration: boolean;
}): Promise<BuildContext<T>> {
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
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 credentialsCtx = new CredentialsContext({
exp,
nonInteractive,
projectDir,
user,
});
const devClientProperties = getDevClientEventProperties({
platform,
projectDir,
buildProfile,
});
const trackingCtx = {
tracking_id: uuidv4(),
platform,
...(accountId && { account_id: accountId }),
account_name: accountName,
project_id: projectId,
project_type: workflow,
...devClientProperties,
};
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
return {
accountName,
buildProfile,
buildProfileName,
clearCache,
credentialsCtx,
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;
}
}
-143
View File
@@ -1,143 +0,0 @@
import { EasJsonReader } from '@expo/eas-json';
import chalk from 'chalk';
import ora from 'ora';
import { apiClient } from '../api';
import log from '../log';
import { sleep } from '../utils/promise';
import { prepareAndroidBuildAsync } from './android/build';
import { CommandContext } from './context';
import { prepareIosBuildAsync } from './ios/build';
import { Build, BuildStatus, 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);
log.newLine();
printLogsUrls(commandCtx.accountName, scheduledBuilds);
log.newLine();
if (commandCtx.waitForBuildEnd) {
const builds = await waitForBuildEndAsync(
commandCtx,
scheduledBuilds.map(i => i.buildId)
);
log.newLine();
printBuildResults(commandCtx.accountName, 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>;
}[] = [];
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 Promise.all(
builds.map(async ({ platform, sendBuildRequestAsync }) => ({
platform,
buildId: await sendBuildRequestAsync(),
}))
);
}
async function waitForBuildEndAsync(
commandCtx: CommandContext,
buildIds: string[],
{ timeoutSec = 1800, intervalSec = 30 } = {}
): Promise<(Build | null)[]> {
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: (Build | null)[] = await Promise.all(
buildIds.map(async buildId => {
try {
const { data } = await apiClient
.get(`projects/${commandCtx.projectId}/builds/${buildId}`)
.json();
return data;
} catch (err) {
return null;
}
})
);
if (builds.length === 1) {
switch (builds[0]?.status) {
case BuildStatus.FINISHED:
spinner.succeed('Build finished');
return builds;
case BuildStatus.IN_QUEUE:
spinner.text = 'Build queued...';
break;
case BuildStatus.IN_PROGRESS:
spinner.text = 'Build in progress...';
break;
case BuildStatus.ERRORED:
spinner.fail('Build failed');
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].includes(build.status) : false
).length === builds.length
) {
spinner.fail('Some of the builds failed');
return builds;
} else {
const inQueue = builds.filter(build => build?.status === BuildStatus.IN_QUEUE).length;
const inProgress = builds.filter(build => build?.status === BuildStatus.IN_PROGRESS).length;
const errored = builds.filter(build => build?.status === BuildStatus.ERRORED).length;
const finished = builds.filter(build => build?.status === BuildStatus.FINISHED).length;
const unknownState = builds.length - inQueue - inProgress - errored - finished;
spinner.text = [
inQueue && `Builds in queue: ${inQueue}`,
inProgress && `Builds in progress: ${inProgress}`,
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...'
);
}
-104
View File
@@ -1,104 +0,0 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import { CredentialsProvider } from '../credentials/CredentialsProvider';
import log from '../log';
import { confirmAsync, promptAsync } from '../prompts';
import { platformDisplayNames } from './constants';
function logCredentials(target: 'local' | 'remote', platform: Platform) {
let message = `Using ${target} ${platformDisplayNames[platform]} credentials`;
if (target === 'local') message += ` ${chalk.dim('(credentials.json)')}`;
if (target === 'remote') message += ` ${chalk.dim('(Expo server)')}`;
log.succeed(message);
}
async function ensureCredentialsAutoAsync(
provider: CredentialsProvider,
workflow: Workflow,
nonInteractive: boolean
): Promise<CredentialsSource.LOCAL | CredentialsSource.REMOTE> {
const platform = platformDisplayNames[provider.platform];
switch (workflow) {
case Workflow.Managed:
if (await provider.hasLocalAsync()) {
return CredentialsSource.LOCAL;
} else {
return CredentialsSource.REMOTE;
}
case Workflow.Generic: {
const hasLocal = await provider.hasLocalAsync();
const hasRemote = await provider.hasRemoteAsync();
if (hasRemote && hasLocal) {
if (!(await provider.isLocalSyncedAsync())) {
if (nonInteractive) {
throw new Error(
`Contents of your local credentials.json for ${platform} are not the same as credentials on EAS servers. To use the desired credentials, set the "builds.${platform}.{profile}.credentialsSource" field in the credentials.json file to one of the following: "local", "remote".`
);
} else {
log(
`Contents of your local credentials.json for ${platform} are not the same as credentials on EAS servers`
);
}
const { select } = await promptAsync({
type: 'select',
name: 'select',
message: 'Which credentials do you want to use for this build?',
choices: [
{ title: 'Local credentials.json', value: CredentialsSource.LOCAL },
{ title: 'Credentials stored on EAS servers.', value: CredentialsSource.REMOTE },
],
});
return select;
} else {
return CredentialsSource.LOCAL;
}
} else if (hasLocal) {
logCredentials('local', provider.platform);
return CredentialsSource.LOCAL;
} else if (hasRemote) {
logCredentials('remote', provider.platform);
return CredentialsSource.REMOTE;
} else {
if (nonInteractive) {
throw new Error(
`Credentials for this app are not configured and there is no entry in credentials.json for ${platform}. Either configure credentials.json, or launch the build without "--non-interactive" flag to get a prompt to generate credentials automatically.`
);
} else if (log.isDebug) {
log.warn(
`Credentials for this app are not configured and there is no entry in credentials.json for ${platform}`
);
}
const confirm = await confirmAsync({
message: `${platform} credentials not found, generate now?`,
});
if (confirm) {
return CredentialsSource.REMOTE;
} else {
throw new Error(`Aborting build process, credentials are not configured for ${platform}`);
}
}
}
}
}
export async function ensureCredentialsAsync(
provider: CredentialsProvider,
workflow: Workflow,
src: CredentialsSource,
nonInteractive: boolean
): Promise<CredentialsSource.LOCAL | CredentialsSource.REMOTE> {
switch (src) {
case CredentialsSource.LOCAL:
logCredentials('local', provider.platform);
return CredentialsSource.LOCAL;
case CredentialsSource.REMOTE:
logCredentials('remote', provider.platform);
return CredentialsSource.REMOTE;
case CredentialsSource.AUTO:
return await ensureCredentialsAutoAsync(provider, workflow, nonInteractive);
}
}
+78
View File
@@ -0,0 +1,78 @@
import { ArchiveSource, ArchiveSourceType, Metadata, Workflow } from '@expo/eas-build-job';
import {
BuildCredentialsSource,
BuildIosEnterpriseProvisioning,
BuildMetadataInput,
BuildWorkflow,
DistributionType,
ProjectArchiveSourceInput,
ProjectArchiveSourceType,
} from '../graphql/generated';
export function transformProjectArchive(archiveSource: ArchiveSource): ProjectArchiveSourceInput {
if (archiveSource.type === ArchiveSourceType.S3) {
return {
type: ProjectArchiveSourceType.S3,
bucketKey: archiveSource.bucketKey,
};
} else if (archiveSource.type === ArchiveSourceType.URL) {
return {
type: ProjectArchiveSourceType.Url,
url: archiveSource.url,
};
} else {
throw new Error(`Unsupported project archive source type: '${archiveSource.type}'`);
}
}
export function transformMetadata(metadata: Metadata): BuildMetadataInput {
return {
...metadata,
credentialsSource:
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
distribution: metadata.distribution && transformDistribution(metadata.distribution),
workflow: metadata.workflow && transformWorkflow(metadata.workflow),
iosEnterpriseProvisioning:
metadata.iosEnterpriseProvisioning &&
transformIosEnterpriseProvisioning(metadata.iosEnterpriseProvisioning),
};
}
function transformCredentialsSource(
credentialsSource: Metadata['credentialsSource']
): BuildCredentialsSource {
if (credentialsSource === 'local') {
return BuildCredentialsSource.Local;
} else {
return BuildCredentialsSource.Remote;
}
}
function transformDistribution(distribution: Metadata['distribution']): DistributionType {
if (distribution === 'internal') {
return DistributionType.Internal;
} else if (distribution === 'simulator') {
return DistributionType.Simulator;
} else {
return DistributionType.Store;
}
}
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;
}
}
+30 -38
View File
@@ -1,28 +1,16 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import plist from '@expo/plist';
import fs from 'fs-extra';
import path from 'path';
import log from '../../log';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { gitAddAsync } from '../../utils/git';
import { getVcsClient } from '../../vcs';
import { ensureValidVersions } from '../utils/updates';
import { readPlistAsync, writePlistAsync } from './plist';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
let xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, exp, xcodeProject)) {
xcodeProject = IOSConfig.Updates.ensureBundleReactNativePhaseContainsConfigurationScript(
projectDir,
exp,
xcodeProject
);
await fs.writeFile(IOSConfig.Paths.getPBXProjectPath(projectDir), xcodeProject.writeSync());
}
let expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
@@ -39,9 +27,9 @@ export async function syncUpdatesConfigurationAsync(
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
try {
await ensureUpdatesConfiguredAsync(projectDir, exp);
await ensureUpdatesConfiguredAsync(projectDir);
} catch (error) {
log.error(
Log.error(
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
);
throw error;
@@ -54,21 +42,15 @@ export async function syncUpdatesConfigurationAsync(
}
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
log.warn(
Log.warn(
'Native project configuration is not synced with values present in your app.json, run "eas build:configure" to make sure all values are applied in the native project'
);
}
}
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
const xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, exp, xcodeProject)) {
const script = 'expo-updates/scripts/create-manifest-ios.sh';
const buildPhase = '"Bundle React Native code and images"';
throw new Error(`Path to ${script} is missing in a ${buildPhase} build phase.`);
}
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in Expo.plist
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
const expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistConfigurationSet(expoPlist)) {
throw new Error('Missing values in Expo.plist');
@@ -77,13 +59,7 @@ async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig)
async function readExpoPlistAsync(projectDir: string): Promise<IOSConfig.ExpoPlist> {
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
let expoPlist = {};
if (await fs.pathExists(expoPlistPath)) {
const expoPlistContent = await fs.readFile(expoPlistPath, 'utf8');
expoPlist = plist.parse(expoPlistContent);
}
return expoPlist;
return (await readPlistAsync(expoPlistPath)) as IOSConfig.ExpoPlist;
}
async function writeExpoPlistAsync(
@@ -91,9 +67,25 @@ async function writeExpoPlistAsync(
expoPlist: IOSConfig.ExpoPlist
): Promise<void> {
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
const expoPlistContent = plist.build(expoPlist);
await writePlistAsync(expoPlistPath, expoPlist);
await getVcsClient().trackFileAsync(expoPlistPath);
}
await fs.mkdirp(path.dirname(expoPlistPath));
await fs.writeFile(expoPlistPath, expoPlistContent);
await gitAddAsync(expoPlistPath, { intentToAdd: true });
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
const expoPlist = await readExpoPlistAsync(projectDir);
return expoPlist[IOSConfig.Updates.Config.RELEASE_CHANNEL] ?? null;
} catch (err) {
return null;
}
}
export async function readChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
const expoPlist: any = await readExpoPlistAsync(projectDir); // TODO-JJ remove any once IOSConfig.ExpoPlist is updated to include `EXUpdatesRequestHeaders : Record<string,string>`
const updatesRequestHeaders = expoPlist['EXUpdatesRequestHeaders'] ?? {}; //TODO-JJ 'EXUpdatesRequestHeaders' IosConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY once https://github.com/expo/expo-cli/pull/3571 is published
return updatesRequestHeaders['expo-channel-name'] ?? null;
} catch (err) {
return null;
}
}
@@ -0,0 +1,88 @@
import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { readChannelSafelyAsync } from '../UpdatesModule';
jest.mock('fs');
afterAll(async () => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.remove(os.tmpdir());
});
beforeEach(async () => {
vol.reset();
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.mkdirp(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>
`;
@@ -0,0 +1,344 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { readPlistAsync } from '../plist';
import {
BumpStrategy,
bumpVersionAsync,
bumpVersionInAppJsonAsync,
evaluateTemplateString,
getInfoPlistPath,
readBuildNumberAsync,
readShortVersionAsync,
} from '../version';
jest.mock('fs');
afterAll(async () => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.remove(os.tmpdir());
});
beforeEach(async () => {
vol.reset();
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.mkdirp(os.tmpdir());
});
describe(evaluateTemplateString, () => {
it('evaluates the template string when value is a number', () => {
expect(evaluateTemplateString('$(BLAH_BLAH)', { BLAH_BLAH: 123 })).toBe('123');
});
it('evaluates the template string when value is a string', () => {
expect(evaluateTemplateString('$(BLAH_BLAH)', { BLAH_BLAH: '123' })).toBe('123');
});
it('evaluates the template string when template is not the only element', () => {
expect(evaluateTemplateString('before$(BLAH_BLAH)after', { BLAH_BLAH: '123' })).toBe(
'before123after'
);
});
it('evaluates the template string when template value is double quoted', () => {
expect(evaluateTemplateString('before$(BLAH_BLAH)after', { BLAH_BLAH: '"123"' })).toBe(
'before123after'
);
});
});
// bare workflow
describe(bumpVersionAsync, () => {
it('bumps expo.ios.buildNumber and CFBundleVersion when strategy = BumpStrategy.BUILD_NUMBER', async () => {
const fakeExp = initBareWorkflowProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
projectDir: '/repo',
exp: fakeExp,
buildSettings: {},
});
const appJSON = await fs.readJSON('/repo/app.json');
const infoPlist = (await readPlistAsync(
'/repo/ios/myproject/Info.plist'
)) as IOSConfig.InfoPlist;
expect(fakeExp.version).toBe('1.0.0');
expect(fakeExp.ios?.buildNumber).toBe('2');
expect(appJSON.expo.version).toBe('1.0.0');
expect(appJSON.expo.ios.buildNumber).toBe('2');
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
expect(infoPlist['CFBundleVersion']).toBe('2');
});
it('bumps expo.ios.buildNumber and CFBundleVersion for non default Info.plist location', async () => {
const fakeExp = initBareWorkflowProject({ infoPlistName: 'Info2.plist' });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
projectDir: '/repo',
exp: fakeExp,
buildSettings: {
INFOPLIST_FILE: '$(SRCROOT)/myproject/Info2.plist',
},
});
const appJSON = await fs.readJSON('/repo/app.json');
const infoPlist = (await readPlistAsync(
'/repo/ios/myproject/Info2.plist'
)) as IOSConfig.InfoPlist;
expect(fakeExp.version).toBe('1.0.0');
expect(fakeExp.ios?.buildNumber).toBe('2');
expect(appJSON.expo.version).toBe('1.0.0');
expect(appJSON.expo.ios.buildNumber).toBe('2');
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
expect(infoPlist['CFBundleVersion']).toBe('2');
});
it('bumps expo.version and CFBundleShortVersionString when strategy = BumpStrategy.SHORT_VERSION', async () => {
const fakeExp = initBareWorkflowProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
projectDir: '/repo',
exp: fakeExp,
buildSettings: {},
});
const appJSON = await fs.readJSON('/repo/app.json');
const infoPlist = (await readPlistAsync(
'/repo/ios/myproject/Info.plist'
)) as IOSConfig.InfoPlist;
expect(fakeExp.version).toBe('1.0.1');
expect(fakeExp.ios?.buildNumber).toBe('1');
expect(appJSON.expo.version).toBe('1.0.1');
expect(appJSON.expo.ios.buildNumber).toBe('1');
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.1');
expect(infoPlist['CFBundleVersion']).toBe('1');
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initBareWorkflowProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
projectDir: '/repo',
exp: fakeExp,
buildSettings: {},
});
const appJSON = await fs.readJSON('/repo/app.json');
const infoPlist = (await readPlistAsync(
'/repo/ios/myproject/Info.plist'
)) as IOSConfig.InfoPlist;
expect(fakeExp.version).toBe('1.0.0');
expect(fakeExp.ios?.buildNumber).toBe('1');
expect(appJSON.expo.version).toBe('1.0.0');
expect(appJSON.expo.ios.buildNumber).toBe('1');
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
expect(infoPlist['CFBundleVersion']).toBe('1');
});
});
// managed workflow
describe(bumpVersionInAppJsonAsync, () => {
it('bumps expo.ios.buildNumber when strategy = BumpStrategy.BUILD_NUMBER', async () => {
const fakeExp = initManagedProject();
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('1.0.0');
expect(fakeExp.ios?.buildNumber).toBe('2');
expect(appJSON.expo.version).toBe('1.0.0');
expect(appJSON.expo.ios.buildNumber).toBe('2');
});
it('bumps expo.version when strategy = BumpStrategy.SHORT_VERSION', async () => {
const fakeExp = initManagedProject();
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('1.0.1');
expect(fakeExp.ios?.buildNumber).toBe('1');
expect(appJSON.expo.version).toBe('1.0.1');
expect(appJSON.expo.ios.buildNumber).toBe('1');
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initManagedProject();
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.NOOP,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('1.0.0');
expect(fakeExp.ios?.buildNumber).toBe('1');
expect(appJSON.expo.version).toBe('1.0.0');
expect(appJSON.expo.ios.buildNumber).toBe('1');
});
});
describe(readBuildNumberAsync, () => {
describe('bare project', () => {
it('reads the build number from native code', async () => {
const exp = initBareWorkflowProject();
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
expect(buildNumber).toBe('1');
});
});
describe('managed project', () => {
it('reads the build number from expo config', async () => {
const exp = initManagedProject();
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
expect(buildNumber).toBe('1');
});
});
});
describe(readShortVersionAsync, () => {
describe('bare project', () => {
it('reads the short version from native code', async () => {
const exp = initBareWorkflowProject();
const appVersion = await readShortVersionAsync('/repo', exp, {});
expect(appVersion).toBe('1.0.0');
});
it('evaluates interpolated build number', async () => {
const exp = initBareWorkflowProject({
appVersion: '$(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 = initBareWorkflowProject();
const appVersion = await readShortVersionAsync('/repo', exp, {});
expect(appVersion).toBe('1.0.0');
});
});
});
describe(getInfoPlistPath, () => {
it('returns default path if INFOPLIST_FILE is not specified', () => {
vol.fromJSON(
{
'./ios/testapp/Info.plist': '',
},
'/repo'
);
const plistPath = getInfoPlistPath('/repo', {});
expect(plistPath).toBe('/repo/ios/testapp/Info.plist');
});
it('returns INFOPLIST_FILE if specified', () => {
vol.fromJSON(
{
'./ios/testapp/Info.plist': '',
},
'/repo'
);
const plistPath = getInfoPlistPath('/repo', { INFOPLIST_FILE: './qwert/NotInfo.plist' });
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
});
it('evaluates SRCROOT in Info.plist', () => {
vol.fromJSON(
{
'./ios/testapp/Info.plist': '',
},
'/repo'
);
const plistPath = getInfoPlistPath('/repo', {
INFOPLIST_FILE: '$(SRCROOT)/qwert/NotInfo.plist',
});
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
});
it('evaluates BuildSettings in Info.plist', () => {
vol.fromJSON(
{
'./ios/testapp/Info.plist': '',
},
'/repo'
);
const plistPath = getInfoPlistPath('/repo', {
INFOPLIST_FILE: '$(SRCROOT)/qwert/$(TARGET_NAME).plist',
TARGET_NAME: 'NotInfo',
});
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
});
});
function initBareWorkflowProject({
appVersion = '1.0.0',
version = '1',
infoPlistName = 'Info.plist',
}: { appVersion?: string; version?: string; infoPlistName?: string } = {}): ExpoConfig {
const fakeExp: ExpoConfig = {
name: 'myproject',
slug: 'myproject',
version: '1.0.0',
ios: {
buildNumber: '1',
},
};
vol.fromJSON(
{
'./app.json': JSON.stringify({
expo: fakeExp,
}),
[`./ios/myproject/${infoPlistName}`]: `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleShortVersionString</key>
<string>${appVersion}</string>
<key>CFBundleVersion</key>
<string>${version}</string>
</dict>
</plist>`,
},
'/repo'
);
return fakeExp;
}
function initManagedProject(): ExpoConfig {
const fakeExp: ExpoConfig = {
name: 'myproject',
slug: 'myproject',
version: '1.0.0',
ios: {
buildNumber: '1',
},
};
vol.fromJSON(
{
'./app.json': JSON.stringify({
expo: fakeExp,
}),
},
'/repo'
);
return fakeExp;
}
+91 -79
View File
@@ -1,100 +1,112 @@
import { IOSConfig } from '@expo/config-plugins';
import { 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 type { XCBuildConfiguration } from 'xcode';
import log from '../../log';
import { promptAsync } from '../../prompts';
import { prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommandContext, createBuildContext } from '../context';
import { Platform } from '../types';
import { ensureBundleIdentifierIsValidAsync } from './bundleIdentifer';
import { IosCredentials, Target } from '../../credentials/ios/types';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
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 { IosMetadataContext } from '../metadata';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { ensureIosCredentialsAsync } from './credentials';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
interface BuildConfiguration {
targetName?: string;
buildConfiguration?: string;
buildSettings: XCBuildConfiguration['buildSettings'];
}
export async function prepareIosBuildAsync(
commandCtx: CommandContext,
easConfig: EasConfig
): Promise<() => Promise<string>> {
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);
}
let iosNativeProjectScheme: string | undefined;
let iosApplicationNativeTarget: string | undefined;
if (buildCtx.buildProfile.workflow === Workflow.Generic) {
iosNativeProjectScheme = buildCtx.buildProfile.scheme ?? (await resolveSchemeAsync(buildCtx));
iosApplicationNativeTarget = await IOSConfig.Target.getApplicationTargetForSchemeAsync(
buildCtx.commandCtx.projectDir,
iosNativeProjectScheme
);
}
checkNodeEnvVariable(ctx);
await checkGoogleServicesFileAsync(ctx);
await ensureBundleIdentifierIsValidAsync(commandCtx.projectDir);
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
projectDir: ctx.projectDir,
nonInteractive: ctx.nonInteractive,
exp: ctx.exp,
},
buildProfile
);
const targets = await resolveTargetsAsync(
{
projectDir: ctx.projectDir,
exp: ctx.exp,
},
xcodeBuildContext
);
const buildConfiguration = resolveBuildConfiguration(ctx, targets);
return await prepareBuildRequestForPlatformAsync({
ctx: buildCtx,
projectConfiguration: {
iosNativeProjectScheme,
iosApplicationNativeTarget,
ctx,
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
return ensureIosCredentialsAsync(ctx, targets);
},
ensureCredentialsAsync: ensureIosCredentialsAsync,
ensureProjectConfiguredAsync: async () => {
if (buildCtx.buildProfile.workflow === Workflow.Generic) {
await validateAndSyncProjectConfigurationAsync(commandCtx.projectDir, commandCtx.exp);
}
await validateAndSyncProjectConfigurationAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile,
buildSettings: buildConfiguration.buildSettings,
});
},
getMetadataContext: (): IosMetadataContext => {
return buildConfiguration;
},
prepareJobAsync: async (
ctx: BuildContext<Platform.IOS>,
jobData: JobData<IosCredentials>
): Promise<Job> => {
return await prepareJobAsync(ctx, { ...jobData, buildScheme: xcodeBuildContext.buildScheme });
},
sendBuildRequestAsync: async (
appId: string,
job: Ios.Job,
metadata: Metadata
): Promise<BuildResult> => {
const graphqlMetadata = transformMetadata(metadata);
const graphqlJob = transformJob(job);
return await BuildMutation.createIosBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
},
prepareJobAsync,
});
}
async function resolveSchemeAsync(ctx: BuildContext<Platform.iOS>): Promise<string> {
const schemes = IOSConfig.Scheme.getSchemesFromXcodeproj(ctx.commandCtx.projectDir);
if (schemes.length === 1) {
return schemes[0];
}
const sortedSchemes = sortBy(schemes);
log.newLine();
log(
`We've found multiple schemes in your Xcode project: ${chalk.bold(sortedSchemes.join(', '))}`
);
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(`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;
function resolveBuildConfiguration(
ctx: BuildContext<Platform.IOS>,
targets: Target[]
): BuildConfiguration {
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,
targetName: applicationTarget.targetName,
buildConfiguration: applicationTarget.buildConfiguration,
};
}
@@ -1,148 +0,0 @@
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import assert from 'assert';
import chalk from 'chalk';
import fs from 'fs-extra';
import log from '../../log';
import {
ensureAppIdentifierIsDefinedAsync,
getProjectConfigDescription,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { Platform } from '../types';
enum BundleIdentiferSource {
XcodeProject,
AppJson,
}
function isBundleIdentifierValid(bundleIdentifier: string): boolean {
return /^[a-zA-Z][a-zA-Z0-9\-.]+$/.test(bundleIdentifier);
}
export async function ensureBundleIdentifierIsValidAsync(projectDir: string) {
const bundleIdentifier = await ensureAppIdentifierIsDefinedAsync(projectDir, Platform.iOS);
if (!isBundleIdentifierValid(bundleIdentifier)) {
const configDescription = getProjectConfigDescription(projectDir);
log.error(
`Invalid format of iOS bundleId. Only alphanumeric characters, '.' and '-' are allowed, and each '.' must be followed by a letter.`
);
log.error(`Update "ios.bundleIdentifier" in ${configDescription} and run this command again.`);
throw new Error('Invalid bundleIdentifier');
}
}
export async function configureBundleIdentifierAsync(
projectDir: string,
exp: ExpoConfig
): Promise<void> {
const configDescription = getProjectConfigDescription(projectDir);
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdenitifer.getBundleIdentifierFromPbxproj(
projectDir
);
const bundleIdentifierFromConfig = IOSConfig.BundleIdenitifer.getBundleIdentifier(exp);
if (bundleIdentifierFromPbxproj && bundleIdentifierFromConfig) {
if (bundleIdentifierFromPbxproj !== bundleIdentifierFromConfig) {
log.addNewLineIfNone();
log.warn(
`We detected that your Xcode project is configured with a different bundle identifier than the one defined in ${configDescription}.`
);
const hasBundleIdentifierInStaticConfig = await hasBundleIdentifierInStaticConfigAsync(
projectDir,
exp
);
if (!hasBundleIdentifierInStaticConfig) {
log(`If you choose the one defined in ${configDescription} we'll automatically configure your Xcode project with it.
However, if you choose the one defined in the Xcode project you'll have to update ${configDescription} on your own.`);
}
log.newLine();
const { bundleIdentifierSource } = await promptAsync({
type: 'select',
name: 'bundleIdentifierSource',
message: 'Which bundle identifier should we use?',
choices: [
{
title: `${chalk.bold(bundleIdentifierFromPbxproj)} - In Xcode project`,
value: BundleIdentiferSource.XcodeProject,
},
{
title: `${chalk.bold(bundleIdentifierFromConfig)} - In your ${configDescription}`,
value: BundleIdentiferSource.AppJson,
},
],
});
switch (bundleIdentifierSource) {
case BundleIdentiferSource.AppJson: {
IOSConfig.BundleIdenitifer.setBundleIdentifierForPbxproj(
projectDir,
bundleIdentifierFromConfig
);
break;
}
case BundleIdentiferSource.XcodeProject: {
if (hasBundleIdentifierInStaticConfig) {
await updateAppJsonConfigAsync(projectDir, exp, bundleIdentifierFromPbxproj);
} else {
throw new Error(missingBundleIdentifierMessage(configDescription));
}
break;
}
}
}
} else if (!bundleIdentifierFromPbxproj && !bundleIdentifierFromConfig) {
throw new Error(missingBundleIdentifierMessage(configDescription));
} else if (bundleIdentifierFromPbxproj && !bundleIdentifierFromConfig) {
if (getConfigFilePaths(projectDir).staticConfigPath) {
await updateAppJsonConfigAsync(projectDir, exp, bundleIdentifierFromPbxproj);
} else {
throw new Error(missingBundleIdentifierMessage(configDescription));
}
} else if (!bundleIdentifierFromPbxproj && bundleIdentifierFromConfig) {
IOSConfig.BundleIdenitifer.setBundleIdentifierForPbxproj(
projectDir,
bundleIdentifierFromConfig
);
}
}
function missingBundleIdentifierMessage(configDescription: string): string {
return `Please define "ios.bundleIdentifier" in ${configDescription} and run "eas build:configure" again.`;
}
async function updateAppJsonConfigAsync(
projectDir: string,
exp: ExpoConfig,
newBundleIdentifier: string
): Promise<void> {
const paths = getConfigFilePaths(projectDir);
assert(paths.staticConfigPath, "can't update dynamic configs");
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
rawStaticConfig.expo = {
...rawStaticConfig.expo,
ios: { ...rawStaticConfig.expo?.ios, bundleIdentifier: newBundleIdentifier },
};
await fs.writeJson(paths.staticConfigPath, rawStaticConfig, { spaces: 2 });
exp.ios = { ...exp.ios, bundleIdentifier: newBundleIdentifier };
}
/**
* Check if static config exists and if ios.bundleIdentifier is defined there.
* It will return false if the value in static config is different than "ios.bundleIdentifier" in ExpoConfig
*/
async function hasBundleIdentifierInStaticConfigAsync(
projectDir: string,
exp: ExpoConfig
): Promise<boolean> {
if (!exp.ios?.bundleIdentifier) {
return false;
}
const paths = getConfigFilePaths(projectDir);
if (!paths.staticConfigPath) {
return false;
}
const rawStaticConfig = await fs.readJson(paths.staticConfigPath);
return rawStaticConfig?.expo?.ios?.bundleIdentifier === exp.ios.bundleIdentifier;
}
+47 -22
View File
@@ -1,41 +1,66 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { IosBuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import log from '../../log';
import Log from '../../log';
import {
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject,
} from '../../project/ios/bundleIdentifier';
import { resolveWorkflowAsync } from '../../project/workflow';
import { ConfigureContext } from '../context';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
import {
configureBundleIdentifierAsync,
ensureBundleIdentifierIsValidAsync,
} from './bundleIdentifer';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
if (!ctx.hasIosNativeProject) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
return;
}
await configureBundleIdentifierAsync(ctx.projectDir, ctx.exp);
await ensureBundleIdentifierIsValidAsync(ctx.projectDir);
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
}
log.withTick('iOS project configured');
Log.withTick('iOS project configured');
}
export async function validateAndSyncProjectConfigurationAsync(
projectDir: string,
exp: ExpoConfig
): Promise<void> {
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdenitifer.getBundleIdentifierFromPbxproj(
projectDir
);
if (!bundleIdentifierFromPbxproj || bundleIdentifierFromPbxproj !== exp.ios?.bundleIdentifier) {
throw new Error(
'Bundle identifier is not configured correctly in your Xcode project. Please run "eas build:configure" to configure it.'
);
export async function validateAndSyncProjectConfigurationAsync({
projectDir,
exp,
buildProfile,
buildSettings,
}: {
projectDir: string;
exp: ExpoConfig;
buildProfile: IosBuildProfile;
buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const { autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, buildSettings });
} else {
await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
}
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
}
function resolveVersionBumpStrategy(autoIncrement: IosVersionAutoIncrement): BumpStrategy {
if (autoIncrement === true) {
return BumpStrategy.BUILD_NUMBER;
} else if (autoIncrement === false) {
return BumpStrategy.NOOP;
} else if (autoIncrement === 'buildNumber') {
return BumpStrategy.BUILD_NUMBER;
} else {
return BumpStrategy.APP_VERSION;
}
}
+21 -54
View File
@@ -1,73 +1,40 @@
import { Workflow } from '@expo/eas-build-job';
import { CredentialsSource, DistributionType } from '@expo/eas-json';
import assert from 'assert';
import { Platform } from '@expo/eas-build-job';
import { createCredentialsContextAsync } from '../../credentials/context';
import IosCredentialsProvider, {
IosCredentials,
} from '../../credentials/ios/IosCredentialsProvider';
import { AppLookupParams } from '../../credentials/ios/credentials';
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
import { resolveEntitlementsJsonAsync } from '../../credentials/ios/appstore/entitlements';
import { IosCredentials, Target } from '../../credentials/ios/types';
import { CredentialsResult } from '../build';
import { BuildContext } from '../context';
import { ensureCredentialsAsync } from '../credentials';
import { Platform } from '../types';
import { logCredentialsSource } from '../utils/credentials';
export async function ensureIosCredentialsAsync(
ctx: BuildContext<Platform.iOS>
buildCtx: BuildContext<Platform.IOS>,
targets: Target[]
): Promise<CredentialsResult<IosCredentials> | undefined> {
if (!shouldProvideCredentials(ctx)) {
if (!shouldProvideCredentials(buildCtx)) {
return;
}
assert(ctx.commandCtx.exp?.ios?.bundleIdentifier, 'ios.bundleIdentifier is required');
return await resolveIosCredentialsAsync(ctx.commandCtx.projectDir, {
app: {
accountName: ctx.commandCtx.accountName,
projectName: ctx.commandCtx.projectName,
bundleIdentifier: ctx.commandCtx.exp.ios.bundleIdentifier,
const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, {
app: getAppFromContext(buildCtx.credentialsCtx),
targets,
iosCapabilitiesOptions: {
entitlements: await resolveEntitlementsJsonAsync(buildCtx.projectDir, buildCtx.workflow),
},
workflow: ctx.buildProfile.workflow,
credentialsSource: ctx.buildProfile.credentialsSource,
distribution: ctx.buildProfile.distribution ?? DistributionType.STORE,
nonInteractive: ctx.commandCtx.nonInteractive,
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
distribution: buildCtx.buildProfile.distribution ?? 'store',
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
});
}
interface ResolveCredentialsParams {
app: AppLookupParams;
workflow: Workflow;
credentialsSource: CredentialsSource;
distribution: DistributionType;
nonInteractive: boolean;
skipCredentialsCheck: boolean;
}
const { credentialsSource } = buildCtx.buildProfile;
export async function resolveIosCredentialsAsync(
projectDir: string,
params: ResolveCredentialsParams
): Promise<CredentialsResult<IosCredentials>> {
const provider = new IosCredentialsProvider(
await createCredentialsContextAsync(projectDir, {
nonInteractive: params.nonInteractive,
}),
{
app: params.app,
distribution: params.distribution,
skipCredentialsCheck: params.skipCredentialsCheck,
}
);
const credentialsSource = await ensureCredentialsAsync(
provider,
params.workflow,
params.credentialsSource,
params.nonInteractive
);
logCredentialsSource(credentialsSource, Platform.IOS);
return {
credentials: await provider.getCredentialsAsync(credentialsSource),
source: credentialsSource,
};
}
function shouldProvideCredentials(ctx: BuildContext<Platform.iOS>): boolean {
return true;
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
return !buildCtx.buildProfile.simulator;
}
+46
View File
@@ -0,0 +1,46 @@
import { Env, Ios } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
import { transformProjectArchive, transformWorkflow } from '../graphql';
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,
secrets: transformIosSecrets(job.secrets),
builderEnvironment: job.builderEnvironment,
cache: job.cache,
scheme: job.scheme,
buildConfiguration: job.buildConfiguration,
artifactPath: job.artifactPath,
username: job.username,
developmentClient: job.developmentClient,
simulator: job.simulator,
};
}
function transformIosSecrets(secrets: {
buildCredentials?: Ios.BuildCredentials;
env?: Env;
}): IosJobSecretsInput {
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
for (const targetName of Object.keys(secrets.buildCredentials ?? {})) {
buildCredentials.push({
targetName,
provisioningProfileBase64: nullthrows(secrets.buildCredentials?.[targetName])
.provisioningProfileBase64,
distributionCertificate: nullthrows(secrets.buildCredentials?.[targetName])
.distributionCertificate,
});
}
return {
buildCredentials,
environmentSecrets: secrets.env,
};
}
+22
View File
@@ -0,0 +1,22 @@
import { IOSConfig } from '@expo/config-plugins';
import plist from '@expo/plist';
import fs from 'fs-extra';
import path from 'path';
export async function readPlistAsync(plistPath: string): Promise<object> {
if (await fs.pathExists(plistPath)) {
const expoPlistContent = await fs.readFile(plistPath, 'utf8');
return plist.parse(expoPlistContent);
} else {
return {};
}
}
export async function writePlistAsync(
plistPath: string,
plistObject: IOSConfig.ExpoPlist | IOSConfig.InfoPlist
): Promise<void> {
const contents = plist.build(plistObject);
await fs.mkdirp(path.dirname(plistPath));
await fs.writeFile(plistPath, contents);
}
+54 -128
View File
@@ -1,155 +1,81 @@
import {
ArchiveSource,
ArchiveSourceType,
Job,
Workflow,
iOS,
sanitizeJob,
} from '@expo/eas-build-job';
import { iOSGenericBuildProfile, iOSManagedBuildProfile } from '@expo/eas-json';
import assert from 'assert';
import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
import path from 'path';
import slash from 'slash';
import {
IosCredentials,
IosTargetCredentials,
isCredentialsMap,
readSecretEnvsAsync,
} from '../../credentials/credentialsJson/read';
import { getProjectAccountNameAsync } from '../../project/projectUtils';
import { gitRootDirectoryAsync } from '../../utils/git';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
import { Platform } from '../types';
interface JobData {
archiveBucketKey: string;
projectArchive: ArchiveSource;
credentials?: IosCredentials;
projectConfiguration: {
iosNativeProjectScheme?: string;
iosApplicationNativeTarget?: string;
};
buildScheme: string;
}
const cacheDefaults = {
disabled: false,
customPaths: [],
cacheDefaultPaths: true,
};
export async function prepareJobAsync(
ctx: BuildContext<Platform.iOS>,
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;
releaseChannel: string;
secrets: {
buildCredentials: iOS.BuildCredentials;
secretEnvs?: Record<string, string>;
};
}
async function prepareJobCommonAsync(
ctx: BuildContext<Platform.iOS>,
{
archiveBucketKey,
credentials,
targetName,
}: { archiveBucketKey: string; credentials?: IosCredentials; targetName?: string }
): Promise<Partial<CommonJobProperties>> {
const secretEnvs = await readSecretEnvsAsync(ctx.commandCtx.projectDir);
let buildCredentials: CommonJobProperties['secrets']['buildCredentials'] = {};
if (credentials && isCredentialsMap(credentials)) {
const targets = Object.keys(credentials);
for (const target of targets) {
buildCredentials[target] = prepareTargetCredentials(credentials[target]);
const projectRootDirectory =
slash(path.relative(await getVcsClient().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(jobData.credentials[targetName]);
}
} else if (credentials) {
// targetName
// - for managed projects: sanitized .name from the app config
// - for generic projects: name of the main application target
assert(targetName, 'target name should be defined');
buildCredentials = {
[targetName]: prepareTargetCredentials(credentials),
};
}
return {
platform: Platform.iOS,
projectArchive: {
type: ArchiveSourceType.S3,
bucketKey: archiveBucketKey,
const job: Ios.Job = {
type: ctx.workflow,
platform: Platform.IOS,
projectArchive: jobData.projectArchive,
projectRootDirectory,
builderEnvironment: {
image: ctx.buildProfile.image,
node: ctx.buildProfile.node,
yarn: ctx.buildProfile.yarn,
bundler: ctx.buildProfile.bundler,
cocoapods: ctx.buildProfile.cocoapods,
fastlane: ctx.buildProfile.fastlane,
expoCli: ctx.buildProfile.expoCli,
env: ctx.buildProfile.env,
},
cache: {
...cacheDefaults,
...ctx.buildProfile.cache,
clear: ctx.clearCache,
},
secrets: {
...(secretEnvs ? { secretEnvs } : {}),
buildCredentials,
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
developmentClient: ctx.buildProfile.developmentClient,
simulator: ctx.buildProfile.simulator,
scheme: jobData.buildScheme,
buildConfiguration: ctx.buildProfile.buildConfiguration,
artifactPath: ctx.buildProfile.artifactPath,
username,
};
return sanitizeJob(job);
}
function prepareTargetCredentials(targetCredentials: IosTargetCredentials): iOS.TargetCredentials {
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
return {
provisioningProfileBase64: targetCredentials.provisioningProfile,
distributionCertificate: {
dataBase64: targetCredentials.distributionCertificate.certP12,
password: targetCredentials.distributionCertificate.certPassword,
dataBase64: targetCredentials.distributionCertificate.certificateP12,
password: targetCredentials.distributionCertificate.certificatePassword,
},
};
}
async function prepareGenericJobAsync(
ctx: BuildContext<Platform.iOS>,
jobData: JobData,
buildProfile: iOSGenericBuildProfile
): Promise<Partial<iOS.GenericJob>> {
const projectRootDirectory = path.relative(await gitRootDirectoryAsync(), process.cwd()) || '.';
return {
...(await prepareJobCommonAsync(ctx, {
archiveBucketKey: jobData.archiveBucketKey,
credentials: jobData.credentials,
targetName: jobData.projectConfiguration.iosApplicationNativeTarget,
})),
type: Workflow.Generic,
scheme: jobData.projectConfiguration.iosNativeProjectScheme,
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(), process.cwd()) || '.';
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.projectDir);
const targetName = sanitizedName(ctx.commandCtx.exp.name);
return {
...(await prepareJobCommonAsync(ctx, {
archiveBucketKey: jobData.archiveBucketKey,
credentials: jobData.credentials,
targetName,
})),
type: Workflow.Managed,
username: accountName,
releaseChannel: buildProfile.releaseChannel,
projectRootDirectory,
};
}
// copy-pasted from expo-cli/packages/xdl/src/Exp.ts
// it's used in eject
function sanitizedName(name: string) {
return name
.replace(/[\W_]+/g, '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '');
}
+209
View File
@@ -0,0 +1,209 @@
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import chalk from 'chalk';
import path from 'path';
import type { XCBuildConfiguration } from 'xcode';
import Log from '../../log';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync } from '../utils/version';
import { readPlistAsync, writePlistAsync } from './plist';
export enum BumpStrategy {
APP_VERSION,
BUILD_NUMBER,
NOOP,
}
export async function bumpVersionAsync({
bumpStrategy,
projectDir,
exp,
buildSettings,
}: {
projectDir: string;
exp: ExpoConfig;
bumpStrategy: BumpStrategy;
buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<void> {
if (bumpStrategy === BumpStrategy.NOOP) {
return;
}
ensureStaticConfigExists(projectDir);
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
Log.log('Updated versions in app.json');
await writeVersionsToInfoPlistAsync({ projectDir, exp, infoPlist, buildSettings });
Log.log('Synchronized versions with Info.plist');
}
export async function bumpVersionInAppJsonAsync({
bumpStrategy,
projectDir,
exp,
}: {
bumpStrategy: BumpStrategy;
projectDir: string;
exp: ExpoConfig;
}): Promise<void> {
if (bumpStrategy === BumpStrategy.NOOP) {
return;
}
ensureStaticConfigExists(projectDir);
Log.addNewLineIfNone();
if (bumpStrategy === BumpStrategy.APP_VERSION) {
const appVersion = IOSConfig.Version.getVersion(exp);
await bumpAppVersionAsync({ appVersion, projectDir, exp });
} else {
const buildNumber = IOSConfig.Version.getBuildNumber(exp);
if (buildNumber.match(/^\d+(\.\d+)*$/)) {
const comps = buildNumber.split('.');
comps[comps.length - 1] = String(Number(comps[comps.length - 1]) + 1);
const bumpedBuildNumber = comps.join('.');
Log.log(
`Bumping ${chalk.bold('expo.ios.buildNumber')} from ${chalk.bold(
buildNumber
)} to ${chalk.bold(bumpedBuildNumber)}`
);
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.ios = { ...config.ios, buildNumber: String(bumpedBuildNumber) };
});
} else {
Log.log(`${chalk.bold('expo.ios.buildNumber')} = ${chalk.bold(buildNumber)} is not a number`);
const { bumpedBuildNumber } = await promptAsync({
type: 'text',
name: 'bumpedBuildNumber',
message: 'What is the next build number?',
});
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.ios = { ...config.ios, buildNumber: String(bumpedBuildNumber) };
});
}
}
}
export async function readShortVersionAsync(
projectDir: string,
exp: ExpoConfig,
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
return (
infoPlist.CFBundleShortVersionString &&
evaluateTemplateString(infoPlist.CFBundleShortVersionString, buildSettings)
);
} else {
return exp.version;
}
}
export async function readBuildNumberAsync(
projectDir: string,
exp: ExpoConfig,
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
return (
infoPlist.CFBundleVersion && evaluateTemplateString(infoPlist.CFBundleVersion, buildSettings)
);
} else {
return IOSConfig.Version.getBuildNumber(exp);
}
}
export async function maybeResolveVersionsAsync(
projectDir: string,
exp: ExpoConfig,
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
try {
return {
appBuildVersion: await readBuildNumberAsync(projectDir, exp, buildSettings),
appVersion: await readShortVersionAsync(projectDir, exp, buildSettings),
};
} catch {
return {};
}
}
async function writeVersionsToInfoPlistAsync({
projectDir,
exp,
infoPlist,
buildSettings,
}: {
projectDir: string;
exp: ExpoConfig;
infoPlist: IOSConfig.InfoPlist;
buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<IOSConfig.InfoPlist> {
let updatedInfoPlist = IOSConfig.Version.setVersion(exp, infoPlist);
updatedInfoPlist = IOSConfig.Version.setBuildNumber(exp, updatedInfoPlist);
await writeInfoPlistAsync({ projectDir, infoPlist: updatedInfoPlist, buildSettings });
return updatedInfoPlist;
}
export function getInfoPlistPath(
projectDir: string,
buildSettings: XCBuildConfiguration['buildSettings']
): string {
if (buildSettings.INFOPLIST_FILE) {
const infoPlistFile = buildSettings.INFOPLIST_FILE.startsWith('"')
? buildSettings.INFOPLIST_FILE.slice(1, -1)
: buildSettings.INFOPLIST_FILE;
const iosDir = path.join(projectDir, 'ios');
const plistPath = evaluateTemplateString(infoPlistFile, {
...buildSettings,
SRCROOT: iosDir,
});
return path.isAbsolute(plistPath) ? plistPath : path.resolve(iosDir, plistPath);
} else {
return IOSConfig.Paths.getInfoPlistPath(projectDir);
}
}
async function readInfoPlistAsync(
projectDir: string,
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<IOSConfig.InfoPlist> {
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
return (await readPlistAsync(infoPlistPath)) as IOSConfig.InfoPlist;
}
async function writeInfoPlistAsync({
projectDir,
infoPlist,
buildSettings,
}: {
projectDir: string;
infoPlist: IOSConfig.InfoPlist;
buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<void> {
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
await writePlistAsync(infoPlistPath, infoPlist);
}
function ensureStaticConfigExists(projectDir: string): void {
const paths = getConfigFilePaths(projectDir);
if (!paths.staticConfigPath) {
throw new Error('autoIncrement option is not supported when using app.config.js');
}
}
export function evaluateTemplateString(s: string, vars: Record<string, any>): string {
return s.replace(/\$\((\w+)\)/g, (match, key) => {
if (vars.hasOwnProperty(key)) {
const value = String(vars[key]);
return value.startsWith('"') ? value.slice(1, -1) : value;
} else {
return match;
}
});
}
+45
View File
@@ -0,0 +1,45 @@
import { Job } from '@expo/eas-build-job';
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import Log from '../log';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
export async function runLocalBuildAsync(job: Job): Promise<void> {
const execNameOrPath =
process.env.EAS_LOCAL_BUILD_PLUGIN_PATH ??
(await findWithNodeResolutionAsync()) ??
PLUGIN_PACKAGE_NAME;
try {
const arg = Buffer.from(JSON.stringify({ job })).toString('base64');
await spawnAsync(execNameOrPath, [arg], {
stdio: 'inherit',
});
} catch (err: any) {
if (err.code === 'ENOENT') {
Log.warn(`Could not resolve executable ${execNameOrPath}.`);
Log.warn(
`Install ${PLUGIN_PACKAGE_NAME} package from npm e.g. ${chalk.bold(
`npm install -g ${PLUGIN_PACKAGE_NAME}`
)} and make sure it's in PATH.`
);
throw err;
}
}
}
async function findWithNodeResolutionAsync(): Promise<string | undefined> {
try {
const resolvedPath = path.resolve(
path.dirname(require.resolve(PLUGIN_PACKAGE_NAME)),
'../bin/run'
);
if (await fs.pathExists(resolvedPath)) {
return resolvedPath;
}
} catch {}
return undefined;
}
+153 -26
View File
@@ -1,34 +1,161 @@
import { CredentialsSource, DistributionType } 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 { getAppIdentifierAsync } from '../project/projectUtils';
import { getApplicationIdAsync } from '../project/android/applicationId';
import { GradleBuildContext } from '../project/android/gradle';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
import { getUsername } from '../project/projectUtils';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs';
import {
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
} from './android/UpdatesModule';
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
import { BuildContext } from './context';
import { BuildMetadata, Platform } from './types';
import {
readChannelSafelyAsync as readIosChannelSafelyAsync,
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
} from './ios/UpdatesModule';
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
import { isExpoUpdatesInstalled } from './utils/updates';
/**
* We use require() to exclude package.json from TypeScript's analysis since it lives outside
* the src directory and would change the directory structure of the emitted files
* under the build directory
*/
const packageJSON = require('../../package.json');
export type MetadataContext<T extends Platform> = T extends Platform.ANDROID
? AndroidMetadataContext
: IosMetadataContext;
export async function collectMetadata<T extends Platform>(
export interface AndroidMetadataContext {
gradleContext?: GradleBuildContext;
}
export interface IosMetadataContext {
buildSettings: XCBuildConfiguration['buildSettings'];
targetName?: string;
buildConfiguration?: string;
}
export async function collectMetadataAsync<T extends Platform>(
ctx: BuildContext<T>,
{
credentialsSource,
}: {
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
}
): Promise<BuildMetadata> {
return {
appVersion: ctx.commandCtx.exp.version!,
cliVersion: packageJSON.version,
workflow: ctx.buildProfile.workflow,
credentialsSource,
sdkVersion: ctx.commandCtx.exp.sdkVersion,
platformContext: MetadataContext<T>
): Promise<Metadata> {
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
const distribution =
('simulator' in ctx.buildProfile && ctx.buildProfile.simulator
? 'simulator'
: ctx.buildProfile.distribution) ?? 'store';
const metadata = {
trackingContext: ctx.trackingCtx,
distribution: ctx.buildProfile.distribution ?? DistributionType.STORE,
appName: ctx.commandCtx.exp.name,
appIdentifier:
(await getAppIdentifierAsync(ctx.commandCtx.projectDir, ctx.platform)) ?? undefined,
...(await maybeResolveVersionsAsync(ctx, platformContext)),
cliVersion: easCliVersion,
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, platformContext),
buildProfile: ctx.buildProfileName,
gitCommitHash: await getVcsClient().getCommitHashAsync(),
isGitWorkingTreeDirty: await getVcsClient().hasUncommittedChangesAsync(),
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
ctx as BuildContext<Platform.IOS>
),
}),
};
return sanitizeMetadata(metadata);
}
async function maybeResolveVersionsAsync<T extends Platform>(
ctx: BuildContext<T>,
platformContext: MetadataContext<T>
): Promise<{ appBuildVersion?: string; appVersion?: string }> {
if (ctx.platform === Platform.IOS) {
const iosContext = platformContext as IosMetadataContext;
return await maybeResolveIosVersionsAsync(
ctx.projectDir,
ctx.exp,
iosContext?.buildSettings ?? {}
);
} else if (ctx.platform === Platform.ANDROID) {
const androidCtx = ctx as BuildContext<Platform.ANDROID>;
return await maybeResolveAndroidVersionsAsync(ctx.projectDir, ctx.exp, androidCtx.buildProfile);
} else {
throw new Error(`Unsupported platform ${ctx.platform}`);
}
}
async function resolveAppIdentifierAsync<T extends Platform>(
ctx: BuildContext<T>,
platformContext: MetadataContext<T>
): Promise<string> {
if (ctx.platform === Platform.IOS) {
const iosContext = platformContext as IosMetadataContext;
return await getBundleIdentifierAsync(ctx.projectDir, ctx.exp, {
targetName: iosContext.targetName,
buildConfiguration: iosContext.buildConfiguration,
});
} else {
const androidContext = platformContext as AndroidMetadataContext;
return await getApplicationIdAsync(ctx.projectDir, ctx.exp, androidContext.gradleContext);
}
}
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> {
switch (ctx.platform) {
case Platform.ANDROID: {
return (await readAndroidChannelSafelyAsync(ctx.projectDir)) ?? undefined;
}
case Platform.IOS: {
return (await readIosChannelSafelyAsync(ctx.projectDir)) ?? undefined;
}
}
return undefined;
}
function resolveIosEnterpriseProvisioning(
ctx: BuildContext<Platform.IOS>
): IosEnterpriseProvisioning | undefined {
return ctx.buildProfile.enterpriseProvisioning;
}
+7 -115
View File
@@ -1,124 +1,16 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import {
AndroidBuildProfile,
CredentialsSource,
DistributionType,
iOSBuildProfile,
} from '@expo/eas-json';
export enum RequestedPlatform {
Android = 'android',
iOS = 'ios',
All = 'all',
}
export { Platform };
export enum BuildStatus {
NEW = 'new',
IN_QUEUE = 'in-queue',
IN_PROGRESS = 'in-progress',
ERRORED = 'errored',
FINISHED = 'finished',
CANCELED = 'canceled',
}
export type TrackingContext = Record<string, string | number>;
export interface Build {
id: string;
status: BuildStatus;
platform: Platform;
createdAt: string;
updatedAt: string;
artifacts?: BuildArtifacts;
metadata?: Partial<BuildMetadata>;
export enum BuildDistributionType {
STORE = 'store',
INTERNAL = 'internal',
SIMULATOR = 'simulator',
}
interface BuildArtifacts {
buildUrl?: string;
logsUrl: string;
}
export type BuildMetadata = {
/**
* Application version (the expo.version key in app.json/app.config.js)
*/
appVersion: string;
/**
* EAS CLI version
*/
cliVersion: string;
/**
* Build workflow
* It's either 'generic' or 'managed'
*/
workflow: Workflow;
/**
* Credentials source
* Credentials could be obtained either from credential.json or EAS servers.
*/
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
/**
* Expo SDK version
* It's determined by the expo package version in package.json.
* It's undefined if the expo package is not installed for the project.
*/
sdkVersion?: string;
/**
* Release channel (for expo-updates)
* It's undefined if the expo-updates package is not installed for the project.
*/
releaseChannel?: string;
/**
* Tracking context
* It's used to track build process across different Expo services and tools.
*/
trackingContext: TrackingContext;
/**
* Distribution type
* Indicates whether this is a build for store or internal distribution.
*/
distribution: DistributionType;
/**
* App name (expo.name in app.json/app.config.js)
*/
appName?: string;
/**
* App identifier:
* - iOS builds: the bundle identifier (expo.ios.bundleIdentifier in app.json/app.config.js)
* - Android builds: the application id (expo.android.package in app.json/app.config.js)
*/
appIdentifier?: string;
};
export type PlatformBuildProfile<T extends Platform> = T extends Platform.Android
? AndroidBuildProfile
: iOSBuildProfile;
export enum AnalyticsEvent {
BUILD_COMMAND = 'builds cli build command',
PROJECT_UPLOAD_SUCCESS = 'builds cli project upload success',
PROJECT_UPLOAD_FAIL = 'builds cli project upload fail',
GATHER_CREDENTIALS_SUCCESS = 'builds cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'builds cli gather credentials fail',
CONFIGURE_PROJECT_SUCCESS = 'builds cli configure project success',
CONFIGURE_PROJECT_FAIL = 'builds cli configure project fail',
BUILD_REQUEST_SUCCESS = 'build cli build request success',
BUILD_REQUEST_FAIL = 'builds cli build request fail',
BUILD_STATUS_COMMAND = 'builds cli build status',
CREDENTIALS_SYNC_COMMAND = 'builds cli credentials sync command',
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'builds cli credentials sync update local success',
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'builds cli credentials sync update local fail',
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'builds cli credentials sync update remote success',
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'builds cli credentials sync update remote fail',
}
export type TrackingContext = Record<string, string | number | boolean>;
+27 -1
View File
@@ -1,3 +1,29 @@
import { logEvent } from '../../analytics';
export enum Event {
BUILD_COMMAND = 'build cli build command',
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
BUILD_REQUEST_SUCCESS = 'build cli build request success',
BUILD_REQUEST_FAIL = 'build cli build request fail',
BUILD_STATUS_COMMAND = 'build cli build status',
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
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 {
logEvent: (a: any, b: any) => {},
logEvent(name: Event, properties: Record<string, any>) {
logEvent(name, properties);
},
};
@@ -0,0 +1,33 @@
import * as ExpoConfig from '@expo/config';
import JsonFile from '@expo/json-file';
import assert from 'assert';
export async function updateAppJsonConfigAsync(
{
projectDir,
exp,
}: {
projectDir: string;
exp: ExpoConfig.ExpoConfig;
},
modifyConfig: (config: any) => void
): Promise<void> {
const paths = ExpoConfig.getConfigFilePaths(projectDir);
assert(paths.staticConfigPath, "can't update dynamic config");
const rawStaticConfig = readAppJson(paths.staticConfigPath);
rawStaticConfig.expo = rawStaticConfig.expo ?? {};
modifyConfig(rawStaticConfig.expo);
await JsonFile.writeAsync(paths.staticConfigPath, rawStaticConfig, { json5: false });
modifyConfig(exp);
}
// TODO: remove this once @expo/config exports getStaticConfig
export function readAppJson(appJsonPath: string): any {
const config = JsonFile.read(appJsonPath, { json5: true });
if (config) {
return config as any;
}
throw new Error(`Failed to read app.json`);
}
@@ -0,0 +1,19 @@
import { Platform } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import Log from '../../log';
import { requestedPlatformDisplayNames } from '../../platform';
export function logCredentialsSource(
credentialsSource: CredentialsSource,
platform: Platform
): void {
let message = `Using ${credentialsSource} ${requestedPlatformDisplayNames[platform]} credentials`;
if (credentialsSource === CredentialsSource.LOCAL) {
message += ` ${chalk.dim('(credentials.json)')}`;
} else if (credentialsSource === CredentialsSource.REMOTE) {
message += ` ${chalk.dim('(Expo server)')}`;
}
Log.succeed(message);
}
@@ -0,0 +1,116 @@
import { Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import resolveFrom from 'resolve-from';
import { toAppPlatform } from '../../graphql/types/AppPlatform';
import Log, { learnMore } from '../../log';
import { appPlatformDisplayNames } from '../../platform';
import { resolveWorkflowAsync } from '../../project/workflow';
import { confirmAsync } from '../../prompts';
import { expoCommandAsync } from '../../utils/expoCli';
import { ProfileData } from '../../utils/profiles';
import { getVcsClient } from '../../vcs';
import { reviewAndCommitChangesAsync } from './repository';
export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir,
nonInteractive = false,
buildProfiles = [],
}: {
projectDir: string;
nonInteractive?: boolean;
buildProfiles?: ProfileData<BuildProfile>[];
}): Promise<void> {
if (await isExpoDevClientInstalledAsync(projectDir)) {
return;
}
const buildProfilesWithDevelopmentClientRequired = buildProfiles.filter(
buildProfile => buildProfile.profile.developmentClient
);
const isDevelopmentClientRequired = buildProfilesWithDevelopmentClientRequired.some(Boolean);
if (!isDevelopmentClientRequired) {
return;
}
const platformsToCheck = buildProfilesWithDevelopmentClientRequired.map(
({ platform }) => platform
);
const workflowPerPlatformList = await Promise.all(
platformsToCheck.map(platform => resolveWorkflowAsync(projectDir, platform))
);
Log.newLine();
Log.error(
`You want to build a development client build for platforms: ${platformsToCheck
.map(i => chalk.bold(appPlatformDisplayNames[toAppPlatform(i)]))
.join(', ')}`
);
Log.error(
`However, we detected that you don't have ${chalk.bold(
'expo-dev-client'
)} installed for your project.`
);
const areAllManaged = workflowPerPlatformList.every(i => i === Workflow.MANAGED);
if (areAllManaged) {
const install = await confirmAsync({
message: 'Do you want EAS CLI to install expo-dev-client for you?',
instructions: 'The command will abort unless you agree.',
});
if (install) {
await installExpoDevClientAsync(projectDir, { nonInteractive });
} else {
error(`Install ${chalk.bold('expo-dev-client')} manually and come back later.`, {
exit: 1,
});
}
} else {
Log.warn(`You'll need to install ${chalk.bold('expo-dev-client')} manually.`);
Log.warn(
learnMore('https://docs.expo.dev/clients/installation/', {
learnMoreMessage: 'See installation instructions on how to do it.',
dim: false,
})
);
Log.warn('If you proceed anyway, you might not get the build you want.');
Log.newLine();
const shouldContinue = await confirmAsync({
message: 'Do you want to proceed anyway?',
initial: false,
});
if (!shouldContinue) {
error('Come back later', { exit: 1 });
}
}
}
async function isExpoDevClientInstalledAsync(projectDir: string): Promise<boolean> {
try {
resolveFrom(projectDir, 'expo-dev-client/package.json');
return true;
} catch (err: any) {
Log.debug(err);
return false;
}
}
async function installExpoDevClientAsync(
projectDir: string,
{ nonInteractive }: { nonInteractive: boolean }
): Promise<void> {
Log.newLine();
Log.log(`Running ${chalk.bold('expo install expo-dev-client')}`);
Log.newLine();
await expoCommandAsync(projectDir, ['install', 'expo-dev-client']);
Log.newLine();
if (await getVcsClient().isCommitRequiredAsync()) {
await reviewAndCommitChangesAsync('Install expo-dev-client', {
nonInteractive,
});
}
}
+86 -31
View File
@@ -1,69 +1,124 @@
import chalk from 'chalk';
import {
AppPlatform,
BuildFragment,
BuildStatus as GraphQLBuildStatus,
} from '../../graphql/generated';
import { appPlatformDisplayNames } from '../../platform';
import formatFields from '../../utils/formatFields';
import { platformDisplayNames } from '../constants';
import { Build, BuildStatus } from '../types';
import { getBuildLogsUrl } from './url';
interface Options {
accountName: string;
}
export default function formatBuild(build: Build, { accountName }: Options) {
const fields: { label: string; value: string }[] = [
export function formatGraphQLBuild(build: BuildFragment): string {
const actor = getActorName(build);
const fields: { label: string; value?: string | null }[] = [
{ label: 'ID', value: build.id },
{
label: 'Platform',
value: platformDisplayNames[build.platform],
value: appPlatformDisplayNames[build.platform],
},
{
label: 'Status',
get value() {
switch (build.status) {
case BuildStatus.IN_QUEUE:
case GraphQLBuildStatus.New:
return chalk.blue('new');
case GraphQLBuildStatus.InQueue:
return chalk.blue('in queue');
case BuildStatus.IN_PROGRESS:
case GraphQLBuildStatus.InProgress:
return chalk.blue('in progress');
case BuildStatus.FINISHED:
case GraphQLBuildStatus.Canceled:
return chalk.gray('canceled');
case GraphQLBuildStatus.Finished:
return chalk.green('finished');
case BuildStatus.ERRORED:
case GraphQLBuildStatus.Errored:
return chalk.red('errored');
default:
return 'unknown';
}
},
},
{
label: 'Distribution',
value: build.distribution?.toLowerCase(),
},
{
label: 'Enterprise Provisioning',
value: build.iosEnterpriseProvisioning?.toLowerCase(),
},
{
label: 'Release Channel',
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: accountName }),
value: getBuildLogsUrl(build),
},
{
label: 'Artifact',
get value() {
if (build.status === BuildStatus.IN_QUEUE || build.status === BuildStatus.IN_PROGRESS) {
return '<in progress>';
switch (build.status) {
case GraphQLBuildStatus.New:
case GraphQLBuildStatus.InQueue:
case GraphQLBuildStatus.InProgress:
return '<in progress>';
case GraphQLBuildStatus.Canceled:
case GraphQLBuildStatus.Errored:
return null;
case GraphQLBuildStatus.Finished: {
const url = build.artifacts?.buildUrl;
return url ? url : chalk.red('not found');
}
default:
return null;
}
const url = build.artifacts?.buildUrl;
if (!url) {
return chalk.red('not found');
}
return url;
},
},
{ label: 'Started at', value: new Date(build.createdAt).toLocaleString() },
{
label: 'Finished at',
value:
build.status === BuildStatus.IN_QUEUE || build.status === BuildStatus.IN_PROGRESS
? '<in progress>'
: new Date(build.updatedAt).toLocaleString(),
value: [
GraphQLBuildStatus.New,
GraphQLBuildStatus.InQueue,
GraphQLBuildStatus.InProgress,
].includes(build.status)
? '<in progress>'
: new Date(build.updatedAt).toLocaleString(),
},
// TODO: we can do it once we migrate to GraphQL
// { label: 'Started by', get: build => 'TODO' },
{ 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 => {
return build.initiatingActor?.displayName || 'unknown';
};
@@ -1,90 +1,122 @@
import { DistributionType } from '@expo/eas-json';
import assert from 'assert';
import chalk from 'chalk';
import indentString from 'indent-string';
import qrcodeTerminal from 'qrcode-terminal';
import log from '../../log';
import { platformDisplayNames, platformEmojis } from '../constants';
import { Build } from '../types';
import { getBuildLogsUrl } from './url';
import {
AppPlatform,
BuildError,
BuildFragment,
BuildStatus,
DistributionType,
EasBuildDeprecationInfo,
EasBuildDeprecationInfoType,
} from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
import { printJsonOnlyOutput } from '../../utils/json';
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
export interface DeprecationInfo {
type: 'user-facing' | 'internal';
message: string;
}
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(`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,
});
log(`${platformDisplayNames[platform]} build details: ${chalk.underline(logsUrl)}`);
builds.forEach(build => {
const logsUrl = getBuildLogsUrl(build);
Log.log(
`${appPlatformEmojis[build.platform]} ${
appPlatformDisplayNames[build.platform]
} build details: ${chalk.underline(logsUrl)}`
);
});
}
}
export function printBuildResults(accountName: string, builds: (Build | null)[]): void {
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 Build[])
.filter(build => build.status === 'finished')
.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: Build): void {
if (build.metadata?.distribution === DistributionType.INTERNAL) {
const logsUrl = getBuildLogsUrl({
buildId: build.id,
account: accountName,
});
log(
`${platformEmojis[build.platform]} Open this link on your ${
platformDisplayNames[build.platform]
} devices to install the app:`
function printBuildResult(build: BuildFragment): void {
Log.addNewLineIfNone();
if (build.status === BuildStatus.Errored) {
const userError = build.error;
Log.error(
`${appPlatformEmojis[build.platform]} ${
appPlatformDisplayNames[build.platform]
} build failed${userError ? ':' : ''}`
);
log(`${chalk.underline(logsUrl)}`);
log.newLine();
if (userError) {
printUserError(userError);
}
return;
}
if (build.status === BuildStatus.Canceled) {
Log.error(
`${appPlatformEmojis[build.platform]} ${
appPlatformDisplayNames[build.platform]
} build was canceled`
);
return;
}
if (build.distribution === DistributionType.Internal) {
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]
} devices (or scan the QR code) to install the app:`
);
Log.log(`${chalk.underline(logsUrl)}`);
} else {
// TODO: it looks like buildUrl could possibly be undefined, based on the code below.
// we should account for this case better if it is possible
const url = build.artifacts?.buildUrl ?? '';
log(`${platformEmojis[build.platform]} ${platformDisplayNames[build.platform]} app:`);
log(`${chalk.underline(url)}`);
log.newLine();
Log.log(`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`);
Log.log(`${chalk.underline(url)}`);
}
}
export function printDeprecationWarnings(deprecationInfo?: DeprecationInfo): void {
export function printDeprecationWarnings(deprecationInfo?: EasBuildDeprecationInfo | null): void {
if (!deprecationInfo) {
return;
}
if (deprecationInfo.type === 'internal') {
log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
log.warn("Changes won't affect your project config.");
log.warn(deprecationInfo.message);
} else if (deprecationInfo.type === 'user-facing') {
log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
log.warn(
if (deprecationInfo.type === EasBuildDeprecationInfoType.Internal) {
Log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
Log.warn("Changes won't affect your project config.");
Log.warn(deprecationInfo.message);
} else if (deprecationInfo.type === EasBuildDeprecationInfoType.UserFacing) {
Log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
Log.warn(
'There might be some changes necessary to your project config, latest eas-cli will provide more specific error messages.'
);
log.warn(deprecationInfo.message);
Log.warn(deprecationInfo.message);
} else {
log.warn('An unexpected warning was encountered. Please report it as a bug:');
log.warn(deprecationInfo);
Log.warn('An unexpected warning was encountered. Please report it as a bug:');
Log.warn(deprecationInfo);
}
}
export function printUserError(error: BuildError): void {
Log.error(error.message);
if (error.docsUrl) {
Log.error(learnMore(error.docsUrl, { dim: false }));
}
}
+98 -96
View File
@@ -1,64 +1,24 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import fs from 'fs-extra';
import ora from 'ora';
import path from 'path';
import tar from 'tar';
import { v4 as uuidv4 } from 'uuid';
import log from '../../log';
import Log from '../../log';
import { ora } from '../../ora';
import { confirmAsync, promptAsync } from '../../prompts';
import {
doesGitRepoExistAsync,
getGitDiffOutputAsync,
gitDiffAsync,
gitRootDirectoryAsync,
gitStatusAsync,
isGitInstalledAsync,
} from '../../utils/git';
import { formatBytes } from '../../utils/files';
import { getTmpDirectory } from '../../utils/paths';
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
import { getVcsClient } from '../../vcs';
async function ensureGitRepoExistsAsync(): Promise<void> {
if (!(await isGitInstalledAsync())) {
throw new Error('git command not found, install it before proceeding');
}
if (await doesGitRepoExistAsync()) {
export async function maybeBailOnRepoStatusAsync(): Promise<void> {
if (!(await getVcsClient().isCommitRequiredAsync())) {
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("We're going to make an initial commit for you repository.");
await spawnAsync('git', ['add', '-A']);
await commitPromptAsync('Initial commit');
}
async function isGitStatusCleanAsync(): Promise<boolean> {
const changes = await gitStatusAsync();
return changes.length === 0;
}
async function maybeBailOnGitStatusAsync(): Promise<void> {
if (await isGitStatusCleanAsync()) {
return;
}
log.addNewLineIfNone();
log.warn(`${chalk.bold('Warning!')} Your git working tree is dirty.`);
log(
Log.addNewLineIfNone();
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'
)}, so you can revert the changes made by this command if necessary.`
@@ -72,13 +32,13 @@ 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 getVcsClient().isCommitRequiredAsync())) {
return;
}
log.addNewLineIfNone();
log.warn(`${chalk.bold('Warning!')} Your git working tree is dirty.`);
log(
Log.addNewLineIfNone();
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'
)}.`
@@ -90,13 +50,30 @@ async function ensureGitStatusIsCleanAsync(nonInteractive = false): Promise<void
message: `Commit changes to git?`,
});
if (answer) {
await commitPromptAsync();
await commitPromptAsync({ commitAllFiles: true });
} else {
throw new Error('Please commit all changes. Aborting...');
}
}
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 getVcsClient().commitAsync({ commitAllFiles, commitMessage: message });
}
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
const spinner = ora('Compressing project files');
await fs.mkdirp(getTmpDirectory());
@@ -111,66 +88,91 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
() => {
spinner.start();
},
log.isDebug ? 1 : 1000
Log.isDebug ? 1 : 1000
);
// TODO: Possibly warn after more time about unoptimized assets.
const compressTimerLabel = 'makeProjectTarballAsync';
startTimer(compressTimerLabel);
try {
await spawnAsync('git', [
'clone',
'--local',
'--no-hardlinks',
'--depth',
'1',
`file://${await gitRootDirectoryAsync()}`,
shallowClonePath,
]);
await getVcsClient().makeShallowCopyAsync(shallowClonePath);
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
'.',
]);
} finally {
// Stop the timer
} catch (err) {
clearTimeout(timer);
await fs.remove(shallowClonePath);
const duration = endTimer(compressTimerLabel);
if (spinner.isSpinning) {
const prettyTime = formatMilliseconds(duration);
spinner.succeed(`Compressed project files ${chalk.dim(prettyTime)}`);
spinner.fail();
}
throw err;
} finally {
await fs.remove(shallowClonePath);
}
clearTimeout(timer);
const { size } = await fs.stat(tarPath);
const duration = endTimer(compressTimerLabel);
if (spinner.isSpinning) {
const prettyTime = formatMilliseconds(duration);
spinner.succeed(
`Compressed project files ${chalk.dim(`${prettyTime} (${formatBytes(size)})`)}`
);
}
return { size, path: tarPath };
}
async function showDiffAsync() {
const outputTooLarge = (await getGitDiffOutputAsync()).split(/\r\n|\r|\n/).length > 100;
await gitDiffAsync({ withPager: outputTooLarge });
enum ShouldCommitChanges {
Yes,
ShowDiffFirst,
Abort,
}
async function commitPromptAsync(initialCommitMessage?: string): Promise<void> {
const { message } = await promptAsync({
type: 'text',
name: 'message',
message: 'Commit message:',
initial: initialCommitMessage,
validate: (input: string) => input !== '',
export async function reviewAndCommitChangesAsync(
initialCommitMessage: string,
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
): Promise<void> {
if (process.env.EAS_BUILD_AUTOCOMMIT) {
await getVcsClient().commitAsync({
commitMessage: initialCommitMessage,
commitAllFiles: false,
});
Log.withTick('Committed changes.');
return;
}
if (nonInteractive) {
throw new Error(
'Cannot commit changes when --non-interactive is specified. Run the command in interactive mode or set EAS_BUILD_AUTOCOMMIT=1 in your environment.'
);
}
const { selected } = await promptAsync({
type: 'select',
name: 'selected',
message: 'Can we commit these changes to git for you?',
choices: [
{ title: 'Yes', value: ShouldCommitChanges.Yes },
...(askedFirstTime
? [{ title: 'Show the diff and ask me again', value: ShouldCommitChanges.ShowDiffFirst }]
: []),
{
title: 'Abort build process',
value: ShouldCommitChanges.Abort,
},
],
});
// Add changed files only
await spawnAsync('git', ['add', '-u']);
await spawnAsync('git', ['commit', '-m', message]);
if (selected === ShouldCommitChanges.Abort) {
throw new Error(
"Aborting, run the command again once you're ready. Make sure to commit any changes you've made."
);
} else if (selected === ShouldCommitChanges.Yes) {
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes.');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await getVcsClient().showDiffAsync();
await reviewAndCommitChangesAsync(initialCommitMessage, {
nonInteractive,
askedFirstTime: false,
});
}
}
export {
isGitStatusCleanAsync,
showDiffAsync,
commitPromptAsync,
ensureGitRepoExistsAsync,
ensureGitStatusIsCleanAsync,
maybeBailOnGitStatusAsync,
makeProjectTarballAsync,
};
+2 -2
View File
@@ -1,8 +1,8 @@
import { ExpoConfig, getPackageJson } from '@expo/config';
export function isExpoUpdatesInstalled(projectDir: string) {
export function isExpoUpdatesInstalled(projectDir: string): boolean {
const packageJson = getPackageJson(projectDir);
return packageJson.dependencies && 'expo-updates' in packageJson.dependencies;
return !!(packageJson.dependencies && 'expo-updates' in packageJson.dependencies);
}
export function ensureValidVersions(exp: ExpoConfig): void {
+27 -9
View File
@@ -1,15 +1,33 @@
import { getExpoWebsiteBaseUrl } from '../../api';
import assert from 'assert';
export function getBuildLogsUrl({
buildId,
account,
}: {
buildId: string;
account: string;
}): string {
return `${getExpoWebsiteBaseUrl()}/accounts/${account}/builds/${buildId}`;
import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api';
import { AppPlatform, BuildFragment } from '../../graphql/generated';
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 {
return `${getExpoWebsiteBaseUrl()}/artifacts/${artifactId}`;
}
export function getInternalDistributionInstallUrl(build: BuildFragment): string {
if (build.platform === AppPlatform.Ios) {
return `itms-services://?action=download-manifest;url=${getExpoApiBaseUrl()}/--/api/v2/projects/${
build.project.id
}/builds/${build.id}/manifest.plist`;
}
assert(build.artifacts?.buildUrl, 'buildUrl is missing');
return build.artifacts.buildUrl;
}
@@ -0,0 +1,47 @@
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import semver from 'semver';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { updateAppJsonConfigAsync } from './appJson';
export function ensureStaticConfigExists(projectDir: string): void {
const paths = getConfigFilePaths(projectDir);
if (!paths.staticConfigPath) {
throw new Error('autoIncrement option is not supported when using app.config.js');
}
}
export async function bumpAppVersionAsync({
appVersion,
projectDir,
exp,
}: {
appVersion: string;
projectDir: string;
exp: ExpoConfig;
}): Promise<void> {
let bumpedAppVersion: string;
if (semver.valid(appVersion)) {
bumpedAppVersion = nullthrows(semver.inc(appVersion, 'patch'));
Log.log(
`Bumping ${chalk.bold('expo.version')} from ${chalk.bold(appVersion)} to ${chalk.bold(
bumpedAppVersion
)}`
);
} else {
Log.log(`${chalk.bold('expo.version')} = ${chalk.bold(appVersion)} is not a valid semver`);
bumpedAppVersion = (
await promptAsync({
type: 'text',
name: 'bumpedAppVersion',
message: 'What is the next version?',
})
).bumpedAppVersion;
}
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.version = bumpedAppVersion;
});
}
+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 { getVcsClient } 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 = path.normalize(await getVcsClient().getRootPathAsync());
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
if (
(await fs.pathExists(absGoogleServicesFilePath)) &&
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
(await getVcsClient().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 your build profile's env.GOOGLE_SERVICES_FILE in eas.json to any non-empty string.`
);
Log.newLine();
}
}
function isInsideDirectory(file: string, directory: string): boolean {
return file.startsWith(directory);
}
@@ -0,0 +1,69 @@
import { EasJsonReader } from '@expo/eas-json';
import { Command } from '@oclif/command';
import semver from 'semver';
import {
AnalyticsEvent,
flushAsync as flushAnalyticsAsync,
initAsync as initAnalyticsAsync,
logEvent,
} from '../analytics';
import Log from '../log';
import { findProjectRootAsync } from '../project/projectUtils';
import { getUserAsync } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { setVcsClient } from '../vcs';
import GitClient from '../vcs/clients/git';
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;
protected abstract runAsync(): Promise<any>;
// eslint-disable-next-line async-protect/async-suffix
async run(): Promise<any> {
EasJsonReader.setLog(Log);
await initAnalyticsAsync();
await this.applyCliConfigAsync();
if (this.requiresAuthentication) {
const { flags } = this.parse();
const nonInteractive = (flags as any)['non-interactive'] ?? false;
await ensureLoggedInAsync({ nonInteractive });
} 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}`,
});
return this.runAsync();
}
// eslint-disable-next-line async-protect/async-suffix
async finally(err: Error): Promise<any> {
await flushAnalyticsAsync();
return super.finally(err);
}
private async applyCliConfigAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const easJsonReader = new EasJsonReader(projectDir);
const config = await easJsonReader.getCliConfigAsync();
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
throw new Error(
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
);
}
if (config?.requireCommit) {
setVcsClient(new GitClient());
}
}
}
@@ -0,0 +1,101 @@
import { asMock } from '../../__tests__/utils';
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(),
};
});
let originalProcessArgv: string[];
beforeAll(() => {
originalProcessArgv = process.argv;
process.argv = [];
});
afterAll(() => {
process.argv = originalProcessArgv;
});
beforeEach(() => {
asMock(getUserAsync)
.mockReset()
.mockImplementation(() => mockJester);
asMock(ensureLoggedInAsync)
.mockReset()
.mockImplementation(() => mockJester);
asMock(initAsync).mockReset();
asMock(flushAsync).mockReset();
asMock(logEvent).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 () => {
asMock(TestEasCommand.prototype.authValue).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(): boolean {
return false;
}
async runAsync(): Promise<void> {}
}
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
@@ -1,16 +1,15 @@
import { Command } from '@oclif/command';
import log from '../../log';
import EasCommand from '../../commandUtils/EasCommand';
import Log from '../../log';
import { showLoginPromptAsync } from '../../user/actions';
export default class AccountLogin extends Command {
export default class AccountLogin extends EasCommand {
static description = 'log in with your Expo account';
static aliases = ['login'];
async run() {
log('Log in to EAS');
protected requiresAuthentication = false;
async runAsync(): Promise<void> {
await showLoginPromptAsync();
log('Logged in');
Log.log('Logged in');
}
}
@@ -1,15 +1,15 @@
import { Command } from '@oclif/command';
import log from '../../log';
import EasCommand from '../../commandUtils/EasCommand';
import Log from '../../log';
import { logoutAsync } from '../../user/User';
export default class AccountLogout extends Command {
export default class AccountLogout extends EasCommand {
static description = 'log out';
static aliases = ['logout'];
async run() {
protected requiresAuthentication = false;
async runAsync(): Promise<void> {
await logoutAsync();
log('Logged out');
Log.log('Logged out');
}
}
@@ -1,21 +1,21 @@
import { Command } from '@oclif/command';
import chalk from 'chalk';
import log from '../../log';
import { getUserAsync } from '../../user/User';
import { getActorDisplayName } from '../../user/actions';
import EasCommand from '../../commandUtils/EasCommand';
import Log from '../../log';
import { getActorDisplayName, getUserAsync } from '../../user/User';
export default class AccountView extends Command {
export default class AccountView extends EasCommand {
static description = 'show the username you are logged in as';
static aliases = ['whoami'];
async run() {
protected requiresAuthentication = false;
async runAsync(): Promise<void> {
const user = await getUserAsync();
if (user) {
log(chalk.green(getActorDisplayName(user)));
Log.log(chalk.green(getActorDisplayName(user)));
} else {
log.warn('Not logged in');
Log.warn('Not logged in');
process.exit(1);
}
}
@@ -0,0 +1,26 @@
import EasCommand from '../commandUtils/EasCommand';
import Log from '../log';
import UserSettings from '../user/UserSettings';
export default class AnalyticsView extends EasCommand {
static description = 'view or change analytics settings';
static args = [{ name: 'STATUS', options: ['on', 'off'] }];
protected requiresAuthentication = false;
async runAsync(): Promise<void> {
const { STATUS: status } = this.parse(AnalyticsView).args;
if (status) {
await UserSettings.setAsync('analyticsEnabled', status === 'on');
Log.withTick(`${status === 'on' ? 'Enabling' : 'Disabling'} analytics.`);
} else {
const analyticsEnabled = await UserSettings.getAsync('analyticsEnabled', null);
Log.log(
`Analytics are ${
analyticsEnabled === false ? 'disabled' : 'enabled'
} on this eas-cli installation.`
);
}
}
}
@@ -0,0 +1,110 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import chalk from 'chalk';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
CreateUpdateBranchForAppMutation,
CreateUpdateBranchForAppMutationVariables,
UpdateBranch,
} from '../../graphql/generated';
import Log from '../../log';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { getVcsClient } from '../../vcs';
export async function createUpdateBranchOnAppAsync({
appId,
name,
}: CreateUpdateBranchForAppMutationVariables): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
const result = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
gql`
mutation createUpdateBranchForApp($appId: ID!, $name: String!) {
updateBranch {
createUpdateBranchForApp(appId: $appId, name: $name) {
id
name
}
}
}
`,
{
appId,
name,
}
)
.toPromise()
);
const newBranch = result.updateBranch.createUpdateBranchForApp;
if (!newBranch) {
throw new Error(`Could not create branch ${name}.`);
}
return newBranch;
}
export default class BranchCreate extends EasCommand {
static hidden = true;
static description = 'Create a branch on the current project.';
static args = [
{
name: 'name',
required: false,
description: 'Name of the branch to create',
},
];
static flags = {
json: flags.boolean({
description: 'return a json with the new branch ID and name.',
default: false,
}),
};
async runAsync(): Promise<void> {
let {
args: { name },
flags,
} = this.parse(BranchCreate);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!name) {
const validationMessage = 'Branch name may not be empty.';
if (flags.json) {
throw new Error(validationMessage);
}
({ name } = await promptAsync({
type: 'text',
name: 'name',
message: 'Please name the branch:',
initial:
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substr(2, 4)}`,
validate: value => (value ? true : validationMessage),
}));
}
const newBranch = await createUpdateBranchOnAppAsync({ appId: projectId, name });
if (flags.json) {
Log.log(newBranch);
return;
}
Log.withTick(
`️Created a new branch: ${chalk.bold(newBranch.name)} on project ${chalk.bold(fullName)}.`
);
}
}
@@ -0,0 +1,152 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import chalk from 'chalk';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
DeleteUpdateBranchMutation,
DeleteUpdateBranchMutationVariables,
DeleteUpdateBranchResult,
GetBranchInfoQuery,
GetBranchInfoQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
async function getBranchInfoAsync({
appId,
name,
}: GetBranchInfoQueryVariables): Promise<GetBranchInfoQuery> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<GetBranchInfoQuery, GetBranchInfoQueryVariables>(
gql`
query GetBranchInfo($appId: String!, $name: String!) {
app {
byId(appId: $appId) {
id
updateBranchByName(name: $name) {
id
name
}
}
}
}
`,
{
appId,
name,
},
{ additionalTypenames: ['UpdateBranch'] }
)
.toPromise()
);
return data;
}
async function deleteBranchOnAppAsync({
branchId,
}: DeleteUpdateBranchMutationVariables): Promise<DeleteUpdateBranchResult> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<DeleteUpdateBranchMutation, DeleteUpdateBranchMutationVariables>(
gql`
mutation DeleteUpdateBranch($branchId: ID!) {
updateBranch {
deleteUpdateBranch(branchId: $branchId) {
id
}
}
}
`,
{
branchId,
}
)
.toPromise()
);
return data.updateBranch.deleteUpdateBranch;
}
export default class BranchDelete extends EasCommand {
static hidden = true;
static description = 'Republish an update group';
static args = [
{
name: 'name',
required: false,
description: 'Name of the branch to delete',
},
];
static flags = {
json: flags.boolean({
description: `return JSON with the edited branch's ID and name.`,
default: false,
}),
};
async runAsync(): Promise<void> {
let {
args: { name },
flags: { json: jsonFlag },
} = this.parse(BranchDelete);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!name) {
const validationMessage = 'branch name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ name } = await promptAsync({
type: 'text',
name: 'name',
message: 'Please enter the name of the branch to delete:',
validate: value => (value ? true : validationMessage),
}));
}
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();
Log.warn(
`You are about to permamently delete branch: "${name}" and all of the updates published on it.` +
`\nThis action is irreversible.`
);
Log.newLine();
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
if (!confirmed) {
Log.error(`Cancelled deletion of branch: "${name}".`);
process.exit(1);
}
}
const deletionResult = await deleteBranchOnAppAsync({
branchId,
});
if (jsonFlag) {
Log.log(JSON.stringify(deletionResult));
}
Log.withTick(
`️Deleted branch "${name}" and all of its updates on project ${chalk.bold(fullName)}.`
);
}
}
@@ -0,0 +1,104 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import chalk from 'chalk';
import CliTable from 'cli-table3';
import { print } from 'graphql';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
BranchesByAppQuery,
BranchesByAppQueryVariables,
UpdateBranchFragment,
} from '../../graphql/generated';
import { UpdateBranchFragmentNode } from '../../graphql/types/UpdateBranch';
import Log from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
const BRANCHES_LIMIT = 10_000;
export async function listBranchesAsync({
projectId,
}: {
projectId: string;
}): Promise<UpdateBranchFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<BranchesByAppQuery, BranchesByAppQueryVariables>(
gql`
query BranchesByAppQuery($appId: String!, $limit: Int!) {
app {
byId(appId: $appId) {
id
updateBranches(offset: 0, limit: $limit) {
id
...UpdateBranchFragment
}
}
}
}
${print(UpdateBranchFragmentNode)}
`,
{
appId: projectId,
limit: BRANCHES_LIMIT,
},
{ additionalTypenames: ['UpdateBranch'] }
)
.toPromise()
);
return data?.app?.byId.updateBranches ?? [];
}
export default class BranchList extends EasCommand {
static hidden = true;
static description = 'List all branches on this project.';
static flags = {
json: flags.boolean({
description: 'return output as JSON',
default: false,
}),
};
async runAsync(): Promise<void> {
const { flags } = this.parse(BranchList);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const branches = await listBranchesAsync({ projectId });
if (flags.json) {
Log.log(JSON.stringify(branches, null, 2));
} else {
const table = new CliTable({
head: ['branch', ...UPDATE_COLUMNS],
wordWrap: true,
});
table.push(
...branches.map(branch => [
branch.name,
formatUpdate(branch.updates[0]),
branch.updates[0]?.runtimeVersion ?? 'N/A',
branch.updates[0]?.group ?? 'N/A',
getPlatformsForGroup({
updates: branch.updates,
group: branch.updates[0]?.group,
}),
])
);
Log.addNewLineIfNone();
Log.log(chalk.bold('Branches with their most recent update group:'));
Log.log(table.toString());
if (branches.length >= BRANCHES_LIMIT) {
Log.warn(`Showing first ${BRANCHES_LIMIT} branches, some results might be omitted.`);
}
}
}
}
@@ -0,0 +1,443 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { flags } from '@oclif/command';
import assert from 'assert';
import chalk from 'chalk';
import dateFormat from 'dateformat';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
GetUpdateGroupAsyncQuery,
Robot,
RootQueryUpdatesByGroupArgs,
Update,
UpdateInfoGroup,
User,
ViewBranchQuery,
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import Log from '../../log';
import { ora } from '../../ora';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import {
PublishPlatform,
buildBundlesAsync,
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync, selectAsync } from '../../prompts';
import { formatUpdate } from '../../update/utils';
import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
import { getVcsClient } from '../../vcs';
import { createUpdateBranchOnAppAsync } from './create';
import { listBranchesAsync } from './list';
import { viewUpdateBranchAsync } from './view';
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
type PlatformFlag = PublishPlatform | 'all';
async function getUpdateGroupAsync({
group,
}: RootQueryUpdatesByGroupArgs): Promise<GetUpdateGroupAsyncQuery['updatesByGroup']> {
const { updatesByGroup } = await withErrorHandlingAsync(
graphqlClient
.query<GetUpdateGroupAsyncQuery, RootQueryUpdatesByGroupArgs>(
gql`
query getUpdateGroupAsync($group: ID!) {
updatesByGroup(group: $group) {
id
group
runtimeVersion
manifestFragment
platform
message
}
}
`,
{
group,
},
{ additionalTypenames: ['Update'] }
)
.toPromise()
);
return updatesByGroup;
}
async function ensureBranchExistsAsync({
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 EasCommand {
static hidden = true;
static description = 'Publish an update group to a branch.';
static args = [
{
name: 'name',
description: 'Name of the branch to publish on',
},
];
static flags = {
message: flags.string({
description: 'short message describing the updates.',
required: false,
}),
republish: flags.boolean({
description: 'republish an update group',
exclusive: ['input-dir', 'skip-bundler'],
}),
group: flags.string({
description: 'update group to republish',
exclusive: ['input-dir', 'skip-bundler'],
}),
'input-dir': flags.string({
description: 'location of the bundle',
default: 'dist',
required: false,
}),
'skip-bundler': flags.boolean({
description: `skip running Expo CLI to bundle the app before publishing`,
default: false,
}),
platform: flags.enum({
char: 'p',
description: `only publish to a single platform`,
options: [...defaultPublishPlatforms, 'all'],
default: 'all',
required: false,
}),
json: flags.boolean({
description: `return a json with the new update group.`,
default: false,
}),
auto: flags.boolean({
description:
'use the current git branch and commit message for the EAS branch and update message',
default: false,
}),
};
async runAsync(): Promise<void> {
let {
args: { name: branchName },
flags: {
json: jsonFlag,
auto: autoFlag,
message,
republish,
group,
'input-dir': inputDir,
'skip-bundler': skipBundler,
},
} = this.parse(BranchPublish);
const platformFlag = this.parse(BranchPublish).flags.platform as PlatformFlag;
// If a group was specified, that means we are republishing it.
republish = group ? true : republish;
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
isPublicConfig: true,
});
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
const projectId = await getProjectIdAsync(exp);
if (!branchName && autoFlag) {
branchName =
(await getVcsClient().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 });
branchName = await selectAsync<string>(
'which branch would you like to publish on?',
branches.map(branch => {
return {
title: `${branch.name} ${chalk.grey(
`- current update: ${formatUpdate(branch.updates[0])}`
)}`,
value: branch.name,
};
})
);
assert(branchName, 'branch name must be specified.');
}
const { id: branchId, updates } = await ensureBranchExistsAsync({
appId: projectId,
name: branchName,
});
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.
// Instead we get the `updateInfoGroup` from the update we wish to republish.
let updatesToRepublish: Pick<
Update,
'group' | 'message' | 'runtimeVersion' | 'manifestFragment' | 'platform'
>[];
if (group) {
updatesToRepublish = await getUpdateGroupAsync({ group });
} else {
// Drop into interactive mode if the user has not specified an update group to republish.
if (jsonFlag) {
throw new Error('You must specify the update group to republish.');
}
const updateGroups = uniqBy(updates, u => u.group)
.filter(update => {
// Only show groups that have updates on the specified platform(s).
return platformFlag === 'all' || update.platform === platformFlag;
})
.map(update => ({
title: formatUpdateTitle(update),
value: update.group,
}));
if (updateGroups.length === 0) {
throw new Error(
`There are no updates on branch "${branchName}" published on the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
);
}
const selectedUpdateGroup = await selectAsync<string>(
'which update would you like to republish?',
updateGroups
);
updatesToRepublish = updates.filter(update => update.group === selectedUpdateGroup);
}
const updatesToRepublishFilteredByPlatform = updatesToRepublish.filter(
// Only republish to the specified platforms
update => platformFlag === 'all' || update.platform === platformFlag
);
if (updatesToRepublishFilteredByPlatform.length === 0) {
throw new Error(
`There are no updates on branch "${branchName}" published on the platform(s) "${platformFlag}" with group ID "${
group ? group : updatesToRepublish[0].group
}". Did you mean to publish a new update instead?`
);
}
let publicationPlatformMessage: string;
if (platformFlag === 'all') {
if (updatesToRepublishFilteredByPlatform.length !== defaultPublishPlatforms.length) {
Log.warn(`You are republishing an update that wasn't published for all platforms.`);
}
publicationPlatformMessage = `The republished update will appear on the same plaforms it was originally published on: ${updatesToRepublishFilteredByPlatform
.map(update => update.platform)
.join(',')}`;
} else {
publicationPlatformMessage = `The republished update will appear only on: ${platformFlag}`;
}
Log.withTick(publicationPlatformMessage);
for (const update of updatesToRepublishFilteredByPlatform) {
const { manifestFragment } = update;
const platform = update.platform as PublishPlatform;
unsortedUpdateInfoGroups[platform] = JSON.parse(manifestFragment);
}
// These are the same for each member of an update group
group = updatesToRepublishFilteredByPlatform[0].group;
oldMessage = updatesToRepublishFilteredByPlatform[0].message ?? '';
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
} else {
// build bundle and upload assets for a new publish
if (!skipBundler) {
await buildBundlesAsync({ projectDir, inputDir });
}
const assetSpinner = ora('Uploading assets...').start();
try {
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
const assets = await collectAssetsAsync({ inputDir: inputDir!, platforms });
await uploadAssetsAsync(assets);
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
assetSpinner.succeed('Uploaded assets!');
} catch (e) {
assetSpinner.fail('Failed to upload assets');
throw e;
}
}
if (!message && autoFlag) {
message = (await getVcsClient().getLastCommitMessageAsync())?.trim();
}
if (!message) {
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ publishMessage: message } = await promptAsync({
type: 'text',
name: 'publishMessage',
message: `Please enter a publication message.`,
initial: republish
? `Republish "${oldMessage!}" - group: ${group}`
: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
validate: value => (value ? true : validationMessage),
}));
}
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 {
newUpdates = await PublishMutation.publishUpdateGroupAsync(updateGroups);
publishSpinner.succeed('Published!');
} catch (e) {
publishSpinner.fail('Failed to published updates');
throw e;
}
if (jsonFlag) {
Log.log(JSON.stringify(newUpdates));
} else {
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();
}
}
}
}
async function getRuntimeVersionObjectAsync(
exp: ExpoConfig,
platformFlag: PlatformFlag,
projectDir: string
): Promise<Record<string, string>> {
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
for (const platform of platforms) {
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
if (isPolicy) {
const isManaged = (await resolveWorkflowAsync(projectDir, platform)) === Workflow.MANAGED;
if (!isManaged) {
throw new Error('Runtime version policies are only supported in the managed workflow.');
}
}
}
return Object.fromEntries(
platforms.map(platform => [platform, Updates.getRuntimeVersion(exp, platform)])
);
}
function formatUpdateTitle(
update: Exclude<
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
null | undefined
>['updates'][number]
): string {
const { message, createdAt, actor, runtimeVersion } = update;
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}`;
}
@@ -0,0 +1,125 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import chalk from 'chalk';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
EditUpdateBranchInput,
EditUpdateBranchMutation,
EditUpdateBranchMutationVariables,
UpdateBranch,
} from '../../graphql/generated';
import Log from '../../log';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
async function renameUpdateBranchOnAppAsync({
appId,
name,
newName,
}: EditUpdateBranchInput): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<EditUpdateBranchMutation, EditUpdateBranchMutationVariables>(
gql`
mutation EditUpdateBranch($input: EditUpdateBranchInput!) {
updateBranch {
editUpdateBranch(input: $input) {
id
name
}
}
}
`,
{
input: {
appId,
name,
newName,
},
}
)
.toPromise()
);
return data.updateBranch.editUpdateBranch;
}
export default class BranchRename extends EasCommand {
static hidden = true;
static description = 'Rename a branch.';
static flags = {
from: flags.string({
description: 'current name of the branch.',
required: false,
}),
to: flags.string({
description: 'new name of the branch.',
required: false,
}),
json: flags.boolean({
description: `return a json with the edited branch's ID and name.`,
default: false,
}),
};
async runAsync(): Promise<void> {
let {
flags: { json: jsonFlag, from: currentName, to: newName },
} = this.parse(BranchRename);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!currentName) {
const validationMessage = 'current name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ currentName } = await promptAsync({
type: 'text',
name: 'currentName',
message: 'Please enter the current name of the branch to rename:',
validate: value => (value ? true : validationMessage),
}));
}
if (!newName) {
const validationMessage = 'new name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ newName } = await promptAsync({
type: 'text',
name: 'newName',
message: `Please rename ${currentName}`,
validate: value => (value ? true : validationMessage),
}));
}
const editedBranch = await renameUpdateBranchOnAppAsync({
appId: projectId,
name: currentName!,
newName: newName!,
});
if (jsonFlag) {
Log.log(JSON.stringify(editedBranch));
return;
}
Log.withTick(
`️Renamed branch from ${currentName} to ${chalk.bold(
editedBranch.name
)} on project ${chalk.bold(fullName)}.`
);
}
}
@@ -0,0 +1,158 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import chalk from 'chalk';
import Table from 'cli-table3';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { ViewBranchQuery, ViewBranchQueryVariables } from '../../graphql/generated';
import Log from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
import groupBy from '../../utils/expodash/groupBy';
import formatFields from '../../utils/formatFields';
const PAGE_LIMIT = 10_000;
export async function viewUpdateBranchAsync({
appId,
name,
}: Pick<ViewBranchQueryVariables, 'appId' | 'name'>): Promise<ViewBranchQuery> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<ViewBranchQuery, ViewBranchQueryVariables>(
gql`
query ViewBranch($appId: String!, $name: String!, $limit: Int!) {
app {
byId(appId: $appId) {
id
updateBranchByName(name: $name) {
id
name
updates(offset: 0, limit: $limit) {
id
group
message
createdAt
runtimeVersion
platform
manifestFragment
actor {
id
... on User {
username
}
... on Robot {
firstName
}
}
}
}
}
}
}
`,
{
appId,
name,
limit: PAGE_LIMIT,
},
{ additionalTypenames: ['UpdateBranch', 'Update'] }
)
.toPromise()
);
return data;
}
export default class BranchView extends EasCommand {
static hidden = true;
static description = 'View a branch.';
static args = [
{
name: 'name',
required: false,
description: 'Name of the branch to view',
},
];
static flags = {
json: flags.boolean({
description: `return a json with the branch's ID name and recent update groups.`,
default: false,
}),
};
async runAsync(): Promise<void> {
let {
args: { name },
flags: { json: jsonFlag },
} = this.parse(BranchView);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
if (!name) {
const validationMessage = 'Branch name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ name } = await promptAsync({
type: 'text',
name: 'name',
message: 'Please enter the name of the branch to view:',
validate: value => (value ? true : validationMessage),
}));
}
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]
);
if (jsonFlag) {
Log.log(JSON.stringify({ ...UpdateBranch, updates }));
return;
}
const groupTable = new Table({
head: UPDATE_COLUMNS,
wordWrap: true,
});
for (const update of updates) {
groupTable.push([
formatUpdate(update),
update.runtimeVersion,
update.group,
getPlatformsForGroup({
updates: UpdateBranch.updates,
group: update.group,
}),
]);
}
Log.addNewLineIfNone();
Log.log(chalk.bold('Branch:'));
Log.log(
formatFields([
{ label: 'Name', value: UpdateBranch.name },
{ label: 'ID', value: UpdateBranch.id },
])
);
Log.addNewLineIfNone();
Log.log(chalk.bold('Recently published update groups:'));
Log.log(groupTable.toString());
}
}
@@ -0,0 +1,148 @@
import { getConfig } from '@expo/config';
import chalk from 'chalk';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
Build,
BuildStatus,
CancelBuildMutation,
CancelBuildMutationVariables,
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import { ora } from '../../ora';
import { appPlatformEmojis } from '../../platform';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { confirmAsync, selectAsync } from '../../prompts';
async function cancelBuildAsync(buildId: string): Promise<Pick<Build, 'id' | 'status'>> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CancelBuildMutation, CancelBuildMutationVariables>(
gql`
mutation CancelBuildMutation($buildId: ID!) {
build(buildId: $buildId) {
cancel {
id
status
}
}
}
`,
{ buildId }
)
.toPromise()
);
return data.build!.cancel;
}
function formatUnfinishedBuild(
build: Pick<Build, 'id' | 'platform' | 'status' | 'createdAt'>
): string {
const platform = appPlatformEmojis[build.platform];
const startTime = new Date(build.createdAt).toLocaleString();
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}`;
}
async function selectBuildToCancelAsync(
projectId: string,
projectFullName: string
): Promise<string | null> {
const spinner = ora().start('Fetching the uncompleted builds…');
let builds;
try {
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 = [...newBuilds, ...inQueueBuilds, ...inProgressBuilds];
} catch (error) {
spinner.fail(
`Something went wrong and we couldn't fetch the builds for the project ${projectFullName}.`
);
throw error;
}
if (builds.length === 0) {
Log.warn(`There aren't any uncompleted builds for the project ${projectFullName}.`);
return null;
} else if (builds.length === 1) {
Log.log('Found one build');
Log.log(formatUnfinishedBuild(builds[0]));
await confirmAsync({
message: 'Do you want to cancel it?',
});
return builds[0].id;
} else {
const buildId = await selectAsync<string>(
'Which build do you want to cancel?',
builds.map(build => ({
title: formatUnfinishedBuild(build),
value: build.id,
}))
);
return buildId;
}
}
async function ensureBuildExistsAsync(buildId: string): Promise<void> {
try {
await BuildQuery.byIdAsync(buildId);
} catch (err) {
throw new Error(`Couldn't find a build matching the id ${buildId}`);
}
}
export default class BuildCancel extends EasCommand {
static description = 'Cancel a build.';
static args = [{ name: 'BUILD_ID' }];
async runAsync(): Promise<void> {
const { BUILD_ID: buildIdFromArg } = this.parse(BuildCancel).args;
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectFullName = await getProjectFullNameAsync(exp);
if (buildIdFromArg) {
await ensureBuildExistsAsync(buildIdFromArg);
}
const buildId = buildIdFromArg || (await selectBuildToCancelAsync(projectId, projectFullName));
if (!buildId) {
return;
}
const spinner = ora().start('Canceling the build…');
try {
const { status } = await cancelBuildAsync(buildId);
if (status === BuildStatus.Canceled) {
spinner.succeed('Build canceled');
} else {
spinner.text = 'Build is already completed';
spinner.stopAndPersist();
}
} catch (error) {
spinner.fail(`Something went wrong and we couldn't cancel your build ${buildId}`);
throw error;
}
}
}
@@ -1,14 +1,14 @@
import { Command, flags } from '@oclif/command';
import { flags } from '@oclif/command';
import chalk from 'chalk';
import { configureAsync } from '../../build/configure';
import { RequestedPlatform } from '../../build/types';
import log, { learnMore } from '../../log';
import EasCommand from '../../commandUtils/EasCommand';
import Log, { learnMore } from '../../log';
import { RequestedPlatform } from '../../platform';
import { findProjectRootAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions';
export default class BuildConfigure extends Command {
export default class BuildConfigure extends EasCommand {
static description = 'Configure the project to support EAS Build.';
static flags = {
@@ -17,44 +17,29 @@ 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() {
async runAsync(): Promise<void> {
const { flags } = this.parse(BuildConfigure);
log(
Log.log(
'💡 The following process will configure your iOS and/or Android project to be compatible with EAS Build. These changes only apply to your local project files and you can safely revert them at any time.'
);
log.newLine();
Log.newLine();
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(),
projectDir: await findProjectRootAsync(),
});
log.newLine();
Log.newLine();
logSuccess(platform);
}
}
function logSuccess(platform: RequestedPlatform) {
function logSuccess(platform: RequestedPlatform): void {
let platformsText = 'iOS and Android projects are';
let storesText = 'the Apple App Store or Google Play Store';
@@ -66,14 +51,13 @@ function logSuccess(platform: RequestedPlatform) {
storesText = 'the Apple App Store';
}
log(`🎉 Your ${platformsText} ready to build.
Log.log(`🎉 Your ${platformsText} ready to build.
- 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',
'Learn more about other capabilities of EAS Build'
)}`);
- ${learnMore('https://docs.expo.dev/build/introduction', {
learnMoreMessage: 'Learn more about other capabilities of EAS Build',
})}`);
}
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
@@ -88,7 +72,7 @@ async function promptForPlatformAsync(): Promise<RequestedPlatform> {
},
{
title: 'iOS',
value: RequestedPlatform.iOS,
value: RequestedPlatform.Ios,
},
{
title: 'Android',
+406 -171
View File
@@ -1,218 +1,453 @@
import { Command, flags } from '@oclif/command';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, EasJsonReader } from '@expo/eas-json';
import { flags } from '@oclif/command';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import figures from 'figures';
import fs from 'fs-extra';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import nullthrows from 'nullthrows';
import { apiClient } from '../../api';
import { configureAsync } from '../../build/configure';
import { createCommandContextAsync } from '../../build/context';
import { buildAsync } from '../../build/create';
import { AnalyticsEvent, Build as BuildType, RequestedPlatform } from '../../build/types';
import Analytics from '../../build/utils/analytics';
import formatBuild from '../../build/utils/formatBuild';
import { isGitStatusCleanAsync } from '../../build/utils/repository';
import { AppPlatform } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import log, { learnMore } from '../../log';
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 { ensureExpoDevClientInstalledForDevClientBuildsAsync } from '../../build/utils/devClient';
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
import { ensureRepoIsCleanAsync, reviewAndCommitChangesAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { CredentialsContext } from '../../credentials/context';
import {
AppPlatform,
BuildFragment,
BuildStatus,
SubmissionFragment,
} from '../../graphql/generated';
import { toAppPlatform, toPlatform } from '../../graphql/types/AppPlatform';
import Log, { learnMore, link } from '../../log';
import {
RequestedPlatform,
appPlatformDisplayNames,
appPlatformEmojis,
selectRequestedPlatformAsync,
toPlatforms,
} from '../../platform';
import {
EAS_UNAVAILABLE_MESSAGE,
isEasEnabledForProjectAsync,
warnEasUnavailable,
} from '../../project/isEasEnabledForProject';
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
import { findProjectRootAsync } from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
import { createSubmissionContextAsync } from '../../submit/context';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { confirmAsync, promptAsync } from '../../prompts';
import { Actor } from '../../user/User';
import { ensureLoggedInAsync } from '../../user/actions';
submitAsync,
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../../submit/submit';
import { printSubmissionDetailsUrls } from '../../submit/utils/urls';
import { easCliVersion } from '../../utils/easCli';
import { enableJsonOutput } from '../../utils/json';
import { ProfileData, getProfilesAsync } from '../../utils/profiles';
import { getVcsClient, setVcsClient } from '../../vcs';
import GitClient from '../../vcs/clients/git';
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;
'auto-submit': boolean;
'auto-submit-with-profile'?: string;
}
interface BuildFlags {
requestedPlatform: RequestedPlatform;
skipProjectConfiguration: boolean;
profile: string | null;
nonInteractive: boolean;
local: boolean;
wait: boolean;
clearCache: boolean;
json: boolean;
autoSubmit: boolean;
submitProfile?: string;
}
export default class Build extends EasCommand {
static description = 'start a build';
static flags = {
platform: flags.enum({ char: 'p', options: ['android', 'ios', 'all'] }),
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,
description: 'Skip project configuration',
}),
profile: flags.string({
default: 'release',
description: 'Name of the build profile from eas.json',
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
}),
'non-interactive': flags.boolean({
default: false,
description: 'Run command in --non-interactive mode',
description: 'Run command in non-interactive mode',
}),
local: flags.boolean({
default: false,
description: 'Run build locally [experimental]',
}),
wait: flags.boolean({
default: true,
allowNo: true,
description: 'Wait for build(s) to complete',
}),
'clear-cache': flags.boolean({
default: false,
description: 'Clear cache before the build',
}),
'auto-submit': flags.boolean({
default: false,
description:
'Submit on build complete using the submit profile with the same name as the build profile',
exclusive: ['auto-submit-with-profile'],
}),
'auto-submit-with-profile': flags.string({
description: 'Submit on build complete using the submit profile with provided name',
helpValue: 'PROFILE_NAME',
exclusive: ['auto-submit'],
}),
};
async run(): Promise<void> {
const { flags } = this.parse(Build);
const user = await ensureLoggedInAsync();
private metroConfigValidated = false;
async runAsync(): Promise<void> {
const { flags: rawFlags } = this.parse(Build);
if (rawFlags.json) {
enableJsonOutput();
}
const flags = await this.sanitizeFlagsAsync(rawFlags);
const { requestedPlatform } = flags;
const projectDir = await findProjectRootAsync();
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await getVcsClient().ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
const platforms = toPlatforms(requestedPlatform);
const easJsonReader = new EasJsonReader(projectDir);
const buildProfiles = await getProfilesAsync({
platforms,
profileName: flags.profile,
async readProfileAsync(platform, profileName) {
return await easJsonReader.readBuildProfileAsync(platform, profileName);
},
});
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir,
nonInteractive: flags.nonInteractive,
buildProfiles,
});
const startedBuilds: {
build: BuildFragment;
buildProfile: ProfileData<BuildProfile>;
}[] = [];
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
for (const buildProfile of buildProfiles) {
const { build: maybeBuild, buildCtx } = await this.prepareAndStartBuildAsync({
projectDir,
flags,
moreBuilds: platforms.length > 1,
buildProfile,
});
if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile });
}
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
}
if (flags.local) {
return;
}
Log.newLine();
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
Log.newLine();
const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) {
for (const startedBuild of startedBuilds) {
const submission = await this.prepareAndStartSubmissionAsync({
build: startedBuild.build,
credentialsCtx: nullthrows(
buildCtxByPlatform[startedBuild.build.platform]?.credentialsCtx
),
flags,
moreBuilds: startedBuilds.length > 1,
projectDir,
buildProfile: startedBuild.buildProfile,
});
submissions.push(submission);
}
Log.newLine();
printSubmissionDetailsUrls(submissions);
Log.newLine();
}
if (!flags.wait) {
return;
}
const builds = await waitForBuildEndAsync(startedBuilds.map(({ build }) => build.id));
printBuildResults(builds, flags.json);
const haveAllBuildsFailedOrCanceled = builds.every(
build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status)
);
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
this.exitWithNonZeroCodeIfSomeBuildsFailed(builds);
} else {
// the following function also exits with non zero code if any of the submissions failed
await waitForSubmissionsToCompleteAsync(submissions);
}
}
private async sanitizeFlagsAsync(flags: RawBuildFlags): Promise<BuildFlags> {
const nonInteractive = flags['non-interactive'];
if (!flags.platform && nonInteractive) {
throw new Error('--platform is required when building in non-interactive mode');
error('--platform is required when building in non-interactive mode', { exit: 1 });
}
const platform =
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const projectId = await getProjectIdAsync(projectDir);
if (!(await isEasEnabledForProjectAsync(projectId))) {
warnEasUnavailable();
process.exitCode = 1;
return;
if (flags.json && !nonInteractive) {
error('--json is allowed only when building in non-interactive mode', { exit: 1 });
}
const accountName = await getProjectAccountNameAsync(projectDir);
const accountHasPendingBuilds = await ensureNoPendingBuildsExistAsync({
accountName,
platform,
projectId,
user,
});
if (accountHasPendingBuilds) {
return;
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
if (flags.local) {
if (flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined) {
// TODO: implement this
error('Auto-submits are not yet supported when building locally', { exit: 1 });
}
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 });
}
}
await ensureProjectConfiguredAsync(projectDir);
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 trackingCtx = {
tracking_id: uuidv4(),
requested_platform: flags.platform,
};
Analytics.logEvent(AnalyticsEvent.BUILD_COMMAND, trackingCtx);
const commandCtx = await createCommandContextAsync({
requestedPlatform: platform,
profile: flags.profile,
projectDir,
projectId,
trackingCtx,
nonInteractive,
skipCredentialsCheck: flags['skip-credentials-check'],
const profile = flags['profile'];
return {
requestedPlatform,
skipProjectConfiguration: flags['skip-project-configuration'],
waitForBuildEnd: flags.wait,
});
await buildAsync(commandCtx);
}
}
async function ensureNoPendingBuildsExistAsync({
user,
platform,
accountName,
projectId,
}: {
user: Actor;
platform: RequestedPlatform;
accountName: string;
projectId: string;
}): Promise<boolean> {
// allow expo admins to run as many builds as they wish
if (user.isExpoAdmin) {
return false;
profile: profile ?? null,
nonInteractive,
local: flags['local'],
wait: flags['wait'],
clearCache: flags['clear-cache'],
json: flags['json'],
autoSubmit: flags['auto-submit'],
submitProfile: flags['auto-submit-with-profile'] ?? profile,
};
}
const appPlatforms = toAppPlatforms(platform);
const maybePendingBuilds = await Promise.all(
appPlatforms.map(appPlatform => BuildQuery.getPendingBuildIdAsync(accountName, appPlatform))
);
const pendingBuilds = maybePendingBuilds.filter(i => i !== null);
if (pendingBuilds.length > 0) {
log.newLine();
log.error(
'Your other builds are still pending. Wait for them to complete before running this command again.'
);
log.newLine();
const results = await Promise.all(
pendingBuilds.map(pendingBuild => {
return apiClient.get<{ data: BuildType }>(
`projects/${projectId}/builds/${pendingBuild?.id}`,
{
responseType: 'json',
}
);
})
);
for (const result of results) {
log(formatBuild(result.body.data, { accountName }));
}
process.exitCode = 1;
return true;
}
return false;
}
function toAppPlatforms(requestedPlatform: RequestedPlatform): AppPlatform[] {
if (requestedPlatform === RequestedPlatform.All) {
return [AppPlatform.Android, AppPlatform.Ios];
} else if (requestedPlatform === RequestedPlatform.Android) {
return [AppPlatform.Android];
} else {
return [AppPlatform.Ios];
}
}
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
const { platform } = await promptAsync({
type: 'select',
message: 'Build for platforms',
name: 'platform',
choices: [
{
title: 'All',
value: RequestedPlatform.All,
},
{
title: 'iOS',
value: RequestedPlatform.iOS,
},
{
title: 'Android',
value: RequestedPlatform.Android,
},
],
});
return platform;
}
async function ensureProjectConfiguredAsync(projectDir: string): Promise<void> {
if (await fs.pathExists(path.join(projectDir, 'eas.json'))) {
return;
}
const confirm = await confirmAsync({
message: 'This app is not set up for building with EAS. Set it up now?',
});
if (confirm) {
await configureAsync({
private async prepareAndStartBuildAsync({
projectDir,
flags,
moreBuilds,
buildProfile,
}: {
projectDir: string;
flags: BuildFlags;
moreBuilds: boolean;
buildProfile: ProfileData<BuildProfile>;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName,
clearCache: flags.clearCache,
buildProfile: buildProfile.profile,
local: flags.local,
nonInteractive: flags.nonInteractive,
platform: buildProfile.platform,
projectDir,
platform: RequestedPlatform.All,
allowExperimental: false,
skipProjectConfiguration: flags.skipProjectConfiguration,
});
if (!(await isGitStatusCleanAsync())) {
throw new Error(
'Build process requires clean git working tree, please commit all your changes and run `eas build` again'
if (moreBuilds) {
Log.newLine();
const appPlatform = toAppPlatform(buildProfile.platform);
Log.log(
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
`${appPlatformDisplayNames[appPlatform]} build`
)}`
);
}
} else {
throw new Error(
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
'https://docs.expo.io/build/eas-json'
)})`
if (buildCtx.workflow === Workflow.MANAGED && !this.metroConfigValidated) {
await validateMetroConfigForManagedWorkflowAsync(buildCtx);
this.metroConfigValidated = true;
}
if (!buildCtx.local && !(await isEasEnabledForProjectAsync(buildCtx.projectId))) {
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
}
const build = await this.startBuildAsync(buildCtx);
return {
build,
buildCtx,
};
}
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 async prepareAndStartSubmissionAsync({
build,
credentialsCtx,
flags,
moreBuilds,
projectDir,
buildProfile,
}: {
build: BuildFragment;
credentialsCtx: CredentialsContext;
flags: BuildFlags;
moreBuilds: boolean;
projectDir: string;
buildProfile: ProfileData<BuildProfile>;
}): Promise<SubmissionFragment> {
const easJsonReader = new EasJsonReader(projectDir);
const platform = toPlatform(build.platform);
const submitProfile = await easJsonReader.readSubmitProfileAsync(platform, flags.submitProfile);
const submissionCtx = await createSubmissionContextAsync({
platform,
projectDir,
projectId: build.project.id,
profile: submitProfile,
archiveFlags: { id: build.id },
nonInteractive: flags.nonInteractive,
env: buildProfile.profile.env,
credentialsCtx,
});
if (moreBuilds) {
Log.newLine();
Log.log(
`${appPlatformEmojis[build.platform]} ${chalk.bold(
`${appPlatformDisplayNames[build.platform]} submission`
)}`
);
}
return await submitAsync(submissionCtx);
}
private 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);
}
}
}
export async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
if (!(await fs.pathExists(easJsonPath))) {
return;
}
const easJsonReader = new EasJsonReader(projectDir);
const rawEasJson = await easJsonReader.readRawAsync();
if (rawEasJson?.cli) {
return;
}
if (nonInteractive) {
Log.warn(
`${
figures.warning
} Action required: the default behavior of EAS CLI has changed and your eas.json must be updated to remove ambiguity around which Git integration workflow to use. Refer to ${link(
'https://expo.fyi/eas-vcs-workflow'
)} for more information.`
);
Log.warn(
'This warning will become an error in an upcoming EAS CLI release. For now, we will proceed with the old default behavior to avoid disruption of your builds.'
);
setVcsClient(new GitClient());
return;
}
Log.log(
`${chalk.bold(
'eas-cli@>=0.34.0 no longer requires that you commit changes to Git before starting a build.'
)} ${learnMore('https://expo.fyi/eas-vcs-workflow')}`
);
Log.log(
`If you want to continue using the Git integration, you can opt in with ${chalk.bold(
'cli.requireCommit'
)} in ${chalk.bold('eas.json')} or with the following prompt.`
);
Log.newLine();
const mode = await selectAsync('Select your preferred Git integration', [
{ title: 'Require changes to be committed in Git (old default)', value: 'requireCommit' },
{ title: 'Allow builds with dirty Git working tree (new default)', value: 'noCommit' },
]);
if (mode === 'requireCommit') {
setVcsClient(new GitClient());
await ensureRepoIsCleanAsync(nonInteractive);
}
rawEasJson.cli =
mode === 'requireCommit'
? { version: `>= ${easCliVersion}`, requireCommit: true }
: { version: `>= ${easCliVersion}` };
await fs.writeJSON(easJsonPath, rawEasJson, { spaces: 2 });
Log.withTick('Updated eas.json');
if (mode === 'requireCommit') {
await getVcsClient().trackFileAsync(easJsonPath);
await reviewAndCommitChangesAsync('Set cli.requireCommit to true in eas.json', {
nonInteractive,
});
}
}
+134 -30
View File
@@ -1,50 +1,108 @@
import { Command, flags } from '@oclif/command';
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import chalk from 'chalk';
import ora from 'ora';
import { apiClient } from '../../api';
import { Build } from '../../build/types';
import formatBuild from '../../build/utils/formatBuild';
import log from '../../log';
import { BuildDistributionType, BuildStatus } from '../../build/types';
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
import EasCommand from '../../commandUtils/EasCommand';
import {
AppPlatform,
DistributionType,
BuildStatus as GraphQLBuildStatus,
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class BuildList extends Command {
export default class BuildList extends EasCommand {
static description = 'list all builds for your project';
static flags = {
platform: flags.enum({ options: ['all', 'android', 'ios'] }),
status: flags.enum({ options: ['in-queue', 'in-progress', 'errored', 'finished'] }),
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,
BuildStatus.FINISHED,
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, status, limit = 10 } = this.parse(BuildList).flags;
async runAsync(): Promise<void> {
const { flags } = this.parse(BuildList);
const {
json,
platform: requestedPlatform,
status: buildStatus,
distribution: buildDistribution,
limit = 10,
} = flags;
if (json) {
enableJsonOutput();
}
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const projectId = await getProjectIdAsync(projectDir);
const accountName = await getProjectAccountNameAsync(projectDir);
const projectName = await getProjectFullNameAsync(projectDir);
const platform = toAppPlatform(requestedPlatform);
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectName = await getProjectFullNameAsync(exp);
const spinner = ora().start('Fetching the build list for the project…');
try {
const response = await apiClient.get<{ data: { builds: Build[] } }>(
`projects/${projectId}/builds`,
{
searchParams: { platform, status, limit },
responseType: 'json',
}
);
const { builds } = response.body.data;
const builds = await BuildQuery.allForAppAsync(projectId, {
limit,
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) {
if (platform || status) {
if (platform || graphqlBuildStatus) {
spinner.succeed(
`Showing ${builds.length} matching builds for the project ${projectName}`
);
@@ -52,11 +110,15 @@ export default class BuildList extends Command {
spinner.succeed(`Showing last ${builds.length} builds for the project ${projectName}`);
}
const list = builds
.map(build => formatBuild(build, { accountName }))
.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(`\n${list}`);
Log.log(`\n${list}`);
}
} else {
spinner.fail(`Couldn't find any builds for the project ${projectName}`);
}
@@ -66,3 +128,45 @@ export default class BuildList extends Command {
}
}
}
const toAppPlatform = (requestedPlatform?: RequestedPlatform): AppPlatform | undefined => {
if (!requestedPlatform || requestedPlatform === RequestedPlatform.All) {
return undefined;
} else if (requestedPlatform === RequestedPlatform.Android) {
return AppPlatform.Android;
} else {
return AppPlatform.Ios;
}
};
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) {
return GraphQLBuildStatus.InProgress;
} else if (buildStatus === BuildStatus.ERRORED) {
return GraphQLBuildStatus.Errored;
} else if (buildStatus === BuildStatus.FINISHED) {
return GraphQLBuildStatus.Finished;
} else {
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;
}
};
+50 -55
View File
@@ -1,55 +1,58 @@
import { Command } from '@oclif/command';
import { HTTPError } from 'got';
import ora from 'ora';
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { apiClient } from '../../api';
import { Build } from '../../build/types';
import formatBuild from '../../build/utils/formatBuild';
import log from '../../log';
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
import EasCommand from '../../commandUtils/EasCommand';
import { BuildFragment } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import { ora } from '../../ora';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class BuildView extends Command {
export default class BuildView extends EasCommand {
static description = 'view a build for your project';
static args = [{ name: 'buildId' }];
static args = [{ name: 'BUILD_ID' }];
async run() {
const { buildId } = this.parse(BuildView).args;
static flags = {
json: flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
}),
};
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const projectId = await getProjectIdAsync(projectDir);
const accountName = await getProjectAccountNameAsync(projectDir);
const projectName = await getProjectFullNameAsync(projectDir);
async runAsync(): Promise<void> {
const {
args: { BUILD_ID: buildId },
flags,
} = this.parse(BuildView);
if (flags.json) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectName = await getProjectFullNameAsync(exp);
const spinner = ora().start('Fetching the build…');
try {
let build: Build;
let build: BuildFragment;
if (buildId == null) {
const response = await apiClient.get<{ data: { builds: Build[] } }>(
`projects/${projectId}/builds`,
{
searchParams: { limit: 1 },
responseType: 'json',
}
);
build = response.body.data.builds[0];
if (buildId) {
build = await BuildQuery.byIdAsync(buildId);
} else {
const response = await apiClient.get<{ data: Build }>(
`projects/${projectId}/builds/${buildId}`,
{
responseType: 'json',
}
);
build = response.body.data;
const builds = await BuildQuery.allForAppAsync(projectId, { limit: 1 });
if (builds.length === 0) {
spinner.fail(`Couldn't find any builds for the project ${projectName}`);
return;
}
build = builds[0];
}
if (buildId) {
@@ -58,29 +61,21 @@ export default class BuildView extends Command {
spinner.succeed(`Showing the last build for the project ${projectName}`);
}
log(`\n${formatBuild(build, { accountName })}`);
} catch (e) {
const error = e as HTTPError;
if (error.response.statusCode === 400) {
if (buildId) {
spinner.fail(
`Couldn't find a build for the project ${projectName} matching the id ${buildId}`
);
} else {
spinner.fail(`Couldn't find any builds for the project ${projectName}`);
}
if (flags.json) {
printJsonOnlyOutput(build);
} else {
if (buildId) {
spinner.fail(`Something went wrong and we couldn't fetch the build with id ${buildId}`);
} else {
spinner.fail(
`Something went wrong and we couldn't fetch the last build for the project ${projectName}`
);
}
Log.log(`\n${formatGraphQLBuild(build)}`);
}
} catch (err) {
if (buildId) {
spinner.fail(`Something went wrong and we couldn't fetch the build with id ${buildId}`);
} else {
spinner.fail(
`Something went wrong and we couldn't fetch the last build for the project ${projectName}`
);
}
throw error;
throw err;
}
}
}
Loaded 100 of 563 files, more files were not shown because too many files have changed in this diff. Show more