Compare commits

...
Author SHA1 Message Date
Dominik Sokal 64efdad61d v0.42.1 2021-12-21 14:03:49 +01:00
Dominik Sokal 8bc07ca452 fix release process (#864)
* fix release process

* run learna version with --exact

* fix build-for-size-check
2021-12-21 14:02:03 +01:00
Dominik Sokal ea4d28e7d9 v0.42.0 2021-12-21 11:22:21 +01:00
Dominik Sokal e1fce4a066 upgrade oclif deps (#858) 2021-12-21 10:42:09 +01:00
jkh 76d75e9c17 [eas-cli] allow eas-cli to be run outside of expo projects (#851) 2021-12-17 09:25:11 -08:00
Dominik Sokal 9278b3c17d use default for function argument 2021-12-17 10:15:11 +01:00
jkh ef9fbe84c0 [eas-cli] clean up eas update logs (#850) 2021-12-16 12:22:05 -08:00
Expo CI b3a69e8863 update CHANGELOG.md 2021-12-16 17:58:52 +00:00
jonsamp 5e4663ac44 v0.41.1 2021-12-16 12:53:18 -05:00
jkh ca18a555c1 [eas-cli] support staging and local Update URLs (#849) 2021-12-16 07:14:11 -08:00
Jon Samp c446bbb016 [eas-cli] Make all EAS Update related commands public (#854) 2021-12-16 09:34:49 -05:00
Dominik Sokal d5254434c6 print expo-updates version in eas diagnostics 2021-12-16 09:58:40 +01:00
Bartłomiej Klocek e2277b9482 [eas-cli] Fix ASC prompt displayed in --non-interactive (#841)
* [eas-cli] Fix ASC prompt displayed in --non-interactive

* Update CHANGELOG.md

* Apply feedback - fail in non-interactive
2021-12-15 13:28:59 +01:00
Test User 739502aef9 [eas-cli] fix typo in branch:delete description 2021-12-13 17:26:36 -05:00
Expo CI de42624992 update CHANGELOG.md 2021-12-13 15:44:24 +00:00
Wojciech Kozyra 0ccfbe5dd6 v0.41.0 2021-12-13 16:19:53 +01:00
Wojciech Kozyra 3ffad12681 upgrade eas-cli-local-build-plugin to 0.0.56 2021-12-13 16:19:40 +01:00
Wojciech Kozyra 565867b5a8 Update CHANGELOG.md 2021-12-13 15:16:51 +01:00
Wojciech KozyraandDominik Sokal 303cc82a06 [eas-cli] add build inspect command (#834)
* [eas-cli] add build inspect command

* Apply suggestions from code review

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

* Apply suggestions from code review

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

* review feedback

Co-authored-by: Dominik Sokal <[email protected]>
2021-12-13 14:56:25 +01:00
Dominik Sokal 4c2abc0693 fix submission url (#845)
* fix submission url

* update CHANGELOG.md
2021-12-13 14:41:19 +01:00
Dominik SokalandWojciech Kozyra b63167fbae [ENG-2052] ask user to select profile if release does not exist (#829)
* ask user to select profile if release does not exist

* Apply suggestions from code review

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

* update CHANGELOG.md

Co-authored-by: Wojciech Kozyra <[email protected]>
2021-12-13 11:00:09 +01:00
Dominik Sokal 8adb6434bf [ENG-2279] warn when attempting to use EAS CLI on managed SDK < 41 project (#833)
* warn when attempting to use EAS CLI on managed SDK < 41 project

* update CHANGELOG.md

* Update CHANGELOG.md
2021-12-13 09:48:00 +01:00
Dominik Sokal 4c9e407fef [eas-json] use semver for version validation (#832)
* [eas-json] use semver for version validation

* update CHANGELOG.md
2021-12-13 09:44:44 +01:00
jkh ef566b3f9c [eas-cli] fix missing republish message (#839) 2021-12-12 15:34:22 -05:00
jkh 895231ea98 [eas-cli] fix table column names (#840) 2021-12-12 15:34:13 -05:00
jkh c7000a754e [eas-cli] Make default runtime version 1.0.0 for bare workflow (#838) 2021-12-12 15:34:04 -05:00
jkh 9ac8404681 [eas-cli] handle 'eas update' when there are no branches (#837) 2021-12-10 15:18:48 -05:00
Expo CI 6c0b7d5951 update CHANGELOG.md 2021-12-08 20:59:10 +00:00
jonsamp ddfe60a7b0 v0.40.0 2021-12-08 15:43:11 -05:00
jkh 7dedeb904b [eas-cli] change eas update:publish to eas update (#830)
* [eas-cli] change eas update:publish to eas update

* update CHANGELOG.md

* grammar
2021-12-08 15:15:25 -05:00
Quinlan Jung 4c02e0102b [eas-cli] pass build profile env to credentials ctx (#828)
* [eas-cli] pass build profile env to credentials ctx

* update CHANGELOG.md

* pr feedback

* oops
2021-12-08 11:15:04 -08:00
kgc00 e91516fcc7 [eas-cli] removes account name from tracking context (#827) 2021-12-08 10:15:46 -08:00
jkh d01f6c0d8e [eas-cli] ask for publish message before staring bundling (#826) 2021-12-08 11:13:26 -05:00
jkh 1ce69cd7c9 [eas-cli] rename branch:publish to update:publish (#818) 2021-12-06 11:38:20 -05:00
Expo CI 83c10793ac update CHANGELOG.md 2021-12-06 11:47:10 +00:00
Wojciech Kozyra a344c11ba1 v0.39.0 2021-12-06 12:41:24 +01:00
Wojciech Kozyra c7d5a9a819 upgrade eas-cli-local-build-plugin to 0.0.54 2021-12-06 12:41:08 +01:00
Wojciech KozyraandDominik Sokal 2f43ed94ab detect react-native version + update eas-build-job (#823)
* detect react-native version + update eas-build-job

* update CHANGELOG.md

* Apply suggestions from code review

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

* update packages

Co-authored-by: Dominik Sokal <[email protected]>
2021-12-06 12:38:29 +01:00
Dominik Sokal 4a4ebf3cd6 print project workflow in eas diagnostics + update bug report template - add manged/bare input (#822)
* update bug report template - add manged/bare input

* print project workflow in eas diagnostics

* update CHANGELOG.md
2021-12-06 12:05:20 +01:00
Dominik Sokal 18b832dbda fix setting up credentials for projects with Apple Watch companion apps (#812)
* fix setting up credentials for projects with Apple Watch companion apps

* update CHANGELOG.md
2021-12-06 11:37:39 +01:00
Dominik Sokal 1200e7fecd upgrade @expo/* deps (#821)
* upgrade @expo/* deps

* update CHANGELOG.md
2021-12-06 10:50:44 +01:00
Dominik Sokal 3cc6b73d3a make iOS submission message less scary: App Store -> App Store Connect (#815) 2021-12-06 09:41:46 +01:00
jkh 4831ff186a [eas-cli] bump config-plugins to 4.0.11 (#817) 2021-12-03 16:26:00 -05:00
Wojciech Kozyra a2d60a4a76 [eas-cli] improve error message for missing ascAppId (#813)
* [eas-cli] improve error message for missing ascAppId

* update CHANGELOG.md

* Update packages/eas-cli/src/submit/ios/IosSubmitCommand.ts
2021-12-03 09:03:11 +01:00
jkh 9f604777b9 [eas-cli] set runtime version in eas update:configure (#811) 2021-12-02 09:15:26 -05:00
Dominik Sokal fce9bda9bb [changelog-bot] revert checkout action back to v2 2021-12-02 11:27:00 +01:00
Dominik Sokal 4cc6745a0d [changelog-bot] revert setup-node action back to v2; use checkout@v1 2021-12-02 11:22:17 +01:00
Dominik Sokal 67f0b806f4 [changelog-bot] revert setup-node action to v1 2021-12-02 11:11:42 +01:00
Dominik Sokal 9280dfb3ad [changelog-bot] always capitalize message 2021-12-02 09:36:12 +01:00
jkh 2befbb0eba [eas-cli] fix typos in eas channel:view logging (#810) 2021-12-01 12:53:55 -05:00
Wojciech Kozyra 41359b7194 [eas-cli] pass bundle identifier when resolving asc app id (#809)
* [eas-cli] pass bundle identifier when resolving asc app id

* update CHANGELOG.md
2021-12-01 16:32:55 +01:00
jkh c57861a67d [eas-cli] add missing @expo/sdk-runtime-versions dependency (#808) 2021-11-30 13:19:55 -05:00
Expo CI d31ece8793 update CHANGELOG.md 2021-11-29 23:11:26 +00:00
Brent Vatne 4f7148b756 v0.38.3 2021-11-29 15:04:37 -08:00
Brent Vatne bc8a781d86 [eas-cli] upgrade @expo/* deps (#806) 2021-11-29 14:49:44 -08:00
jkh 153a70fb85 [eas-cli] add eas update:configure (#800) 2021-11-29 07:58:02 -05:00
Wojciech Kozyra 2f93492fc3 [eas-cli] print warning about outdated version to stderr (#805)
* print warning about outdated version to stderr

* update CHANGELOG.md
2021-11-29 11:23:31 +01:00
Dominik Sokal 0b39ff1457 update README.md 2021-11-29 10:15:39 +01:00
Kim BrandwijkandDominik Sokal f6817037ae docs: fix broken links to all command source files in readme (#791)
* docs: fix broken links to all command source files in readme

They pointed to .js, but they're all .ts

* docs: fix command paths by adding typescript devdependency

* fix: add typescript devDependency to eas-json for consistency

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-29 10:13:53 +01:00
Expo CI 216f97ea8a update CHANGELOG.md 2021-11-26 22:01:36 +00:00
Brent Vatne 11c194a168 v0.38.2 2021-11-26 13:50:29 -08:00
Brent Vatne 3c74c111a7 Update yarn.lock 2021-11-26 13:41:40 -08:00
Brent Vatne b69b8b87a7 [eas-cli] upgrade @expo/* deps (#801) 2021-11-26 13:40:58 -08:00
jkh 825baa9f68 [eas-cli] check EAS Update URL is correctly configured before publish (#789) 2021-11-26 16:23:14 -05:00
Dominik Sokal 9f5736cfaa fix lint error 2021-11-26 14:39:10 +01:00
Dominik Sokal ae24ffbe86 fix getProfilesAsync tests 2021-11-26 14:23:08 +01:00
Dominik Sokal 487316370b refactor @expo/eas-json (#794)
* refactor @expo/eas-json

* update CHANGELOG.md
2021-11-26 12:35:49 +01:00
Dominik Sokal 8bc3565270 fix evaluating template strings from info.plist (#798)
* fix evaluating template strings from info.plist

* update CHANGELOG.md
2021-11-26 11:41:57 +01:00
Expo CI 07450f6bf7 update CHANGELOG.md 2021-11-25 09:34:42 +00:00
Dominik Sokal 71d48c3cf2 v0.38.1 2021-11-25 10:29:10 +01:00
Dominik Sokal 7343e81065 upgrade @expo/* deps (#792)
* upgrade @expo/* deps

* update CHANGELOG.md

* fix import
2021-11-25 10:25:05 +01:00
Expo CI 1f1310bef6 update CHANGELOG.md 2021-11-24 13:56:49 +00:00
Dominik Sokal 27c69ec8e1 v0.38.0 2021-11-24 14:50:06 +01:00
Wojciech Kozyra b81b896bc6 [eas-cli] pass identifier from build to submit (#780)
* pass identifier from build to submit

* update CHANGELOG.md

* review feedback
2021-11-24 14:02:25 +01:00
Dominik Sokal ef79db2ca5 clean up .gitignore (#788) 2021-11-24 13:53:19 +01:00
Dominik Sokal 88565eec62 implement submit webhooks (#777)
* implement submit webhooks

* update CHANGELOG.md
2021-11-24 10:43:25 +01:00
Dominik SokalandBrent Vatne e8a520e55f better error message when git is not installed (#784)
* better error message when git is not installed

* update CHANGELOG.md

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

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

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

Co-authored-by: Brent Vatne <[email protected]>
2021-11-24 10:42:17 +01:00
Dominik Sokal 9a33f7bd93 check npm version when building locally (#787)
* check npm version when building locally

* update CHANGELOG.md
2021-11-24 10:16:54 +01:00
kgc00 23c70e237b bump rudder node sdk (#786) 2021-11-24 09:23:16 +01:00
jkh 7fcf41db78 [eas-cli] fix bug that required runtime version for all builds (#785) 2021-11-23 15:25:44 -05:00
Evan BaconandDominik Sokal 36f4a4bd38 Improve unknown error message from #740 (#775)
* Improve unknown error message from #740

* Update CHANGELOG.md

* Update bundleIdCapabilities.ts

* Update packages/eas-cli/src/credentials/ios/appstore/bundleIdCapabilities.ts

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-23 13:22:12 -07:00
jkh 87541db80d [eas-cli] compute the runtime version for a build's metadata (#783) 2021-11-23 09:36:37 -05:00
Dominik Sokal 80112c682f upgrade typescript to 4.5.2 (#781)
* upgrade typescript to 4.5.2

* bump oclif deps

* update CHANGELOG.md
2021-11-22 16:30:02 +01:00
Expo CI eb29d3dadb update CHANGELOG.md 2021-11-18 09:51:37 +00:00
Dominik Sokal 558c367038 v0.37.0 2021-11-18 10:42:38 +01:00
Dominik Sokal 4b88d00379 upgrade eas-cli-local-build-plugin to 0.0.50 2021-11-18 10:42:16 +01:00
Dominik Sokal aad3b5ccd5 interactively configure git user.name and user.email (#772)
* interactively configure git user.name and user.email

* update CHANGELOG.md

* address PR feedback
2021-11-18 10:30:51 +01:00
Quinlan Jung 8b50806093 [eas-cli] make the missing eas.json messages better (#770)
* [eas-cli] make the missing eas.json messages better

* make error upstream

* revert

* fix tests and stuff

* update CHANGELOG.md
2021-11-17 13:39:44 -08:00
Dominik Sokal 45c9bda2e9 remove eas availability checks (#771) 2021-11-17 13:26:36 +01:00
Dominik Sokal 23237210d1 Update CHANGELOG.md 2021-11-17 09:58:36 +01:00
Dominik Sokal e34382e6a3 show plist path when parsing failed (#768)
* show plist path when parsing failed

* Update packages/eas-cli/src/build/ios/version.ts
2021-11-17 09:57:59 +01:00
jkh b8ad2ee29a [eas-cli] auto create channel on publish (#766) 2021-11-16 13:23:39 -05:00
Wojciech KozyraandDominik Sokal 6bd62bac64 [eas-cli] read correct identifiers for multi-scheme/multi-flavor projects (#765)
* [eas-cli] read correct appIdentifier for multi-scheme/multi-flavor project

* review feedback

* Update CHANGELOG.md

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-16 15:52:39 +01:00
Dominik Sokal 8b959fc6d8 make the missing build/submit profile error message more descriptive (#761) 2021-11-15 20:04:34 +01:00
Bartosz Kaszubowski 6e615e20d6 [workflows] update setup-node action to v2 (#763) 2021-11-15 19:59:29 +01:00
Bartosz Kaszubowski efbc5fc4bc [workspace] update Prettier (#762) 2021-11-15 19:59:12 +01:00
Dominik Sokal 79c77ae835 validate release channel in eas.json (#764)
* validate release channel in eas.json

* update CHANGELOG.md
2021-11-15 16:52:12 +01:00
Expo CI 618b6b1caf update CHANGELOG.md 2021-11-15 11:50:31 +00:00
Dominik Sokal 864658df80 v0.36.1 2021-11-15 12:26:39 +01:00
Dominik Sokal 4510e86b64 update CHANGELOG.md 2021-11-15 12:24:53 +01:00
Dominik Sokal 4af4490abc upgrade eas-cli-local-build-plugin to 0.0.48 2021-11-15 12:23:18 +01:00
Expo CI 37c4040203 update CHANGELOG.md 2021-11-15 10:48:35 +00:00
Dominik Sokal 55f9be3b5c v0.36.0 2021-11-15 11:39:06 +01:00
Dominik Sokal 152b25b93c upgrade eas-cli-local-build-plugin to 0.0.47 2021-11-15 11:38:52 +01:00
Dominik Sokal ea0d0bb4dc pin eas-cli-local-build-plugin to specific version (#753)
* automate eas-cli-local-build-plugin upgrades

* update PR template, CHANGELOG, CONTRIBUTING guide

* update CHANGELOG.md

* address PR feedback
2021-11-15 11:34:32 +01:00
Dominik SokalandBrent Vatne 37e360aead use keytool service (#754)
* use keytool service

* update CHANGELOG.md

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

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

Co-authored-by: Brent Vatne <[email protected]>
2021-11-15 10:45:16 +01:00
Quinlan Jung 20211e6265 fix changelog 2021-11-12 13:32:37 -08:00
Quinlan Jung 226c888cc9 [eas-cli] add analytic attempt events (#757)
* [eas-cli] add analytic attempt events

* update CHANGELOG.md
2021-11-12 10:53:29 -08:00
Quinlan Jung 0e9f8ec272 [eas-cli] submission analytics (#752)
* [eas-cli] submission analytics

* remove todos

* fix lint

* pr feedback

* pr feedback

* fix tests
2021-11-11 16:36:57 -08:00
Quinlan Jung 8eb5d8028d [eas-cli] Api -> API (#755)
* [eas-cli] Api -> API

* update CHANGELOG.md
2021-11-11 12:51:25 -08:00
Wojciech Kozyra 2fb296d116 [eas-cli] pass envs to prebuild config when resolving entitlements (#751)
* [eas-cli] pass envs to prebuild config when resolving entitlements

* update CHANGELOG.md

* review feedback
2021-11-09 12:07:34 +01:00
James Ide 010c1ee439 Revert "fix working with local api server (#747)" (#750)
* Revert "fix working with local api server (#747)"

Why
---
Make local development behave more closely to production and use the same API route paths. I made 127.0.0.1 behave the same way as api.expo.test and localhost. Another fix would have been to change 127.0.0.1 to localhost in EAS CLI, but it's a good idea to make the API server treat localhost and 127.0.0.1 similarly (look up PR 8542).

How
---
This reverts commit 5ba3cc6623.

Test Plan
---------
Started the API server locally, then started EAS CLI locally with `EXPO_LOCAL=1 yarn eas login`. Logged in with my staging credentials and saw the API server locally pring out that it had made new database connections in the log.

* update CHANGELOG.md

* update CHANGELOG.md
2021-11-09 09:12:43 +01:00
Quinlan Jung dddc4a0e95 [eas-cli] only prompt for appleIdUsername if authenticating via asp (#745)
* [eas-cli] only prompt for appleIdUsername if authenticating via asp

* pr feedback

* update CHANGELOG.md
2021-11-08 16:37:36 -08:00
Dominik Sokal 679ccf19d9 fix --auto-submit-with-profile (#748)
* fix --auto-submit-with-profile

* update CHANGELOG.md
2021-11-08 19:00:13 +01:00
Dominik Sokal 5ba3cc6623 fix working with local api server (#747) 2021-11-08 14:53:34 +01:00
Dominik Sokal c1a4731762 fix changelog headings; fix changelog script 2021-11-08 14:49:54 +01:00
Expo CI 767577e386 update CHANGELOG.md 2021-11-08 11:32:25 +00:00
Dominik Sokal d2f620dbc6 v0.35.0 2021-11-08 12:04:28 +01:00
Dominik Sokal 0aa556c29e Update CHANGELOG.md 2021-11-08 12:03:35 +01:00
James Ide 10cf003758 [api] Change API domain for EAS CLI (#744)
* [api] Change API domain for EAS CLI

Why
---
To match the API server domain the website uses, this commit switches the API domain for EAS CLI from exp.host to api.expo.dev.

How
---
URL paths for api.expo.dev are different than those for exp.host.

* GraphQL: api.expo.dev/graphql
* API v2: api.expo.dev/v2/...

Updated the main functions that return the API server origin and the API v2 and GraphQL clients that create the URLs for each type of API.

Test Plan
---------
* Unit tests: `yarn test`
* Log in and out: `yarn eas login`, `yarn eas logout` (tests API v2)
* Project management: `yarn eas project:view` (tests GraphQL) -> confirm project gets created on expo.dev

* update CHANGELOG.md
2021-11-08 10:45:21 +01: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
219 changed files with 8472 additions and 3464 deletions

No files matched your search

-1
View File
@@ -11,7 +11,6 @@ module.exports = {
ignoreDeclarationSort: true,
},
],
'flowtype/no-types-missing-file-annotation': 'off',
curly: 'warn',
'import/no-cycle': 'error',
'import/no-extraneous-dependencies': [
Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

+7
View File
@@ -21,6 +21,13 @@ body:
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: Managed or bare?
description: See the [Workflows](https://docs.expo.dev/introduction/managed-vs-bare/) page for more information.
placeholder: Is your project managed or bare? Bare - Android and/or iOS native projects exist in your repo; Managed - you don't have native code (custom or 3rd party) in your project.
- type: textarea
validations:
required: true
-1
View File
@@ -3,7 +3,6 @@
# 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
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
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
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Checkout Pull Request
+26 -11
View File
@@ -25,21 +25,29 @@ jobs:
needs: release
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: '16'
- run: yarn install --frozen-lockfile --check-files
- run: yarn build
- name: Build tarballs
working-directory: ${{ github.workspace }}/packages/eas-cli
run: yarn oclif-dev pack --targets linux-x64,linux-arm
run: |
yarn pretarball-ci
yarn oclif pack:tarballs --no-xz --targets linux-x64,linux-arm
- id: x64
run: echo "::set-output name=filename::$(ls eas-*-x64.tar.gz)"
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- id: arm
run: echo "::set-output name=filename::$(ls eas-*-arm.tar.gz)"
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- name: Upload linux-x64 tarball
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/eas-linux-x64.tar.gz
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
asset_name: eas-linux-x64.tar.gz
asset_content_type: application/gzip
- name: Upload linux-arm tarball
@@ -48,7 +56,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/eas-linux-arm.tar.gz
asset_path: packages/eas-cli/dist/${{ steps.arm.outputs.filename }}
asset_name: eas-linux-arm.tar.gz
asset_content_type: application/gzip
build-mac:
@@ -58,21 +66,26 @@ jobs:
needs: release
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: '16'
- run: yarn install --frozen-lockfile --check-files
- run: yarn build
- name: Build tarballs
working-directory: ${{ github.workspace }}/packages/eas-cli
run: yarn oclif-dev pack --targets darwin-x64
run: |
yarn pretarball-ci
yarn oclif pack:tarballs --no-xz --targets darwin-x64
- id: x64
run: echo "::set-output name=filename::$(ls eas-*-x64.tar.gz)"
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- name: Upload darwin-x64 tarball
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/eas-darwin-x64.tar.gz
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
asset_name: eas-darwin-x64.tar.gz
asset_content_type: application/gzip
build-windows:
@@ -81,13 +94,15 @@ jobs:
needs: release
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: '16'
- run: yarn install --frozen-lockfile --check-files
- run: sudo apt-get install nsis
- run: yarn build
- run: yarn oclif-dev pack:win
- run: |
yarn pretarball-ci
yarn oclif pack:win
working-directory: ${{ github.workspace }}/packages/eas-cli
- id: x64
run: echo "::set-output name=filename::$(ls eas-*-x64.exe)"
@@ -119,7 +134,7 @@ jobs:
needs: [build-linux, build-mac, build-windows]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
registry-url: 'https://registry.npmjs.org/'
scope: 'expo'
@@ -138,7 +153,7 @@ jobs:
needs: publish-to-npm
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
+4 -2
View File
@@ -7,12 +7,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: 16
- uses: preactjs/compressed-size-action@v2
with:
build-script: 'build:tarballs:linux'
repo-token: '${{ secrets.GITHUB_TOKEN }}'
pattern: './packages/eas-cli/dist/*.tar.gz'
compression: 'none'
strip-hash: "v\\d+.\\d+.\\d+-\\w+-"
clean-script: 'clean'
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- run: yarn install --frozen-lockfile --check-files
+15 -10
View File
@@ -1,14 +1,19 @@
# Build directories
packages/*/build
packages/eas-cli/dist
packages/eas-cli/tmp
# Code editors
.idea
.history
# macOS
.DS_Store
# Node
node_modules
*-debug.log
*-error.log
**/build/**
!packages/eas-cli/src/commands/build/**
!packages/eas-cli/src/build/**
dist
/package-lock.json
tmp
node_modules
# Tests
coverage
.history
.DS_Store
+1 -1
View File
@@ -2,7 +2,7 @@
"printWidth": 100,
"tabWidth": 2,
"singleQuote": true,
"jsxBracketSameLine": true,
"bracketSameLine": true,
"trailingComma": "es5",
"arrowParens": "avoid"
}
+185 -20
View File
@@ -8,20 +8,185 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 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 App Store Connect 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))
### 🐛 Bug fixes
- Allow select eas commands to be run outside of a project. ([#851](https://github.com/expo/eas-cli/pull/851) by [@jkhales](https://github.com/jkhales))
### 🧹 Chores
- Replace all oclif dependencies with `@oclif/core`. ([#858](https://github.com/expo/eas-cli/pull/858) by [@dsokal](https://github.com/dsokal))
## [0.41.1](https://github.com/expo/eas-cli/releases/tag/v0.41.1) - 2021-12-16
### 🎉 New features
- Adds commands for EAS Update, which is now in preview for subscribers. EAS Update makes fixing small bugs and pushing quick fixes a snap in between app store submissions. It accomplishes this by allowing an end-user's app to swap out the non-native parts of their app (for example, JS, styling, and image changes) with a new update that contains bug fixes and other updates.
- Adds `eas update`, which can bundle and publish updates on a branch.
- Adds `eas branch`, which manages branches. Branches contain a list of updates and are linked to channels.
- Adds `eas channel`, which manages channels. Channels are specified inside builds and are linked to branches, allowing us to link specific updates with specific builds.
- Read more in our [feature preview docs](https://docs.expo.dev/eas-update/introduction/).
### 🐛 Bug fixes
- Fix `eas submit` displaying a prompt in non-interactive mode when some ASC API credentials are missing in `eas.json`. ([#841](https://github.com/expo/eas-cli/pull/841) by [@barthap](https://github.com/barthap))
## [0.41.0](https://github.com/expo/eas-cli/releases/tag/v0.41.0) - 2021-12-13
### 🎉 New features
- Ask user to select profile if `release` does not exist. ([#829](https://github.com/expo/eas-cli/pull/829) by [@dsokal](https://github.com/dsokal))
- Add `build:inspect` command. ([#834](https://github.com/expo/eas-cli/pull/834) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Fix validating tool versions in eas.json. ([#832](https://github.com/expo/eas-cli/pull/832) by [@dsokal](https://github.com/dsokal))
- Fix submission URL. ([#845](https://github.com/expo/eas-cli/pull/845) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Warn when building managed project with SDK < 41. ([#833](https://github.com/expo/eas-cli/pull/833) by [@dsokal](https://github.com/dsokal))
## [0.40.0](https://github.com/expo/eas-cli/releases/tag/v0.40.0) - 2021-12-08
### 🛠 Breaking changes
- [eas-cli] change eas update:publish to eas update. ([#830](https://github.com/expo/eas-cli/pull/830) by [@jkhales](https://github.com/jkhales))
### 🐛 Bug fixes
- Pass build profile environment to credentials command. ([#828](https://github.com/expo/eas-cli/pull/828) by [@quinlanj](https://github.com/quinlanj))
## [0.39.0](https://github.com/expo/eas-cli/releases/tag/v0.39.0) - 2021-12-06
### 🎉 New features
- Set runtime version in `eas update:configure`. ([#811](https://github.com/expo/eas-cli/pull/811) by [@jkhales](https://github.com/jkhales))
- Print project workflow in `eas diagnostics`. ([#822](https://github.com/expo/eas-cli/pull/822) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix submit for multi-target projects where ascAppId is not specified. ([#809](https://github.com/expo/eas-cli/pull/809) by [@wkozyra95](https://github.com/wkozyra95))
- Fix building iOS projects with Apple Watch companion app. ([#812](https://github.com/expo/eas-cli/pull/812), [#817](https://github.com/expo/eas-cli/pull/817), [#821](https://github.com/expo/eas-cli/pull/821) by [@jkhales](https://github.com/jkhales) + [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Improve error message for missing ascAppId. ([#813](https://github.com/expo/eas-cli/pull/813) by [@wkozyra95](https://github.com/wkozyra95))
- Add `react-native` version to metadata. ([#823](https://github.com/expo/eas-cli/pull/823) by [@wkozyra95](https://github.com/wkozyra95))
## [0.38.3](https://github.com/expo/eas-cli/releases/tag/v0.38.3) - 2021-11-29
### 🐛 Bug fixes
- Print warning about outdated version to stderr. ([#805](https://github.com/expo/eas-cli/pull/805) by [@wkozyra95](https://github.com/wkozyra95))
- Fix Apple sign in capabilities. ([#806](https://github.com/expo/eas-cli/pull/806) by [@brentvatne](https://github.com/brentvatne))
## [0.38.2](https://github.com/expo/eas-cli/releases/tag/v0.38.2) - 2021-11-26
### 🎉 New features
- Add support for extending submit profiles. ([#794](https://github.com/expo/eas-cli/pull/794) by [@dsokal](https://github.com/dsokal))
- Add command: `eas update:configure`. ([#800](https://github.com/expo/eas-cli/pull/800) by [@jkhales](https://github.com/jkhales))
### 🐛 Bug fixes
- Fix reading app version and app build version of iOS projects. ([#798](https://github.com/expo/eas-cli/pull/798) by [@dsokal](https://github.com/dsokal))
- Fix push notifications entitlements. ([#801](https://github.com/expo/eas-cli/pull/801) by [@brentvatne](https://github.com/brentvatne))
## [0.38.1](https://github.com/expo/eas-cli/releases/tag/v0.38.1) - 2021-11-25
### 🐛 Bug fixes
- Fix building iOS projects with whitespace in the project name. ([#792](https://github.com/expo/eas-cli/pull/792) by [@dsokal](https://github.com/dsokal))
## [0.38.0](https://github.com/expo/eas-cli/releases/tag/v0.38.0) - 2021-11-24
### 🎉 New features
- Improve unknown capability syncing error message. ([#775](https://github.com/expo/eas-cli/pulls/775) by [@EvanBacon](https://github.com/EvanBacon))
- Add submit webhooks. ([#777](https://github.com/expo/eas-cli/pull/777) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Compute runtime version policies. ([#783](https://github.com/expo/eas-cli/pull/783), [#785](https://github.com/expo/eas-cli/pull/785) by [@jkhales](https://github.com/jkhales))
- Fix local builds with npm < 7. ([#787](https://github.com/expo/eas-cli/pull/787) by [@dsokal](https://github.com/dsokal))
- Override `applicationId`/`bundleIdentifier` when auto-submitting after build. ([#780](https://github.com/expo/eas-cli/pull/780) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Upgrade `typescript` to 4.5.2. Upgrade oclif dependencies. ([#781](https://github.com/expo/eas-cli/pull/781) by [@dsokal](https://github.com/dsokal))
- Make missing `git` command error message more descriptive. ([#784](https://github.com/expo/eas-cli/pull/784) by [@dsokal](https://github.com/dsokal))
## [0.37.0](https://github.com/expo/eas-cli/releases/tag/v0.37.0) - 2021-11-18
### 🛠 Breaking changes
- Require explicitly defined applicationId/bundleIdentifier for EAS Submit in case of bare projects consisting of multiple product flavors on Android or multiple schemes/targets on iOS. ([#765](https://github.com/expo/eas-cli/pull/765) by [@wkozyra95](https://github.com/wkozyra95))
### 🎉 New features
- Auto create channel on publish. ([#766](https://github.com/expo/eas-cli/pull/766) by [@jkhales](https://github.com/jkhales))
- Interactively configure Git `user.name` and `user.email`. ([#772](https://github.com/expo/eas-cli/pull/772) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Validate release channel in eas.json. ([#764](https://github.com/expo/eas-cli/pull/764) by [@dsokal](https://github.com/dsokal))
- Make the missing profile error message more descriptive. ([#761](https://github.com/expo/eas-cli/pull/761) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Better error message when missing eas.json. ([#770](https://github.com/expo/eas-cli/pull/770) by [@quinlanj](https://github.com/quinlanj))
## [0.36.1](https://github.com/expo/eas-cli/releases/tag/v0.36.1) - 2021-11-15
### 🐛 Bug fixes
- Fix `dateformat` import issue when building with `--local` flag. ([a520d](https://github.com/expo/eas-cli/commit/a520d1750736d61d6489da6487879f35ed3deb23) by [@dsokal](https://github.com/dsokal))
## [0.36.0](https://github.com/expo/eas-cli/releases/tag/v0.36.0) - 2021-11-15
### 🛠 Breaking changes
- Do not require manual `eas-cli-local-build-plugin` installation. An existing global installation (either with `yarn global add` or `npm install -g`) will not be used anymore. ([#753](https://github.com/expo/eas-cli/pull/753) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Use Keystore Service when `keytool` is not installed. ([#754](https://github.com/expo/eas-cli/pull/754) by [@dsokal](https://github.com/dsokal))
- Add applicationId/bundleIdentifier fields to the submit profile. ([#765](https://github.com/expo/eas-cli/pull/765) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Fix auto-submitting with `--auto-submit-with-profile`. ([#748](https://github.com/expo/eas-cli/pull/748) by [@dsokal](https://github.com/dsokal))
- Pass env from build profile when resolving entitlements. ([#751](https://github.com/expo/eas-cli/pull/751) by [@wkozyra95](https://github.com/wkozyra95))
- Only prompt for Apple Id username if authenticating with an App Specific Password. ([#745](https://github.com/expo/eas-cli/pull/745) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Use same URL paths for local development of EAS CLI as production. ([#750](https://github.com/expo/eas-cli/pull/750) by [@ide](https://github.com/ide))
- Grammar: Api -> API. ([#755](https://github.com/expo/eas-cli/pull/755) by [@quinlanj](https://github.com/quinlanj))
- Add analytics for EAS Submit. ([#752](https://github.com/expo/eas-cli/pull/752) by [@quinlanj](https://github.com/quinlanj))
- Add an 'attempt' event to build and submit analytics. ([#757](https://github.com/expo/eas-cli/pull/757) by [@quinlanj](https://github.com/quinlanj))
## [0.35.0](https://github.com/expo/eas-cli/releases/tag/v0.35.0) - 2021-11-08
### 🎉 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))
- Change EAS API server domain. ([#744](https://github.com/expo/eas-cli/pull/744) by [@ide](https://github.com/ide))
### 🧹 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))
- 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
## [0.34.1](https://github.com/expo/eas-cli/releases/tag/v0.34.1) - 2021-11-02
### 🐛 Bug fixes
@@ -29,16 +194,16 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 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))
- 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
## [0.34.0](https://github.com/expo/eas-cli/releases/tag/v0.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))
- 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))
- Implement no-commit build workflow. 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
@@ -53,7 +218,7 @@ This is the log of notable changes to EAS CLI and related packages.
- 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
## [0.33.1](https://github.com/expo/eas-cli/releases/tag/v0.33.1) - 2021-10-22
### 🎉 New features
@@ -63,7 +228,7 @@ This is the log of notable changes to EAS CLI and related packages.
- [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
## [0.33.0](https://github.com/expo/eas-cli/releases/tag/v0.33.0) - 2021-10-20
### 🎉 New features
@@ -86,11 +251,11 @@ This is the log of notable changes to EAS CLI and related packages.
- 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
## [0.32.0](https://github.com/expo/eas-cli/releases/tag/v0.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))
- 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))
@@ -100,16 +265,16 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 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))
- 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
## [0.31.1](https://github.com/expo/eas-cli/releases/tag/v0.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
## [0.31.0](https://github.com/expo/eas-cli/releases/tag/v0.31.0) - 2021-10-08
### 🛠 Breaking changes
@@ -120,7 +285,7 @@ This is the log of notable changes to EAS CLI and related packages.
- 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
## [0.30.1](https://github.com/expo/eas-cli/releases/tag/v0.30.1) - 2021-10-06
### 🐛 Bug fixes
@@ -499,7 +664,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 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 "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))
+1 -7
View File
@@ -1,10 +1,4 @@
## Releasing a new version
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.
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.34.1"
"version": "0.42.1"
}
+10 -7
View File
@@ -1,20 +1,22 @@
{
"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",
"build:tarballs:linux": "yarn build-for-size-check",
"build-for-size-check": "yarn build && yarn workspace eas-cli pretarball-ci && CLI_SIZE_CHECK=1 yarn workspace eas-cli oclif pack:tarballs --no-xz --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 --ext .js",
"release": "lerna version",
"lint": "eslint .",
"release": "./scripts/bin/run update-local-plugin && lerna version --exact",
"test": "jest",
"clean": "lerna run clean && rm -rf node_modules coverage"
},
"devDependencies": {
"@expo/oclif-dev-cli": "1.24.0-expo",
"@types/jest": "27.0.1",
"@types/node": "16.7.8",
"eslint": "7.32.0",
@@ -24,9 +26,10 @@
"jest": "27.1.0",
"jest-watch-typeahead": "0.6.4",
"lerna": "4.0.0",
"prettier": "2.3.2",
"oclif": "2.0.1",
"prettier": "2.4.1",
"ts-jest": "27.0.5",
"typescript": "4.4.2"
"typescript": "4.5.2"
},
"volta": {
"node": "16.10.0",
+488 -124
View File
@@ -7,9 +7,9 @@ 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)
* [Installation](#installation)
* [Usage](#usage)
* [Commands](#commands)
- [Installation](#installation)
- [Usage](#usage)
- [Commands](#commands)
# Installation
@@ -37,11 +37,21 @@ eas --help COMMAND
* [`eas account:logout`](#eas-accountlogout)
* [`eas account:view`](#eas-accountview)
* [`eas analytics [STATUS]`](#eas-analytics-status)
* [`eas branch:create [NAME]`](#eas-branchcreate-name)
* [`eas branch:delete [NAME]`](#eas-branchdelete-name)
* [`eas branch:list`](#eas-branchlist)
* [`eas branch:rename`](#eas-branchrename)
* [`eas branch:view [NAME]`](#eas-branchview-name)
* [`eas build`](#eas-build)
* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
* [`eas build:configure`](#eas-buildconfigure)
* [`eas build:inspect`](#eas-buildinspect)
* [`eas build:list`](#eas-buildlist)
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
* [`eas channel:create [NAME]`](#eas-channelcreate-name)
* [`eas channel:edit [NAME]`](#eas-channeledit-name)
* [`eas channel:list`](#eas-channellist)
* [`eas channel:view [NAME]`](#eas-channelview-name)
* [`eas config`](#eas-config)
* [`eas credentials`](#eas-credentials)
* [`eas device:create`](#eas-devicecreate)
@@ -55,6 +65,10 @@ eas --help COMMAND
* [`eas secret:delete`](#eas-secretdelete)
* [`eas secret:list`](#eas-secretlist)
* [`eas submit`](#eas-submit)
* [`eas update`](#eas-update)
* [`eas update:configure`](#eas-updateconfigure)
* [`eas update:delete GROUPID`](#eas-updatedelete-groupid)
* [`eas update:view GROUPID`](#eas-updateview-groupid)
* [`eas webhook:create`](#eas-webhookcreate)
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
* [`eas webhook:list`](#eas-webhooklist)
@@ -69,11 +83,14 @@ log in with your Expo account
USAGE
$ eas account:login
DESCRIPTION
log in with your Expo account
ALIASES
$ eas login
```
_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)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -83,11 +100,14 @@ log out
USAGE
$ eas account:logout
DESCRIPTION
log out
ALIASES
$ eas logout
```
_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)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -97,11 +117,14 @@ show the username you are logged in as
USAGE
$ eas account:view
DESCRIPTION
show the username you are logged in as
ALIASES
$ eas whoami
```
_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)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -110,43 +133,137 @@ view or change analytics settings
```
USAGE
$ eas analytics [STATUS]
DESCRIPTION
view or change analytics settings
```
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/analytics.js)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/analytics.ts)_
## `eas build`
## `eas branch:create [NAME]`
start a build
Create a branch on the current project.
```
USAGE
$ eas build
$ eas branch:create [NAME] [--json]
OPTIONS
-p, --platform=(android|ios|all)
ARGUMENTS
NAME Name of the branch to create
--auto-submit Submit on build complete using the submit profile with the same name as the
build profile
FLAGS
--json return a json with the new branch ID and name.
--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
DESCRIPTION
Create a branch on the current project.
```
_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)_
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
Delete a branch on the current project
```
USAGE
$ eas branch:delete [NAME] [--json]
ARGUMENTS
NAME Name of the branch to delete
FLAGS
--json return JSON with the edited branch's ID and name.
DESCRIPTION
Delete a branch on the current project
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
List all branches on this project.
```
USAGE
$ eas branch:list [--json]
FLAGS
--json return output as JSON
DESCRIPTION
List all branches on this project.
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
Rename a branch.
```
USAGE
$ eas branch:rename [--from <value>] [--to <value>] [--json]
FLAGS
--from=<value> current name of the branch.
--json return a json with the edited branch's ID and name.
--to=<value> new name of the branch.
DESCRIPTION
Rename a branch.
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
View a branch.
```
USAGE
$ eas branch:view [NAME] [--json]
ARGUMENTS
NAME Name of the branch to view
FLAGS
--json return a json with the branch's ID name and recent update groups.
DESCRIPTION
View a branch.
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
Start a build
```
USAGE
$ eas build [-p android|ios|all] [--json] [--skip-project-configuration] [--profile <value>]
[--non-interactive] [--local] [--wait] [--clear-cache] [--auto-submit | --auto-submit-with-profile <value>]
FLAGS
-p, --platform=(android|ios|all)
--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
DESCRIPTION
Start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -155,9 +272,12 @@ Cancel a build.
```
USAGE
$ eas build:cancel [BUILD_ID]
DESCRIPTION
Cancel a build.
```
_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)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -165,13 +285,52 @@ Configure the project to support EAS Build.
```
USAGE
$ eas build:configure
$ eas build:configure [-p android|ios|all]
OPTIONS
FLAGS
-p, --platform=(android|ios|all) Platform to configure
DESCRIPTION
Configure the project to support EAS Build.
```
_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)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:inspect`
Inspect the state of the project at specific build stages. Useful for troubleshooting.
```
USAGE
$ eas build:inspect -p android|ios -s archive|pre-build|post-build --output <value> [--profile <value>] [--force]
[--verbose]
FLAGS
-p, --platform=(android|ios)
(required)
-s, --stage=(archive|pre-build|post-build)
(required) Stage of the build you want to inspect.
archive - builds the project archive that would be uploaded to EAS when building
pre-build - prepares the project to be built with Gradle/Xcode. Does not run the native build.
post-build - builds the native project and leaves the output directory for inspection
--force
Delete OUTPUT_DIRECTORY if it already exists.
--output=OUTPUT_DIRECTORY
(required) Output directory.
--profile=PROFILE_NAME
Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.
--verbose
DESCRIPTION
Inspect the state of the project at specific build stages. Useful for troubleshooting.
```
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -179,32 +338,32 @@ list all builds for your project
```
USAGE
$ eas build:list
$ eas build:list [--platform all|android|ios] [--json] [--status
new|in-queue|in-progress|errored|finished|canceled] [--distribution store|internal|simulator] [--channel <value>]
[--appVersion <value>] [--appBuildVersion <value>] [--sdkVersion <value>] [--runtimeVersion <value>]
[--appIdentifier <value>] [--buildProfile <value>] [--gitCommitHash <value>] [--limit <value>]
OPTIONS
--appBuildVersion=appBuildVersion
--appIdentifier=appIdentifier
--appVersion=appVersion
--buildProfile=buildProfile
--channel=channel
FLAGS
--appBuildVersion=<value>
--appIdentifier=<value>
--appVersion=<value>
--buildProfile=<value>
--channel=<value>
--distribution=(store|internal|simulator)
--gitCommitHash=gitCommitHash
--gitCommitHash=<value>
--json Enable JSON output, non-JSON messages will be printed
to stderr
--limit=limit
--limit=<value>
--platform=(all|android|ios)
--runtimeVersion=runtimeVersion
--sdkVersion=sdkVersion
--runtimeVersion=<value>
--sdkVersion=<value>
--status=(new|in-queue|in-progress|errored|finished|canceled)
DESCRIPTION
list all builds for your project
```
_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)_
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:view [BUILD_ID]`
@@ -212,13 +371,94 @@ view a build for your project
```
USAGE
$ eas build:view [BUILD_ID]
$ eas build:view [BUILD_ID] [--json]
OPTIONS
FLAGS
--json Enable JSON output, non-JSON messages will be printed to stderr
DESCRIPTION
view a build for your project
```
_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)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
Create a channel on the current project.
```
USAGE
$ eas channel:create [NAME] [--json]
ARGUMENTS
NAME Name of the channel to create
FLAGS
--json print output as a JSON object with the new channel ID, name and branch mapping.
DESCRIPTION
Create a channel on the current project.
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
Point a channel at a new branch.
```
USAGE
$ eas channel:edit [NAME] [--branch <value>] [--json]
ARGUMENTS
NAME Name of the channel to edit
FLAGS
--branch=<value> Name of the branch to point to
--json print output as a JSON object with the channel ID, name and branch mapping.
DESCRIPTION
Point a channel at a new branch.
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
List all channels on the current project.
```
USAGE
$ eas channel:list [--json]
FLAGS
--json print output as a JSON object with the channel ID, name and branch mapping.
DESCRIPTION
List all channels on the current project.
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:view [NAME]`
View a channel on the current project.
```
USAGE
$ eas channel:view [NAME] [--json]
ARGUMENTS
NAME Name of the channel to view
FLAGS
--json print output as a JSON object with the channel ID, name and branch mapping.
DESCRIPTION
View a channel on the current project.
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -226,14 +466,17 @@ show the eas.json config
```
USAGE
$ eas config
$ eas config [-p android|ios] [--profile <value>]
OPTIONS
FLAGS
-p, --platform=(android|ios)
--profile=profile
--profile=<value>
DESCRIPTION
show the eas.json config
```
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/config.js)_
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -242,9 +485,12 @@ manage your credentials
```
USAGE
$ eas credentials
DESCRIPTION
manage your credentials
```
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/credentials.js)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -253,9 +499,12 @@ register new Apple Devices to use for internal distribution
```
USAGE
$ eas device:create
DESCRIPTION
register new Apple Devices to use for internal distribution
```
_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)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:list`
@@ -263,13 +512,16 @@ list all registered devices for your account
```
USAGE
$ eas device:list
$ eas device:list [--apple-team-id <value>]
OPTIONS
--apple-team-id=apple-team-id
FLAGS
--apple-team-id=<value>
DESCRIPTION
list all registered devices for your account
```
_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)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -278,9 +530,12 @@ view a device for your project
```
USAGE
$ eas device:view [UDID]
DESCRIPTION
view a device for your project
```
_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)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -289,26 +544,32 @@ log environment info to the console
```
USAGE
$ eas diagnostics
DESCRIPTION
log environment info to the console
```
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/diagnostics.js)_
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
display help for eas
Display help for eas.
```
USAGE
$ eas help [COMMAND]
$ eas help [COMMAND] [-n]
ARGUMENTS
COMMAND command to show help for
COMMAND Command to show help for.
OPTIONS
--all see all commands in CLI
FLAGS
-n, --nested-commands Include all nested commands in the output.
DESCRIPTION
Display help for eas.
```
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.3/src/commands/help.ts)_
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.1.9/src/commands/help.ts)_
## `eas project:info`
@@ -317,9 +578,12 @@ information about the current project
```
USAGE
$ eas project:info
DESCRIPTION
information about the current project
```
_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)_
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -329,11 +593,14 @@ create or link an EAS project
USAGE
$ eas project:init
DESCRIPTION
create or link an EAS project
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)_
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/project/init.ts)_
## `eas secret:create`
@@ -341,16 +608,19 @@ Create an environment secret on the current project or owner account.
```
USAGE
$ eas secret:create
$ eas secret:create [--scope account|project] [--name <value>] [--value <value>] [--force]
OPTIONS
FLAGS
--force Delete and recreate existing secrets
--name=name Name of the secret
--name=<value> Name of the secret
--scope=(account|project) [default: project] Scope for the secret
--value=value Value of the secret
--value=<value> Value of the secret
DESCRIPTION
Create an environment secret on the current project or owner account.
```
_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)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -358,16 +628,18 @@ Delete an environment secret by ID.
```
USAGE
$ eas secret:delete
$ eas secret:delete [--id <value>]
OPTIONS
--id=id ID of the secret to delete
FLAGS
--id=<value> ID of the secret to delete
DESCRIPTION
Delete an environment secret by ID.
Unsure where to find the secret's ID? Run eas secret:list
```
_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)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -376,43 +648,124 @@ Lists environment secrets available for your current app
```
USAGE
$ eas secret:list
DESCRIPTION
Lists environment secrets available for your current app
```
_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)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/secret/list.ts)_
## `eas submit`
submit build archive to app store
Submit build archive to App Store Connect
```
USAGE
$ eas submit
$ eas submit [-p android|ios|all] [--profile <value>] [--latest | --id <value> | --path <value> | --url
<value>] [--verbose] [--wait] [--non-interactive]
OPTIONS
FLAGS
-p, --platform=(android|ios|all)
--id=id ID of the build to submit
--id=<value> 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
--profile=profile Name of the submit profile from eas.json. Defaults to "production" if defined in
--path=<value> Path to the .apk/.aab/.ipa file
--profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in
eas.json.
--url=url App archive url
--url=<value> App archive url
--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/
Submit build archive to App Store Connect
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/submit.js)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
Publish an update group.
```
USAGE
$ eas update [--branch <value>] [--message <value>] [--republish | --input-dir <value> | --skip-bundler]
[--group <value> | | ] [-p android|ios|all] [--json] [--auto]
FLAGS
-p, --platform=(android|ios|all) [default: all]
--auto Use the current git branch and commit message for the EAS branch and update message
--branch=<value> Branch to publish the update group on
--group=<value> Update group to republish
--input-dir=<value> [default: dist] Location of the bundle
--json Enable JSON output, non-JSON messages will be printed to stderr
--message=<value> A short message describing the update
--republish Republish an update group
--skip-bundler Skip running Expo CLI to bundle the app before publishing
DESCRIPTION
Publish an update group.
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
Configure the project to support EAS Update.
```
USAGE
$ eas update:configure
DESCRIPTION
Configure the project to support EAS Update.
```
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
Delete all the updates in an update Group.
```
USAGE
$ eas update:delete [GROUPID] [--json]
ARGUMENTS
GROUPID The ID of an update group to delete.
FLAGS
--json Return a json with the group ID of the deleted updates.
DESCRIPTION
Delete all the updates in an update Group.
```
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:view GROUPID`
Update group details.
```
USAGE
$ eas update:view [GROUPID] [--json]
ARGUMENTS
GROUPID The ID of an update group.
FLAGS
--json Return a json with the updates belonging to the group.
DESCRIPTION
Update group details.
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -420,18 +773,19 @@ Create a webhook on the current project.
```
USAGE
$ eas webhook:create
$ eas webhook:create [--event BUILD|SUBMIT] [--url <value>] [--secret <value>]
OPTIONS
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
FLAGS
--event=(BUILD|SUBMIT) Event type that triggers the webhook
--secret=<value> Secret used to create a hash signature of the request payload, provided in the
'Expo-Signature' header.
--url=<value> Webhook URL
--secret=secret Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature'
header.
--url=url Webhook URL
DESCRIPTION
Create a webhook on the current project.
```
_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)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -443,9 +797,12 @@ USAGE
ARGUMENTS
ID ID of the webhook to delete
DESCRIPTION
Delete a webhook on the current project.
```
_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)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -453,13 +810,16 @@ List webhooks on the current project.
```
USAGE
$ eas webhook:list
$ eas webhook:list [--event BUILD|SUBMIT]
OPTIONS
--event=(BUILD) Event type that triggers the webhook
FLAGS
--event=(BUILD|SUBMIT) Event type that triggers the webhook
DESCRIPTION
List webhooks on the current project.
```
_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)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -467,19 +827,20 @@ Create a webhook on the current project.
```
USAGE
$ eas webhook:update
$ eas webhook:update --id <value> [--event BUILD|SUBMIT] [--url <value>] [--secret <value>]
OPTIONS
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
--id=id (required) Webhook ID
FLAGS
--event=(BUILD|SUBMIT) Event type that triggers the webhook
--id=<value> (required) Webhook ID
--secret=<value> Secret used to create a hash signature of the request payload, provided in the
'Expo-Signature' header.
--url=<value> Webhook URL
--secret=secret Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature'
header.
--url=url Webhook URL
DESCRIPTION
Create a webhook on the current project.
```
_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)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -487,11 +848,14 @@ View a webhook on the current project.
```
USAGE
$ eas webhook:view ID
$ eas webhook:view [ID]
ARGUMENTS
ID ID of the webhook to view
DESCRIPTION
View a webhook on the current project.
```
_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)_
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.42.1/packages/eas-cli/src/commands/webhook/view.ts)_
<!-- commandsstop -->
+3 -4
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('@oclif/command')
.run()
.then(require('@oclif/command/flush'))
.catch(require('@oclif/errors/handle'));
const oclif = require('@oclif/core')
oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle'))
+1 -1
View File
@@ -1,5 +1,5 @@
overwrite: true
schema: 'https://staging.exp.host/--/graphql'
schema: 'https://staging-api.expo.dev/graphql'
documents:
- 'src/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
+977 -184
View File
File diff suppressed because it is too large. Load diff
+25 -21
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.34.1",
"version": "0.42.1",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -9,24 +9,20 @@
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@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/config": "6.0.11",
"@expo/config-plugins": "4.0.11",
"@expo/eas-build-job": "0.2.61",
"@expo/eas-json": "^0.42.0",
"@expo/json-file": "8.2.34",
"@expo/pkcs12": "0.0.6",
"@expo/plist": "0.0.16",
"@expo/plugin-warn-if-update-available": "2.0.1",
"@expo/prebuild-config": "3.0.11",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.0",
"@expo/sdk-runtime-versions": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@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",
"@oclif/core": "1.0.10",
"@urql/core": "2.3.1",
"@urql/exchange-retry": "0.3.0",
"chalk": "4.1.2",
@@ -96,7 +92,8 @@
"mockdate": "^3.0.2",
"nock": "^13.0.5",
"tempy": "0.7.0",
"ts-deepmerge": "1.0.8"
"ts-deepmerge": "1.0.8",
"typescript": "4.5.2"
},
"engines": {
"node": ">=12.0.0"
@@ -165,18 +162,25 @@
}
}
}
}
},
"additionalHelpFlags": [
"-h"
],
"additionalVersionFlags": [
"-v"
]
},
"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 && node patch-readme",
"prepack": "./scripts/prepack.sh",
"pretarball-ci": "./scripts/pretarball-ci.sh",
"build": "tsc --project tsconfig.build.json",
"watch": "yarn build --watch --preserveWatchOutput",
"typecheck": "tsc",
"test": "jest",
"version": "yarn oclif-dev readme && node patch-readme && git add README.md",
"version": "yarn oclif readme && node scripts/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"
"clean": "rm -rf dist build tmp node_modules yarn-error.log"
}
}
@@ -1,7 +1,7 @@
const { promises: fs } = require('fs');
(async () => {
// Patch `oclif-dev readme` path and link generation
// Patch `oclif 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');
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -eo pipefail
rm -rf build
yarn build
if [ -z "$CLI_SIZE_CHECK" ]; then
yarn oclif manifest
fi
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -eo pipefail
EAS_CLI_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
PACKAGE_JSON_PATH="$EAS_CLI_DIR/package.json"
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "s/\\\"@expo\/eas-json\\\": \\\".*\\\"/\\\"@expo\/eas-json\\\": \\\"file:..\/..\/..\/eas-json\\\"/g" $PACKAGE_JSON_PATH
else
sed -i "s/\\\"@expo\/eas-json\\\": \\\".*\\\"/\\\"@expo\/eas-json\\\": \\\"file:..\/..\/..\/eas-json\\\"/g" $PACKAGE_JSON_PATH
fi
+2 -2
View File
@@ -8,7 +8,7 @@ import { apiClient, getExpoApiBaseUrl } from '../api';
describe('apiClient', () => {
it('converts Expo APIv2 error to ApiV2Error', async () => {
nock(getExpoApiBaseUrl())
.post('/--/api/v2/test')
.post('/v2/test')
.reply(400, {
errors: [
{
@@ -39,7 +39,7 @@ describe('apiClient', () => {
});
it('does not convert non-APIv2 error to ApiV2Error', async () => {
nock(getExpoApiBaseUrl()).post('/--/api/v2/test').reply(500, 'Something went wrong');
nock(getExpoApiBaseUrl()).post('/v2/test').reply(500, 'Something went wrong');
let error: Error | null = null;
try {
+25
View File
@@ -0,0 +1,25 @@
import { Analytics, Event } from './events';
export type TrackingContext = Record<string, string | number | boolean>;
export async function withAnalyticsAsync<Result>(
fn: () => Promise<Result>,
analytics: {
attemptEvent: Event;
successEvent: Event;
failureEvent: Event;
trackingCtx: TrackingContext;
}
): Promise<Result> {
try {
const result = await fn();
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
return result;
} catch (error: any) {
Analytics.logEvent(analytics.failureEvent, {
...analytics.trackingCtx,
reason: error.message,
});
throw error;
}
}
+52
View File
@@ -0,0 +1,52 @@
import { logEvent } from './rudderstackClient';
export type Event = BuildEvent | SubmissionEvent;
export enum SubmissionEvent {
SUBMIT_COMMAND = 'submit cli submit command',
SUBMIT_COMMAND_ATTEMPT = 'submit cli attempt',
SUBMIT_COMMAND_SUCCESS = 'submit cli success',
SUBMIT_COMMAND_FAIL = 'submit cli fail',
GATHER_CREDENTIALS_ATTEMPT = 'submit cli gather credentials attempt',
GATHER_CREDENTIALS_SUCCESS = 'submit cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'submit cli gather credentials fail',
GATHER_ARCHIVE_ATTEMPT = 'submit cli gather archive attempt',
GATHER_ARCHIVE_SUCCESS = 'submit cli gather archive success',
GATHER_ARCHIVE_FAIL = 'submit cli gather archive fail',
SUBMIT_REQUEST_ATTEMPT = 'submit cli request attempt',
SUBMIT_REQUEST_SUCCESS = 'submit cli request success',
SUBMIT_REQUEST_FAIL = 'submit cli request fail',
}
export enum BuildEvent {
BUILD_COMMAND = 'build cli build command',
PROJECT_UPLOAD_ATTEMPT = 'build cli project upload attempt',
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
GATHER_CREDENTIALS_ATTEMPT = 'build cli gather credentials attempt',
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
CONFIGURE_PROJECT_ATTEMPT = 'build cli configure project attempt',
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
BUILD_REQUEST_ATTEMPT = 'build cli build request attempt',
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_ATTEMPT = 'build cli credentials sync update local attempt',
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_ATTEMPT = 'build cli credentials sync update remote attempt',
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 class Analytics {
static logEvent(name: Event, properties: Record<string, any>): void {
logEvent(name, properties);
}
}
@@ -3,8 +3,8 @@ import os from 'os';
import { URL } from 'url';
import { v4 as uuidv4 } from 'uuid';
import UserSettings from './user/UserSettings';
import { easCliVersion } from './utils/easCli';
import UserSettings from '../user/UserSettings';
import { easCliVersion } from '../utils/easCli';
const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
darwin: 'Mac',
+13 -3
View File
@@ -4,7 +4,7 @@ import ApiV2Error from './ApiV2Error';
import { getAccessToken, getSessionSecret } from './user/sessionStorage';
export const apiClient = got.extend({
prefixUrl: getExpoApiBaseUrl() + '/--/api/v2/',
prefixUrl: getExpoApiBaseUrl() + '/v2/',
hooks: {
beforeRequest: [
(options: NormalizedOptions) => {
@@ -40,11 +40,11 @@ export const apiClient = got.extend({
export function getExpoApiBaseUrl(): string {
if (process.env.EXPO_STAGING) {
return `https://staging.exp.host`;
return `https://staging-api.expo.dev`;
} else if (process.env.EXPO_LOCAL) {
return `http://127.0.0.1:3000`;
} else {
return `https://exp.host`;
return `https://api.expo.dev`;
}
}
@@ -57,3 +57,13 @@ export function getExpoWebsiteBaseUrl(): string {
return `https://expo.dev`;
}
}
export function getEASUpdateURL(projectId: string): string {
if (process.env.EXPO_STAGING) {
return new URL(projectId, `https://staging-u.expo.dev`).href;
} else if (process.env.EXPO_LOCAL) {
return new URL(`expo-updates/${projectId}`, `http://127.0.0.1:3000`).href;
} else {
return new URL(projectId, `https://u.expo.dev`).href;
}
}
+17 -13
View File
@@ -11,7 +11,7 @@ import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
getApplicationIdAsync,
} from '../../project/android/applicationId';
import { GradleBuildContext, resolveGradleBuildContextAsync } from '../../project/android/gradle';
import { resolveGradleBuildContextAsync } from '../../project/android/gradle';
import { toggleConfirmAsync } from '../../prompts';
import { findAccountByName } from '../../user/Account';
import {
@@ -20,7 +20,7 @@ import {
JobData,
prepareBuildRequestForPlatformAsync,
} from '../build';
import { BuildContext } from '../context';
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
import { transformMetadata } from '../graphql';
import { logCredentialsSource } from '../utils/credentials';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
@@ -28,9 +28,9 @@ import { validateAndSyncProjectConfigurationAsync } from './configure';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
export async function prepareAndroidBuildAsync(
ctx: BuildContext<Platform.ANDROID>
): Promise<BuildRequestSender> {
export async function createAndroidContextAsync(
ctx: CommonContext<Platform.ANDROID>
): Promise<AndroidBuildContext> {
const { buildProfile } = ctx;
if (buildProfile.distribution === 'internal' && buildProfile.gradleCommand?.match(/bundle/)) {
@@ -58,21 +58,26 @@ This means that it will most likely produce an AAB and you will not be able to i
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
}
const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext);
return { applicationId, gradleContext };
}
export async function prepareAndroidBuildAsync(
ctx: BuildContext<Platform.ANDROID>
): Promise<BuildRequestSender> {
return await prepareBuildRequestForPlatformAsync({
ctx,
ensureCredentialsAsync: async (ctx: BuildContext<Platform.ANDROID>) => {
return await ensureAndroidCredentialsAsync(ctx, gradleContext);
return await ensureAndroidCredentialsAsync(ctx);
},
ensureProjectConfiguredAsync: async () => {
await validateAndSyncProjectConfigurationAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile,
buildProfile: ctx.buildProfile,
});
},
getMetadataContext: () => ({
gradleContext,
}),
prepareJobAsync: async (
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData<AndroidCredentials>
@@ -100,8 +105,7 @@ function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean
}
async function ensureAndroidCredentialsAsync(
ctx: BuildContext<Platform.ANDROID>,
gradleContext?: GradleBuildContext
ctx: BuildContext<Platform.ANDROID>
): Promise<CredentialsResult<AndroidCredentials> | undefined> {
if (!shouldProvideCredentials(ctx)) {
return;
@@ -109,7 +113,7 @@ async function ensureAndroidCredentialsAsync(
const androidApplicationIdentifier = await getApplicationIdAsync(
ctx.projectDir,
ctx.exp,
gradleContext
ctx.android.gradleContext
);
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
app: {
@@ -1,7 +1,7 @@
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 { AndroidVersionAutoIncrement, BuildProfile } from '@expo/eas-json';
import Log from '../../log';
import {
@@ -41,7 +41,7 @@ export async function validateAndSyncProjectConfigurationAsync({
}: {
projectDir: string;
exp: ExpoConfig;
buildProfile: AndroidBuildProfile;
buildProfile: BuildProfile<Platform.ANDROID>;
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const { autoIncrement } = buildProfile;
@@ -1,5 +1,5 @@
import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
import { AndroidBuildProfile } from '@expo/eas-json';
import { BuildProfile } from '@expo/eas-json';
import path from 'path';
import slash from 'slash';
@@ -25,7 +25,7 @@ export async function prepareJobAsync(
jobData: JobData
): Promise<Job> {
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildProfile: AndroidBuildProfile = ctx.buildProfile;
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const { credentials } = jobData;
+19 -38
View File
@@ -3,6 +3,8 @@ import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import { withAnalyticsAsync } from '../analytics/common';
import { BuildEvent } from '../analytics/events';
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
import { BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildQuery } from '../graphql/queries/BuildQuery';
@@ -16,9 +18,7 @@ import { sleepAsync } from '../utils/promise';
import { getVcsClient } from '../vcs';
import { BuildContext } from './context';
import { runLocalBuildAsync } from './local';
import { MetadataContext, collectMetadataAsync } from './metadata';
import { TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
import { collectMetadataAsync } from './metadata';
import { printDeprecationWarnings } from './utils/printBuildInfo';
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
@@ -39,7 +39,6 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
ctx: BuildContext<TPlatform>
): Promise<CredentialsResult<Credentials> | undefined>;
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
getMetadataContext: () => MetadataContext<TPlatform>;
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
}
@@ -55,15 +54,17 @@ export async function prepareBuildRequestForPlatformAsync<
const credentialsResult = await withAnalyticsAsync(
async () => await builder.ensureCredentialsAsync(ctx),
{
successEvent: Event.GATHER_CREDENTIALS_SUCCESS,
failureEvent: Event.GATHER_CREDENTIALS_FAIL,
attemptEvent: BuildEvent.GATHER_CREDENTIALS_ATTEMPT,
successEvent: BuildEvent.GATHER_CREDENTIALS_SUCCESS,
failureEvent: BuildEvent.GATHER_CREDENTIALS_FAIL,
trackingCtx: ctx.trackingCtx,
}
);
if (!ctx.skipProjectConfiguration) {
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(ctx), {
successEvent: Event.CONFIGURE_PROJECT_SUCCESS,
failureEvent: Event.CONFIGURE_PROJECT_FAIL,
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
trackingCtx: ctx.trackingCtx,
});
}
@@ -76,7 +77,7 @@ export async function prepareBuildRequestForPlatformAsync<
);
}
const projectArchive = ctx.local
const projectArchive = ctx.localBuildOptions.enable
? ({
type: ArchiveSourceType.PATH,
path: (await makeProjectTarballAsync()).path,
@@ -86,16 +87,15 @@ export async function prepareBuildRequestForPlatformAsync<
bucketKey: await uploadProjectAsync(ctx),
} as const);
const metadataContext = builder.getMetadataContext();
const metadata = await collectMetadataAsync(ctx, metadataContext);
const metadata = await collectMetadataAsync(ctx);
const job = await builder.prepareJobAsync(ctx, {
projectArchive,
credentials: credentialsResult?.credentials,
});
return async () => {
if (ctx.local) {
await runLocalBuildAsync(job);
if (ctx.localBuildOptions.enable) {
await runLocalBuildAsync(job, ctx.localBuildOptions);
return undefined;
} else {
try {
@@ -166,8 +166,9 @@ async function uploadProjectAsync<TPlatform extends Platform>(
return bucketKey;
},
{
successEvent: Event.PROJECT_UPLOAD_SUCCESS,
failureEvent: Event.PROJECT_UPLOAD_FAIL,
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
successEvent: BuildEvent.PROJECT_UPLOAD_SUCCESS,
failureEvent: BuildEvent.PROJECT_UPLOAD_FAIL,
trackingCtx: ctx.trackingCtx,
}
);
@@ -200,34 +201,14 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
return build;
},
{
successEvent: Event.BUILD_REQUEST_SUCCESS,
failureEvent: Event.BUILD_REQUEST_FAIL,
attemptEvent: BuildEvent.BUILD_REQUEST_ATTEMPT,
successEvent: BuildEvent.BUILD_REQUEST_SUCCESS,
failureEvent: BuildEvent.BUILD_REQUEST_FAIL,
trackingCtx: ctx.trackingCtx,
}
);
}
async function withAnalyticsAsync<Result>(
fn: () => Promise<Result>,
analytics: {
successEvent: Event;
failureEvent: Event;
trackingCtx: TrackingContext;
}
): Promise<Result> {
try {
const result = await fn();
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
return result;
} catch (error: any) {
Analytics.logEvent(analytics.failureEvent, {
...analytics.trackingCtx,
reason: error.message,
});
throw error;
}
}
export async function waitForBuildEndAsync(
buildIds: string[],
{ timeoutSec = 3600, intervalSec = 30 } = {}
+4 -5
View File
@@ -1,7 +1,7 @@
import { getConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJson, EasJsonReader } from '@expo/eas-json';
import { error } from '@oclif/errors';
import { Errors } from '@oclif/core';
import chalk from 'chalk';
import fs from 'fs-extra';
@@ -39,13 +39,13 @@ export async function ensureProjectConfiguredAsync(
platform: requestedPlatform,
});
if (await getVcsClient().isCommitRequiredAsync()) {
error(
Errors.error(
'Build process requires clean working tree, please commit all your changes and run `eas build` again',
{ exit: 1 }
);
}
} else {
error(
Errors.error(
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
'https://docs.expo.dev/build/eas-json'
)})`,
@@ -58,7 +58,6 @@ export async function configureAsync(options: {
platform: RequestedPlatform;
projectDir: string;
}): Promise<void> {
await getVcsClient().ensureRepoExistsAsync();
await maybeBailOnRepoStatusAsync();
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
@@ -144,7 +143,7 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
if (await fs.pathExists(easJsonPath)) {
const reader = new EasJsonReader(ctx.projectDir);
await reader.readAndValidateAsync();
await reader.readAsync();
Log.withTick('Validated eas.json');
return;
+25 -120
View File
@@ -1,20 +1,16 @@
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 { BuildProfile } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import { TrackingContext } from '../analytics/common';
import { CredentialsContext } from '../credentials/context';
import { Target } from '../credentials/ios/types';
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 { GradleBuildContext } from '../project/android/gradle';
import { XcodeBuildContext } from '../project/ios/scheme';
import { Actor } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
import { TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
import { LocalBuildOptions } from './local';
export interface ConfigureContext {
user: Actor;
@@ -27,6 +23,21 @@ export interface ConfigureContext {
hasIosNativeProject: boolean;
}
export type CommonContext<T extends Platform> = Omit<BuildContext<T>, 'android' | 'ios'>;
export interface AndroidBuildContext {
applicationId: string;
gradleContext?: GradleBuildContext;
}
export interface IosBuildContext {
bundleIdentifier: string;
applicationTargetBuildSettings: XCBuildConfiguration['buildSettings'];
applicationTarget: Target;
targets: Target[];
xcodeBuildContext: XcodeBuildContext;
}
export interface BuildContext<T extends Platform> {
accountName: string;
buildProfile: BuildProfile<T>;
@@ -34,7 +45,7 @@ export interface BuildContext<T extends Platform> {
clearCache: boolean;
credentialsCtx: CredentialsContext;
exp: ExpoConfig;
local: boolean;
localBuildOptions: LocalBuildOptions;
nonInteractive: boolean;
platform: T;
projectDir: string;
@@ -44,112 +55,6 @@ export interface BuildContext<T extends Platform> {
trackingCtx: TrackingContext;
user: Actor;
workflow: Workflow;
}
export async function createBuildContextAsync<T extends Platform>({
buildProfileName,
buildProfile,
clearCache = false,
local,
nonInteractive = false,
platform,
projectDir,
skipProjectConfiguration = false,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
clearCache: boolean;
local: boolean;
nonInteractive: boolean;
platform: T;
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;
}
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
ios: T extends Platform.IOS ? IosBuildContext : undefined;
}
+139
View File
@@ -0,0 +1,139 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import JsonFile from '@expo/json-file';
import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
import { TrackingContext } from '../analytics/common';
import { Analytics, BuildEvent } from '../analytics/events';
import { CredentialsContext } from '../credentials/context';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { findAccountByName } from '../user/Account';
import { ensureLoggedInAsync } from '../user/actions';
import { createAndroidContextAsync } from './android/build';
import { BuildContext, CommonContext } from './context';
import { createIosContextAsync } from './ios/build';
import { LocalBuildOptions } from './local';
export async function createBuildContextAsync<T extends Platform>({
buildProfileName,
buildProfile,
clearCache = false,
localBuildOptions,
nonInteractive = false,
platform,
projectDir,
skipProjectConfiguration = false,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
clearCache: boolean;
localBuildOptions: LocalBuildOptions;
nonInteractive: boolean;
platform: T;
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,
env: buildProfile.env,
});
const devClientProperties = getDevClientEventProperties({
platform,
projectDir,
buildProfile,
});
const trackingCtx = {
tracking_id: uuidv4(),
platform,
...(accountId && { account_id: accountId }),
project_id: projectId,
project_type: workflow,
...devClientProperties,
};
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
const commonContext: CommonContext<T> = {
accountName,
buildProfile,
buildProfileName,
clearCache,
credentialsCtx,
exp,
localBuildOptions,
nonInteractive,
platform,
projectDir,
projectId,
projectName,
skipProjectConfiguration,
trackingCtx,
user,
workflow,
};
if (platform === Platform.ANDROID) {
const common = commonContext as CommonContext<Platform.ANDROID>;
return {
...common,
android: await createAndroidContextAsync(common),
} as BuildContext<T>;
} else {
const common = commonContext as CommonContext<Platform.IOS>;
return {
...common,
ios: await createIosContextAsync(common),
} as BuildContext<T>;
}
}
function getDevClientEventProperties({
platform,
projectDir,
buildProfile,
}: {
platform: Platform;
projectDir: string;
buildProfile: BuildProfile;
}): 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;
}
}
@@ -4,9 +4,9 @@ import { IOSConfig } from '@expo/config-plugins';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
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);
@@ -59,7 +59,7 @@ async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
async function readExpoPlistAsync(projectDir: string): Promise<IOSConfig.ExpoPlist> {
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
return (await readPlistAsync(expoPlistPath)) as IOSConfig.ExpoPlist;
return ((await readPlistAsync(expoPlistPath)) ?? {}) as IOSConfig.ExpoPlist;
}
async function writeExpoPlistAsync(
@@ -4,7 +4,7 @@ import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { readPlistAsync } from '../plist';
import { readPlistAsync } from '../../../utils/plist';
import {
BumpStrategy,
bumpVersionAsync,
+31 -31
View File
@@ -8,24 +8,17 @@ import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../pro
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext } from '../context';
import { BuildContext, CommonContext, IosBuildContext } 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(
ctx: BuildContext<Platform.IOS>
): Promise<BuildRequestSender> {
export async function createIosContextAsync(
ctx: CommonContext<Platform.IOS>
): Promise<IosBuildContext> {
const { buildProfile } = ctx;
if (ctx.workflow === Workflow.MANAGED) {
@@ -50,29 +43,42 @@ export async function prepareIosBuildAsync(
},
xcodeBuildContext
);
const buildConfiguration = resolveBuildConfiguration(ctx, targets);
const applicationTarget = findApplicationTarget(targets);
const applicationTargetBuildSettings = resolveBuildSettings(ctx, applicationTarget);
return {
bundleIdentifier: applicationTarget.bundleIdentifier,
applicationTarget,
applicationTargetBuildSettings,
targets,
xcodeBuildContext,
};
}
export async function prepareIosBuildAsync(
ctx: BuildContext<Platform.IOS>
): Promise<BuildRequestSender> {
return await prepareBuildRequestForPlatformAsync({
ctx,
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
return ensureIosCredentialsAsync(ctx, targets);
return ensureIosCredentialsAsync(ctx, ctx.ios.targets);
},
ensureProjectConfiguredAsync: async () => {
await validateAndSyncProjectConfigurationAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile,
buildSettings: buildConfiguration.buildSettings,
buildProfile: ctx.buildProfile,
buildSettings: ctx.ios.applicationTargetBuildSettings,
});
},
getMetadataContext: (): IosMetadataContext => {
return buildConfiguration;
},
prepareJobAsync: async (
ctx: BuildContext<Platform.IOS>,
jobData: JobData<IosCredentials>
): Promise<Job> => {
return await prepareJobAsync(ctx, { ...jobData, buildScheme: xcodeBuildContext.buildScheme });
return await prepareJobAsync(ctx, {
...jobData,
buildScheme: ctx.ios.xcodeBuildContext.buildScheme,
});
},
sendBuildRequestAsync: async (
appId: string,
@@ -90,23 +96,17 @@ export async function prepareIosBuildAsync(
});
}
function resolveBuildConfiguration(
ctx: BuildContext<Platform.IOS>,
targets: Target[]
): BuildConfiguration {
function resolveBuildSettings(
ctx: CommonContext<Platform.IOS>,
applicationTarget: Target
): XCBuildConfiguration['buildSettings'] {
if (ctx.workflow === Workflow.MANAGED) {
return { buildSettings: {} };
return {};
}
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,
};
return xcBuildConfiguration?.buildSettings ?? {};
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { IosBuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
import { BuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import Log from '../../log';
@@ -36,7 +36,7 @@ export async function validateAndSyncProjectConfigurationAsync({
}: {
projectDir: string;
exp: ExpoConfig;
buildProfile: IosBuildProfile;
buildProfile: BuildProfile<Platform.IOS>;
buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
@@ -20,7 +20,11 @@ export async function ensureIosCredentialsAsync(
app: getAppFromContext(buildCtx.credentialsCtx),
targets,
iosCapabilitiesOptions: {
entitlements: await resolveEntitlementsJsonAsync(buildCtx.projectDir, buildCtx.workflow),
entitlements: await resolveEntitlementsJsonAsync(
buildCtx.projectDir,
buildCtx.workflow,
buildCtx.buildProfile.env ?? {}
),
},
distribution: buildCtx.buildProfile.distribution ?? 'store',
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
+13 -4
View File
@@ -8,9 +8,9 @@ import type { XCBuildConfiguration } from 'xcode';
import Log from '../../log';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync } from '../utils/version';
import { readPlistAsync, writePlistAsync } from './plist';
export enum BumpStrategy {
APP_VERSION,
@@ -128,7 +128,11 @@ export async function maybeResolveVersionsAsync(
appBuildVersion: await readBuildNumberAsync(projectDir, exp, buildSettings),
appVersion: await readShortVersionAsync(projectDir, exp, buildSettings),
};
} catch {
} catch (err: any) {
Log.warn('Failed to read app versions.');
Log.debug(err);
Log.warn(err.message);
Log.warn('Proceeding anyway...');
return {};
}
}
@@ -174,7 +178,7 @@ async function readInfoPlistAsync(
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<IOSConfig.InfoPlist> {
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
return (await readPlistAsync(infoPlistPath)) as IOSConfig.InfoPlist;
return ((await readPlistAsync(infoPlistPath)) ?? {}) as IOSConfig.InfoPlist;
}
async function writeInfoPlistAsync({
@@ -197,7 +201,12 @@ function ensureStaticConfigExists(projectDir: string): void {
}
}
export function evaluateTemplateString(s: string, vars: Record<string, any>): string {
export function evaluateTemplateString(
s: string,
buildSettings: XCBuildConfiguration['buildSettings']
): string {
// necessary because XCBuildConfiguration['buildSettings'] is not a plain object
const vars = { ...buildSettings };
return s.replace(/\$\((\w+)\)/g, (match, key) => {
if (vars.hasOwnProperty(key)) {
const value = String(vars[key]);
+66 -33
View File
@@ -1,45 +1,78 @@
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 { ChildProcess } from 'child_process';
import semver from 'semver';
import Log from '../log';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.56';
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;
export interface LocalBuildOptions {
enable: boolean;
skipCleanup?: boolean;
skipNativeBuild?: boolean;
artifactsDir?: string;
workingdir?: string;
verbose?: boolean;
}
export async function runLocalBuildAsync(job: Job, options: LocalBuildOptions): Promise<void> {
const { command, args } = await getCommandAndArgsAsync(job);
let spinner;
if (!options.verbose) {
spinner = ora().start(options.skipNativeBuild ? 'Preparing project' : 'Building project');
}
let childProcess: ChildProcess | undefined;
const interruptHandler = (): void => {
if (childProcess) {
childProcess.kill();
}
};
process.on('SIGINT', interruptHandler);
try {
const spawnPromise = spawnAsync(command, args, {
stdio: options.verbose ? 'inherit' : 'pipe',
env: {
...process.env,
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
...(options.skipCleanup || options.skipNativeBuild
? { EAS_LOCAL_BUILD_SKIP_CLEANUP: '1' }
: {}),
...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}),
...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}),
},
});
childProcess = spawnPromise.child;
await spawnPromise;
} finally {
process.removeListener('SIGINT', interruptHandler);
spinner?.stop();
}
}
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;
async function getCommandAndArgsAsync(job: Job): Promise<{ command: string; args: string[] }> {
const jobBase64 = Buffer.from(JSON.stringify({ job })).toString('base64');
if (process.env.EAS_LOCAL_BUILD_PLUGIN_PATH) {
return {
command: process.env.EAS_LOCAL_BUILD_PLUGIN_PATH,
args: [jobBase64],
};
} else {
const args = [`${PLUGIN_PACKAGE_NAME}@${PLUGIN_PACKAGE_VERSION}`, jobBase64];
if (await isAtLeastNpm7Async()) {
// npx shipped with npm >= 7.0.0 requires the "-y" flag to run commands without
// prompting the user to install a package that is used for the first time
args.unshift('-y');
}
} catch {}
return undefined;
return {
command: 'npx',
args,
};
}
}
async function isAtLeastNpm7Async(): Promise<boolean> {
const version = (await spawnAsync('npm', ['--version'])).stdout.trim();
return semver.gte(version, '7.0.0');
}
+38 -35
View File
@@ -1,10 +1,10 @@
import { Updates } from '@expo/config-plugins';
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
import { IosEnterpriseProvisioning } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import fs from 'fs-extra';
import resolveFrom from 'resolve-from';
import { getApplicationIdAsync } from '../project/android/applicationId';
import { GradleBuildContext } from '../project/android/gradle';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
import Log from '../log';
import { getUsername } from '../project/projectUtils';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
@@ -22,22 +22,20 @@ import {
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
import { isExpoUpdatesInstalled } from './utils/updates';
export type MetadataContext<T extends Platform> = T extends Platform.ANDROID
? AndroidMetadataContext
: IosMetadataContext;
export interface AndroidMetadataContext {
gradleContext?: GradleBuildContext;
}
export interface IosMetadataContext {
buildSettings: XCBuildConfiguration['buildSettings'];
targetName?: string;
buildConfiguration?: string;
// TODO(JJ): Replace this with the getRuntimeVersionNullable function in @expo/config-plugins
function getRuntimeVersionNullable(
...[config, platform]: Parameters<typeof Updates.getRuntimeVersion>
): string | null {
try {
return Updates.getRuntimeVersion(config, platform);
} catch (e) {
Log.debug(e);
return null;
}
}
export async function collectMetadataAsync<T extends Platform>(
ctx: BuildContext<T>,
platformContext: MetadataContext<T>
ctx: BuildContext<T>
): Promise<Metadata> {
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
const distribution =
@@ -46,16 +44,17 @@ export async function collectMetadataAsync<T extends Platform>(
: ctx.buildProfile.distribution) ?? 'store';
const metadata = {
trackingContext: ctx.trackingCtx,
...(await maybeResolveVersionsAsync(ctx, platformContext)),
...(await maybeResolveVersionsAsync(ctx)),
cliVersion: easCliVersion,
workflow: ctx.workflow,
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: ctx.exp.runtimeVersion,
runtimeVersion: getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
reactNativeVersion: await getReactNativeVersionAsync(ctx),
...channelOrReleaseChannel,
distribution,
appName: ctx.exp.name,
appIdentifier: await resolveAppIdentifierAsync(ctx, platformContext),
appIdentifier: resolveAppIdentifier(ctx),
buildProfile: ctx.buildProfileName,
gitCommitHash: await getVcsClient().getCommitHashAsync(),
isGitWorkingTreeDirty: await getVcsClient().hasUncommittedChangesAsync(),
@@ -70,15 +69,14 @@ export async function collectMetadataAsync<T extends Platform>(
}
async function maybeResolveVersionsAsync<T extends Platform>(
ctx: BuildContext<T>,
platformContext: MetadataContext<T>
ctx: BuildContext<T>
): Promise<{ appBuildVersion?: string; appVersion?: string }> {
if (ctx.platform === Platform.IOS) {
const iosContext = platformContext as IosMetadataContext;
const iosContext = ctx as BuildContext<Platform.IOS>;
return await maybeResolveIosVersionsAsync(
ctx.projectDir,
ctx.exp,
iosContext?.buildSettings ?? {}
iosContext.ios.applicationTargetBuildSettings
);
} else if (ctx.platform === Platform.ANDROID) {
const androidCtx = ctx as BuildContext<Platform.ANDROID>;
@@ -88,19 +86,11 @@ async function maybeResolveVersionsAsync<T extends Platform>(
}
}
async function resolveAppIdentifierAsync<T extends Platform>(
ctx: BuildContext<T>,
platformContext: MetadataContext<T>
): Promise<string> {
function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string {
if (ctx.platform === Platform.IOS) {
const iosContext = platformContext as IosMetadataContext;
return await getBundleIdentifierAsync(ctx.projectDir, ctx.exp, {
targetName: iosContext.targetName,
buildConfiguration: iosContext.buildConfiguration,
});
return (ctx as BuildContext<Platform.IOS>).ios.bundleIdentifier;
} else {
const androidContext = platformContext as AndroidMetadataContext;
return await getApplicationIdAsync(ctx.projectDir, ctx.exp, androidContext.gradleContext);
return (ctx as BuildContext<Platform.ANDROID>).android.applicationId;
}
}
@@ -154,6 +144,19 @@ async function getNativeChannelAsync<T extends Platform>(
return undefined;
}
async function getReactNativeVersionAsync(
ctx: BuildContext<Platform>
): Promise<string | undefined> {
try {
const reactNativePackageJsonPath = resolveFrom(ctx.projectDir, 'react-native/package.json');
return (await fs.readJson(reactNativePackageJsonPath)).version;
} catch (err) {
Log.debug('Failed to resolve react-native version:');
Log.debug(err);
return undefined;
}
}
function resolveIosEnterpriseProvisioning(
ctx: BuildContext<Platform.IOS>
): IosEnterpriseProvisioning | undefined {
@@ -0,0 +1,257 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, SubmitProfile } from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { AppPlatform, BuildFragment, BuildStatus, SubmissionFragment } from '../graphql/generated';
import { toAppPlatform, toPlatform } from '../graphql/types/AppPlatform';
import Log from '../log';
import {
RequestedPlatform,
appPlatformDisplayNames,
appPlatformEmojis,
toPlatforms,
} from '../platform';
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
import { createSubmissionContextAsync } from '../submit/context';
import {
submitAsync,
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { ProfileData, getProfilesAsync } from '../utils/profiles';
import { getVcsClient } from '../vcs';
import { prepareAndroidBuildAsync } from './android/build';
import { BuildRequestSender, waitForBuildEndAsync } from './build';
import { ensureProjectConfiguredAsync } from './configure';
import { BuildContext } from './context';
import { createBuildContextAsync } from './createContext';
import { prepareIosBuildAsync } from './ios/build';
import { LocalBuildOptions } from './local';
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
import { ensureRepoIsCleanAsync } from './utils/repository';
let metroConfigValidated = false;
let sdkVersionChecked = false;
export interface BuildFlags {
requestedPlatform: RequestedPlatform;
skipProjectConfiguration: boolean;
profile?: string;
nonInteractive: boolean;
wait: boolean;
clearCache: boolean;
json: boolean;
autoSubmit: boolean;
submitProfile?: string;
localBuildOptions: LocalBuildOptions;
}
export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFlags): Promise<void> {
await getVcsClient().ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
await ensureProjectConfiguredAsync(projectDir, flags.requestedPlatform);
const platforms = toPlatforms(flags.requestedPlatform);
const buildProfiles = await getProfilesAsync({
type: 'build',
projectDir,
platforms,
profileName: flags.profile ?? undefined,
});
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir,
nonInteractive: flags.nonInteractive,
buildProfiles,
});
const startedBuilds: {
build: BuildFragment;
buildProfile: ProfileData<'build'>;
}[] = [];
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
for (const buildProfile of buildProfiles) {
const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
projectDir,
flags,
moreBuilds: platforms.length > 1,
buildProfile,
});
if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile });
}
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
}
if (flags.localBuildOptions.enable) {
return;
}
Log.newLine();
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
Log.newLine();
const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) {
const submitProfiles = await getProfilesAsync({
projectDir,
platforms,
profileName: flags.submitProfile,
type: 'submit',
});
for (const startedBuild of startedBuilds) {
const submitProfile = nullthrows(
submitProfiles.find(
({ platform }) => toAppPlatform(platform) === startedBuild.build.platform
)
).profile;
const submission = await prepareAndStartSubmissionAsync({
build: startedBuild.build,
buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]),
moreBuilds: startedBuilds.length > 1,
projectDir,
buildProfile: startedBuild.buildProfile.profile,
submitProfile,
nonInteractive: flags.nonInteractive,
});
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) {
exitWithNonZeroCodeIfSomeBuildsFailed(builds);
} else {
// the following function also exits with non zero code if any of the submissions failed
await waitForSubmissionsToCompleteAsync(submissions);
}
}
async function prepareAndStartBuildAsync({
projectDir,
flags,
moreBuilds,
buildProfile,
}: {
projectDir: string;
flags: BuildFlags;
moreBuilds: boolean;
buildProfile: ProfileData<'build'>;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName,
clearCache: flags.clearCache,
buildProfile: buildProfile.profile,
nonInteractive: flags.nonInteractive,
platform: buildProfile.platform,
projectDir,
skipProjectConfiguration: flags.skipProjectConfiguration,
localBuildOptions: flags.localBuildOptions,
});
if (moreBuilds) {
Log.newLine();
const appPlatform = toAppPlatform(buildProfile.platform);
Log.log(
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
`${appPlatformDisplayNames[appPlatform]} build`
)}`
);
}
if (buildCtx.workflow === Workflow.MANAGED) {
if (!sdkVersionChecked) {
await checkExpoSdkIsSupportedAsync(buildCtx);
sdkVersionChecked = true;
}
if (!metroConfigValidated) {
await validateMetroConfigForManagedWorkflowAsync(buildCtx);
metroConfigValidated = true;
}
}
const build = await startBuildAsync(buildCtx);
return {
build,
buildCtx,
};
}
async function 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();
}
async function prepareAndStartSubmissionAsync({
build,
buildCtx,
moreBuilds,
projectDir,
buildProfile,
submitProfile,
nonInteractive,
}: {
build: BuildFragment;
buildCtx: BuildContext<Platform>;
moreBuilds: boolean;
projectDir: string;
buildProfile: BuildProfile;
submitProfile: SubmitProfile;
nonInteractive: boolean;
}): Promise<SubmissionFragment> {
const platform = toPlatform(build.platform);
const submissionCtx = await createSubmissionContextAsync({
platform,
projectDir,
projectId: build.project.id,
profile: submitProfile,
archiveFlags: { id: build.id },
nonInteractive,
env: buildProfile.env,
credentialsCtx: buildCtx.credentialsCtx,
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
});
if (moreBuilds) {
Log.newLine();
Log.log(
`${appPlatformEmojis[build.platform]} ${chalk.bold(
`${appPlatformDisplayNames[build.platform]} submission`
)}`
);
}
return await submitAsync(submissionCtx);
}
function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
i => i.status === BuildStatus.Errored
);
if (failedBuilds.length > 0) {
process.exit(1);
}
}
-2
View File
@@ -12,5 +12,3 @@ export enum BuildDistributionType {
INTERNAL = 'internal',
SIMULATOR = 'simulator',
}
export type TrackingContext = Record<string, string | number | boolean>;
@@ -1,29 +0,0 @@
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(name: Event, properties: Record<string, any>) {
logEvent(name, properties);
},
};
@@ -1,6 +1,5 @@
import { Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { error } from '@oclif/errors';
import { Errors } from '@oclif/core';
import chalk from 'chalk';
import resolveFrom from 'resolve-from';
@@ -21,7 +20,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
}: {
projectDir: string;
nonInteractive?: boolean;
buildProfiles?: ProfileData<BuildProfile>[];
buildProfiles?: ProfileData<'build'>[];
}): Promise<void> {
if (await isExpoDevClientInstalledAsync(projectDir)) {
return;
@@ -65,7 +64,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
if (install) {
await installExpoDevClientAsync(projectDir, { nonInteractive });
} else {
error(`Install ${chalk.bold('expo-dev-client')} manually and come back later.`, {
Errors.error(`Install ${chalk.bold('expo-dev-client')} manually and come back later.`, {
exit: 1,
});
}
@@ -84,7 +83,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
initial: false,
});
if (!shouldContinue) {
error('Come back later', { exit: 1 });
Errors.error('Come back later', { exit: 1 });
}
}
}
@@ -136,6 +136,7 @@ export async function reviewAndCommitChangesAsync(
await getVcsClient().commitAsync({
commitMessage: initialCommitMessage,
commitAllFiles: false,
nonInteractive,
});
Log.withTick('Committed changes.');
return;
+1 -1
View File
@@ -22,7 +22,7 @@ export function getArtifactUrl(artifactId: string): string {
export function getInternalDistributionInstallUrl(build: BuildFragment): string {
if (build.platform === AppPlatform.Ios) {
return `itms-services://?action=download-manifest;url=${getExpoApiBaseUrl()}/--/api/v2/projects/${
return `itms-services://?action=download-manifest;url=${getExpoApiBaseUrl()}/v2/projects/${
build.project.id
}/builds/${build.id}/manifest.plist`;
}
+3 -3
View File
@@ -4,9 +4,9 @@ import path from 'path';
import Log, { learnMore } from '../log';
import { getVcsClient } from '../vcs';
import { BuildContext } from './context';
import { CommonContext } from './context';
export function checkNodeEnvVariable(ctx: BuildContext<Platform>): void {
export function checkNodeEnvVariable(ctx: CommonContext<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.'
@@ -16,7 +16,7 @@ export function checkNodeEnvVariable(ctx: BuildContext<Platform>): void {
}
export async function checkGoogleServicesFileAsync<T extends Platform>(
ctx: BuildContext<T>
ctx: CommonContext<T>
): Promise<void> {
if (ctx.workflow === Workflow.GENERIC || ctx.buildProfile?.env?.GOOGLE_SERVICES_FILE) {
return;
@@ -1,5 +1,5 @@
import { EasJsonReader } from '@expo/eas-json';
import { Command } from '@oclif/command';
import { Command } from '@oclif/core';
import semver from 'semver';
import {
@@ -7,8 +7,7 @@ import {
flushAsync as flushAnalyticsAsync,
initAsync as initAnalyticsAsync,
logEvent,
} from '../analytics';
import Log from '../log';
} from '../analytics/rudderstackClient';
import { findProjectRootAsync } from '../project/projectUtils';
import { getUserAsync } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
@@ -22,18 +21,19 @@ export default abstract class EasCommand extends Command {
* force the user to log in
*/
protected requiresAuthentication = true;
protected mustBeRunInsideProject = 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.mustBeRunInsideProject) {
await this.applyCliConfigAsync();
}
if (this.requiresAuthentication) {
const { flags } = this.parse();
const { flags } = await this.parse();
const nonInteractive = (flags as any)['non-interactive'] ?? false;
await ensureLoggedInAsync({ nonInteractive });
} else {
@@ -1,5 +1,5 @@
import { asMock } from '../../__tests__/utils';
import { flushAsync, initAsync, logEvent } from '../../analytics';
import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClient';
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
import { getUserAsync } from '../../user/User';
import { ensureLoggedInAsync } from '../../user/actions';
@@ -10,8 +10,8 @@ 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');
jest.mock('../../analytics/rudderstackClient', () => {
const { AnalyticsEvent } = jest.requireActual('../../analytics/rudderstackClient');
return {
AnalyticsEvent,
logEvent: jest.fn(),
@@ -6,6 +6,7 @@ export default class AccountLogin extends EasCommand {
static description = 'log in with your Expo account';
static aliases = ['login'];
protected mustBeRunInsideProject = false;
protected requiresAuthentication = false;
async runAsync(): Promise<void> {
@@ -6,6 +6,7 @@ export default class AccountLogout extends EasCommand {
static description = 'log out';
static aliases = ['logout'];
protected mustBeRunInsideProject = false;
protected requiresAuthentication = false;
async runAsync(): Promise<void> {
@@ -8,6 +8,7 @@ export default class AccountView extends EasCommand {
static description = 'show the username you are logged in as';
static aliases = ['whoami'];
protected mustBeRunInsideProject = false;
protected requiresAuthentication = false;
async runAsync(): Promise<void> {
+1 -1
View File
@@ -10,7 +10,7 @@ export default class AnalyticsView extends EasCommand {
protected requiresAuthentication = false;
async runAsync(): Promise<void> {
const { STATUS: status } = this.parse(AnalyticsView).args;
const { STATUS: status } = (await this.parse(AnalyticsView)).args;
if (status) {
await UserSettings.setAsync('analyticsEnabled', status === 'on');
Log.withTick(`${status === 'on' ? 'Enabling' : 'Disabling'} analytics.`);
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
@@ -51,7 +51,6 @@ export async function createUpdateBranchOnAppAsync({
}
export default class BranchCreate extends EasCommand {
static hidden = true;
static description = 'Create a branch on the current project.';
static args = [
@@ -63,7 +62,7 @@ export default class BranchCreate extends EasCommand {
];
static flags = {
json: flags.boolean({
json: Flags.boolean({
description: 'return a json with the new branch ID and name.',
default: false,
}),
@@ -73,7 +72,7 @@ export default class BranchCreate extends EasCommand {
let {
args: { name },
flags,
} = this.parse(BranchCreate);
} = await this.parse(BranchCreate);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
@@ -76,8 +76,7 @@ async function deleteBranchOnAppAsync({
}
export default class BranchDelete extends EasCommand {
static hidden = true;
static description = 'Republish an update group';
static description = 'Delete a branch on the current project';
static args = [
{
@@ -87,7 +86,7 @@ export default class BranchDelete extends EasCommand {
},
];
static flags = {
json: flags.boolean({
json: Flags.boolean({
description: `return JSON with the edited branch's ID and name.`,
default: false,
}),
@@ -97,7 +96,7 @@ export default class BranchDelete extends EasCommand {
let {
args: { name },
flags: { json: jsonFlag },
} = this.parse(BranchDelete);
} = await this.parse(BranchDelete);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
+4 -6
View File
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import CliTable from 'cli-table3';
import { print } from 'graphql';
@@ -54,19 +54,17 @@ export async function listBranchesAsync({
}
export default class BranchList extends EasCommand {
static hidden = true;
static description = 'List all branches on this project.';
static flags = {
json: flags.boolean({
json: Flags.boolean({
description: 'return output as JSON',
default: false,
}),
};
async runAsync(): Promise<void> {
const { flags } = this.parse(BranchList);
const { flags } = await this.parse(BranchList);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -76,7 +74,7 @@ export default class BranchList extends EasCommand {
Log.log(JSON.stringify(branches, null, 2));
} else {
const table = new CliTable({
head: ['branch', ...UPDATE_COLUMNS],
head: ['Branch', ...UPDATE_COLUMNS],
wordWrap: true,
});
+2 -435
View File
@@ -1,443 +1,10 @@
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 description = 'deprecated, use "eas update"';
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();
}
}
throw new Error(BranchPublish.description);
}
}
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}`;
}
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
@@ -51,19 +51,18 @@ async function renameUpdateBranchOnAppAsync({
}
export default class BranchRename extends EasCommand {
static hidden = true;
static description = 'Rename a branch.';
static flags = {
from: flags.string({
from: Flags.string({
description: 'current name of the branch.',
required: false,
}),
to: flags.string({
to: Flags.string({
description: 'new name of the branch.',
required: false,
}),
json: flags.boolean({
json: Flags.boolean({
description: `return a json with the edited branch's ID and name.`,
default: false,
}),
@@ -72,7 +71,7 @@ export default class BranchRename extends EasCommand {
async runAsync(): Promise<void> {
let {
flags: { json: jsonFlag, from: currentName, to: newName },
} = this.parse(BranchRename);
} = await this.parse(BranchRename);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
+3 -4
View File
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import Table from 'cli-table3';
import gql from 'graphql-tag';
@@ -67,7 +67,6 @@ export async function viewUpdateBranchAsync({
}
export default class BranchView extends EasCommand {
static hidden = true;
static description = 'View a branch.';
static args = [
@@ -79,7 +78,7 @@ export default class BranchView extends EasCommand {
];
static flags = {
json: flags.boolean({
json: Flags.boolean({
description: `return a json with the branch's ID name and recent update groups.`,
default: false,
}),
@@ -89,7 +88,7 @@ export default class BranchView extends EasCommand {
let {
args: { name },
flags: { json: jsonFlag },
} = this.parse(BranchView);
} = await this.parse(BranchView);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -115,7 +115,7 @@ export default class BuildCancel extends EasCommand {
static args = [{ name: 'BUILD_ID' }];
async runAsync(): Promise<void> {
const { BUILD_ID: buildIdFromArg } = this.parse(BuildCancel).args;
const { BUILD_ID: buildIdFromArg } = (await this.parse(BuildCancel)).args;
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -1,4 +1,4 @@
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { configureAsync } from '../../build/configure';
@@ -7,12 +7,13 @@ import Log, { learnMore } from '../../log';
import { RequestedPlatform } from '../../platform';
import { findProjectRootAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { getVcsClient } from '../../vcs';
export default class BuildConfigure extends EasCommand {
static description = 'Configure the project to support EAS Build.';
static flags = {
platform: flags.enum({
platform: Flags.enum({
description: 'Platform to configure',
char: 'p',
options: ['android', 'ios', 'all'],
@@ -20,12 +21,15 @@ export default class BuildConfigure extends EasCommand {
};
async runAsync(): Promise<void> {
const { flags } = this.parse(BuildConfigure);
const { flags } = await this.parse(BuildConfigure);
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();
await getVcsClient().ensureRepoExistsAsync();
const platform =
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
@@ -48,7 +52,7 @@ function logSuccess(platform: RequestedPlatform): void {
storesText = 'the Google Play Store';
} else if (platform === 'ios') {
platformsText = 'iOS project is';
storesText = 'the Apple App Store';
storesText = 'the Apple App Store Connect';
}
Log.log(`🎉 Your ${platformsText} ready to build.
+47 -274
View File
@@ -1,53 +1,18 @@
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 { EasJsonReader } from '@expo/eas-json';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import figures from 'figures';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
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 } from '../../build/utils/repository';
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
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,
} from '../../project/isEasEnabledForProject';
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
import { RequestedPlatform, selectRequestedPlatformAsync } from '../../platform';
import { findProjectRootAsync } from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
import { createSubmissionContextAsync } from '../../submit/context';
import {
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';
@@ -65,194 +30,96 @@ interface RawBuildFlags {
'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 description = 'Start a build';
static flags = {
platform: flags.enum({
platform: Flags.enum({
char: 'p',
options: ['android', 'ios', 'all'],
}),
'skip-credentials-check': flags.boolean({
'skip-credentials-check': Flags.boolean({
default: false,
hidden: true,
}),
json: flags.boolean({
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
default: false,
}),
'skip-project-configuration': flags.boolean({
'skip-project-configuration': Flags.boolean({
default: false,
description: 'Skip project configuration',
}),
profile: flags.string({
profile: Flags.string({
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
}),
'non-interactive': flags.boolean({
'non-interactive': Flags.boolean({
default: false,
description: 'Run command in non-interactive mode',
}),
local: flags.boolean({
local: Flags.boolean({
default: false,
description: 'Run build locally [experimental]',
}),
wait: flags.boolean({
wait: Flags.boolean({
default: true,
allowNo: true,
description: 'Wait for build(s) to complete',
}),
'clear-cache': flags.boolean({
'clear-cache': Flags.boolean({
default: false,
description: 'Clear cache before the build',
}),
'auto-submit': flags.boolean({
'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({
'auto-submit-with-profile': Flags.string({
description: 'Submit on build complete using the submit profile with provided name',
helpValue: 'PROFILE_NAME',
exclusive: ['auto-submit'],
}),
};
private metroConfigValidated = false;
async runAsync(): Promise<void> {
const { flags: rawFlags } = this.parse(Build);
const { flags: rawFlags } = await 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);
}
await runBuildAndSubmitAsync(projectDir, flags);
}
private async sanitizeFlagsAsync(flags: RawBuildFlags): Promise<BuildFlags> {
const nonInteractive = flags['non-interactive'];
if (!flags.platform && nonInteractive) {
error('--platform is required when building in non-interactive mode', { exit: 1 });
Errors.error('--platform is required when building in non-interactive mode', { exit: 1 });
}
if (flags.json && !nonInteractive) {
error('--json is allowed only when building in non-interactive mode', { exit: 1 });
Errors.error('--json is allowed only when building in non-interactive mode', { exit: 1 });
}
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 });
Errors.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 });
Errors.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 });
Errors.error('Unsupported platform, macOS is required to build apps for iOS', { exit: 1 });
}
}
@@ -268,135 +135,32 @@ export default class Build extends EasCommand {
return {
requestedPlatform,
skipProjectConfiguration: flags['skip-project-configuration'],
profile: profile ?? null,
profile,
nonInteractive,
local: flags['local'],
localBuildOptions: flags['local']
? { enable: true, verbose: true }
: {
enable: false,
},
wait: flags['wait'],
clearCache: flags['clear-cache'],
json: flags['json'],
autoSubmit: flags['auto-submit'],
autoSubmit: flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined,
submitProfile: flags['auto-submit-with-profile'] ?? profile,
};
}
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,
skipProjectConfiguration: flags.skipProjectConfiguration,
});
if (moreBuilds) {
Log.newLine();
const appPlatform = toAppPlatform(buildProfile.platform);
Log.log(
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
`${appPlatformDisplayNames[appPlatform]} build`
)}`
);
}
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> {
if (!(await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir)))) {
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
if (!(await fs.pathExists(easJsonPath))) {
return;
}
const easJsonReader = new EasJsonReader(projectDir);
const rawEasJson = await easJsonReader.readRawAsync();
const rawEasJson = await easJsonReader.readAsync();
if (rawEasJson?.cli) {
return;
}
@@ -432,12 +196,21 @@ export async function handleDeprecatedEasJsonAsync(
{ 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(EasJsonReader.formatEasJsonPath(projectDir), rawEasJson, { spaces: 2 });
await fs.writeJSON(easJsonPath, rawEasJson, { spaces: 2 });
Log.withTick('Updated eas.json');
if (mode === 'requireCommit') {
setVcsClient(new GitClient());
await getVcsClient().trackFileAsync(easJsonPath);
await reviewAndCommitChangesAsync('Set cli.requireCommit to true in eas.json', {
nonInteractive,
});
}
}
@@ -0,0 +1,128 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import EasCommand from '../../commandUtils/EasCommand';
import Log from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import { findProjectRootAsync } from '../../project/projectUtils';
import { getTmpDirectory } from '../../utils/paths';
import { getVcsClient } from '../../vcs';
enum InspectStage {
ARCHIVE = 'archive',
PRE_BUILD = 'pre-build',
POST_BUILD = 'post-build',
}
const STAGE_DESCRIPTION = `Stage of the build you want to inspect.
archive - builds the project archive that would be uploaded to EAS when building
pre-build - prepares the project to be built with Gradle/Xcode. Does not run the native build.
post-build - builds the native project and leaves the output directory for inspection`;
export default class BuildInspect extends EasCommand {
static description =
'Inspect the state of the project at specific build stages. Useful for troubleshooting.';
static flags = {
platform: Flags.enum({
char: 'p',
options: [RequestedPlatform.Android, RequestedPlatform.Ios],
required: true,
}),
profile: Flags.string({
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
}),
stage: Flags.enum({
char: 's',
description: STAGE_DESCRIPTION,
options: [InspectStage.ARCHIVE, InspectStage.PRE_BUILD, InspectStage.POST_BUILD],
required: true,
}),
output: Flags.string({
description: 'Output directory.',
required: true,
helpValue: 'OUTPUT_DIRECTORY',
}),
force: Flags.boolean({
description: 'Delete OUTPUT_DIRECTORY if it already exists.',
default: false,
}),
verbose: Flags.boolean({
default: false,
}),
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildInspect);
const outputDirectory = path.resolve(process.cwd(), flags.output);
await this.prepareOutputDirAsync(outputDirectory, flags.force);
if (flags.stage === InspectStage.ARCHIVE) {
const vcs = getVcsClient();
await vcs.ensureRepoExistsAsync();
await vcs.makeShallowCopyAsync(outputDirectory);
Log.withTick(`Project saved to ${outputDirectory}`);
} else {
const projectDir = await findProjectRootAsync();
const tmpWorkingdir = path.join(getTmpDirectory(), uuidv4());
try {
await runBuildAndSubmitAsync(projectDir, {
skipProjectConfiguration: false,
nonInteractive: false,
wait: true,
clearCache: false,
json: false,
autoSubmit: false,
requestedPlatform: flags.platform,
profile: flags.profile,
localBuildOptions: {
enable: true,
...(flags.stage === InspectStage.PRE_BUILD ? { skipNativeBuild: true } : {}),
...(flags.stage === InspectStage.POST_BUILD ? { skipCleanup: true } : {}),
verbose: flags.verbose,
workingdir: tmpWorkingdir,
artifactsDir: path.join(tmpWorkingdir, 'artifacts'),
},
});
if (!flags.verbose) {
Log.log(chalk.green('Build successful'));
}
} catch (err) {
if (!flags.verbose) {
Log.error('Build failed');
Log.error(`Re-run this command with ${chalk.bold('--verbose')} flag to see the logs`);
}
} finally {
const spinner = ora().start(`Copying project build directory to ${outputDirectory}`);
try {
const tmpBuildDirectory = path.join(tmpWorkingdir, 'build');
if (await fs.pathExists(tmpBuildDirectory)) {
await fs.copy(tmpBuildDirectory, outputDirectory);
}
await fs.remove(tmpWorkingdir);
spinner.succeed(`Project build directory saved to ${outputDirectory}`);
} catch (err) {
spinner.fail();
throw err;
}
}
}
}
private async prepareOutputDirAsync(outputDir: string, force: boolean): Promise<void> {
if (await fs.pathExists(outputDir)) {
if (force) {
await fs.remove(outputDir);
} else {
throw new Error(`Directory ${outputDir} already exists`);
}
}
await fs.mkdirp(outputDir);
}
}
+15 -15
View File
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { BuildDistributionType, BuildStatus } from '../../build/types';
@@ -25,13 +25,13 @@ export default class BuildList extends EasCommand {
static description = 'list all builds for your project';
static flags = {
platform: flags.enum({
platform: Flags.enum({
options: [RequestedPlatform.All, RequestedPlatform.Android, RequestedPlatform.Ios],
}),
json: flags.boolean({
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
}),
status: flags.enum({
status: Flags.enum({
options: [
BuildStatus.NEW,
BuildStatus.IN_QUEUE,
@@ -41,26 +41,26 @@ export default class BuildList extends EasCommand {
BuildStatus.CANCELED,
],
}),
distribution: flags.enum({
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(),
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 runAsync(): Promise<void> {
const { flags } = this.parse(BuildList);
const { flags } = await this.parse(BuildList);
const {
json,
platform: requestedPlatform,
+3 -3
View File
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
import EasCommand from '../../commandUtils/EasCommand';
@@ -20,7 +20,7 @@ export default class BuildView extends EasCommand {
static args = [{ name: 'BUILD_ID' }];
static flags = {
json: flags.boolean({
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
}),
};
@@ -29,7 +29,7 @@ export default class BuildView extends EasCommand {
const {
args: { BUILD_ID: buildId },
flags,
} = this.parse(BuildView);
} = await this.parse(BuildView);
if (flags.json) {
enableJsonOutput();
}
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
@@ -20,7 +20,7 @@ import { promptAsync } from '../../prompts';
import formatFields from '../../utils/formatFields';
import { createUpdateBranchOnAppAsync } from '../branch/create';
async function createUpdateChannelOnAppAsync({
export async function createUpdateChannelOnAppAsync({
appId,
channelName,
branchId,
@@ -59,7 +59,6 @@ async function createUpdateChannelOnAppAsync({
}
export default class ChannelCreate extends EasCommand {
static hidden = true;
static description = 'Create a channel on the current project.';
static args = [
@@ -71,7 +70,7 @@ export default class ChannelCreate extends EasCommand {
];
static flags = {
json: flags.boolean({
json: Flags.boolean({
description:
'print output as a JSON object with the new channel ID, name and branch mapping.',
default: false,
@@ -82,7 +81,7 @@ export default class ChannelCreate extends EasCommand {
let {
args: { name: channelName },
flags: { json: jsonFlag },
} = this.parse(ChannelCreate);
} = await this.parse(ChannelCreate);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
@@ -89,13 +89,12 @@ export async function updateChannelBranchMappingAsync({
);
const channel = data.updateChannel.editUpdateChannel;
if (!channel) {
throw new Error(`Could not fine channel with id ${channelId}`);
throw new Error(`Could not find a channel with id: ${channelId}`);
}
return data.updateChannel.editUpdateChannel!;
}
export default class ChannelEdit extends EasCommand {
static hidden = true;
static description = 'Point a channel at a new branch.';
static args = [
@@ -107,10 +106,10 @@ export default class ChannelEdit extends EasCommand {
];
static flags = {
branch: flags.string({
branch: Flags.string({
description: 'Name of the branch to point to',
}),
json: flags.boolean({
json: Flags.boolean({
description: 'print output as a JSON object with the channel ID, name and branch mapping.',
default: false,
}),
@@ -120,7 +119,7 @@ export default class ChannelEdit extends EasCommand {
let {
args: { name: channelName },
flags: { branch: branchName, json: jsonFlag },
} = this.parse(ChannelEdit);
} = await this.parse(ChannelEdit);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
@@ -66,11 +66,10 @@ async function getAllUpdateChannelForAppAsync({
}
export default class ChannelList extends EasCommand {
static hidden = true;
static description = 'List all channels on the current project.';
static flags = {
json: flags.boolean({
json: Flags.boolean({
description: 'print output as a JSON object with the channel ID, name and branch mapping.',
default: false,
}),
@@ -79,7 +78,7 @@ export default class ChannelList extends EasCommand {
async runAsync(): Promise<void> {
const {
flags: { json: jsonFlag },
} = this.parse(ChannelList);
} = await this.parse(ChannelList);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
@@ -319,19 +319,19 @@ export default class ChannelRollout extends EasCommand {
];
static flags = {
branch: flags.string({
branch: Flags.string({
description: 'branch to rollout',
required: false,
}),
percent: flags.integer({
percent: Flags.integer({
description: 'percent of traffic to redirect to the new branch',
required: false,
}),
end: flags.boolean({
end: Flags.boolean({
description: 'end the rollout',
default: false,
}),
json: flags.boolean({
json: Flags.boolean({
description: 'print output as a JSON object with the new channel ID, name and branch mapping',
default: false,
}),
@@ -340,11 +340,8 @@ export default class ChannelRollout extends EasCommand {
async runAsync(): Promise<void> {
const {
args: { channel: channelName },
flags: { json: jsonFlag, end: endFlag },
} = this.parse(ChannelRollout);
const {
flags: { branch: branchName, percent },
} = this.parse(ChannelRollout);
flags: { json: jsonFlag, end: endFlag, branch: branchName, percent },
} = await this.parse(ChannelRollout);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import Table from 'cli-table3';
@@ -181,7 +181,7 @@ export function logChannelDetails(channel: {
: []),
formatUpdate(update),
update?.runtimeVersion ?? 'N/A',
update?.group ?? 'N/A,',
update?.group ?? 'N/A',
getPlatformsForGroup({
updates: branch.updates,
group: branch.updates[0]?.group,
@@ -192,7 +192,6 @@ export function logChannelDetails(channel: {
}
export default class ChannelView extends EasCommand {
static hidden = true;
static description = 'View a channel on the current project.';
static args = [
@@ -204,7 +203,7 @@ export default class ChannelView extends EasCommand {
];
static flags = {
json: flags.boolean({
json: Flags.boolean({
description: 'print output as a JSON object with the channel ID, name and branch mapping.',
default: false,
}),
@@ -214,7 +213,7 @@ export default class ChannelView extends EasCommand {
let {
args: { name: channelName },
flags: { json: jsonFlag },
} = this.parse(ChannelView);
} = await this.parse(ChannelView);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -239,7 +238,7 @@ export default class ChannelView extends EasCommand {
});
const channel = getUpdateChannelByNameForAppresult.app?.byId.updateChannelByName;
if (!channel) {
throw new Error(`Could not fine channel with name ${channelName}`);
throw new Error(`Could not find a channel with name: ${channelName}`);
}
if (jsonFlag) {
@@ -256,9 +255,7 @@ export default class ChannelView extends EasCommand {
])
);
Log.addNewLineIfNone();
Log.log(
chalk`{bold Branches, pointed at by this channel, and their most recent update group:}`
);
Log.log(chalk`{bold Branches pointed at this channel and their most recent update group:}`);
logChannelDetails(channel);
}
}
+5 -5
View File
@@ -1,7 +1,7 @@
import { getProjectConfigDescription } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import EasCommand from '../commandUtils/EasCommand';
import Log from '../log';
@@ -14,14 +14,14 @@ export default class Config extends EasCommand {
static description = 'show the eas.json config';
static flags = {
platform: flags.enum({ char: 'p', options: ['android', 'ios'] }),
profile: flags.string(),
platform: Flags.enum({ char: 'p', options: ['android', 'ios'] }),
profile: Flags.string(),
};
protected requiresAuthentication = false;
async runAsync(): Promise<void> {
const { flags } = this.parse(Config);
const { flags } = await this.parse(Config);
const { platform: maybePlatform, profile: maybeProfile } = flags as {
platform?: Platform;
profile?: string;
@@ -55,7 +55,7 @@ export default class Config extends EasCommand {
},
]));
const profile = await reader.readBuildProfileAsync(platform, profileName);
const profile = await reader.getBuildProfileAsync(platform, profileName);
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
Log.log(getProjectConfigDescription(projectDir));
+1 -7
View File
@@ -1,16 +1,10 @@
import EasCommand from '../commandUtils/EasCommand';
import { CredentialsContext } from '../credentials/context';
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
import { ensureLoggedInAsync } from '../user/actions';
export default class Credentials extends EasCommand {
static description = 'manage your credentials';
async runAsync(): Promise<void> {
const ctx = new CredentialsContext({
projectDir: process.cwd(),
user: await ensureLoggedInAsync(),
});
await new SelectPlatform().runAsync(ctx);
await new SelectPlatform().runAsync();
}
}
+3 -3
View File
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
@@ -16,11 +16,11 @@ export default class BuildList extends EasCommand {
static description = 'list all registered devices for your account';
static flags = {
'apple-team-id': flags.string(),
'apple-team-id': Flags.string(),
};
async runAsync(): Promise<void> {
let appleTeamIdentifier = this.parse(BuildList).flags['apple-team-id'];
let appleTeamIdentifier = (await this.parse(BuildList)).flags['apple-team-id'];
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
+1 -1
View File
@@ -13,7 +13,7 @@ export default class DeviceView extends EasCommand {
static args = [{ name: 'UDID' }];
async runAsync(): Promise<void> {
const { UDID } = this.parse(DeviceView).args;
const { UDID } = (await this.parse(DeviceView)).args;
if (!UDID) {
Log.log(
+22 -1
View File
@@ -1,7 +1,10 @@
import { Platform } from '@expo/eas-build-job';
import envinfo from 'envinfo';
import EasCommand from '../commandUtils/EasCommand';
import Log from '../log';
import { findProjectRootAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { easCliVersion } from '../utils/easCli';
export default class Diagnostics extends EasCommand {
@@ -25,6 +28,7 @@ export default class Diagnostics extends EasCommand {
'react-navigation',
'@expo/webpack-config',
'expo-dev-client',
'expo-updates',
],
npmGlobalPackages: ['eas-cli', 'expo-cli'],
},
@@ -32,6 +36,23 @@ export default class Diagnostics extends EasCommand {
title: `EAS CLI ${easCliVersion} environment info`,
}
);
Log.log(info);
Log.log(info.trimEnd());
await this.printWorkflowAsync();
Log.newLine();
}
private async printWorkflowAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const androidWorkflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const iosWorkflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (androidWorkflow === iosWorkflow) {
Log.log(` Project workflow: ${androidWorkflow}`);
} else {
Log.log(` Project Workflow:`);
Log.log(` Android: ${androidWorkflow}`);
Log.log(` iOS: ${iosWorkflow}`);
}
}
}
+6 -16
View File
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
@@ -9,10 +9,6 @@ import {
EnvironmentSecretsQuery,
} from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log';
import {
isEasEnabledForProjectAsync,
warnEasUnavailable,
} from '../../project/isEasEnabledForProject';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
@@ -27,18 +23,18 @@ export default class EnvironmentSecretCreate extends EasCommand {
static description = 'Create an environment secret on the current project or owner account.';
static flags = {
scope: flags.enum({
scope: Flags.enum({
description: 'Scope for the secret',
options: [EnvironmentSecretScope.ACCOUNT, EnvironmentSecretScope.PROJECT],
default: EnvironmentSecretScope.PROJECT,
}),
name: flags.string({
name: Flags.string({
description: 'Name of the secret',
}),
value: flags.string({
value: Flags.string({
description: 'Value of the secret',
}),
force: flags.boolean({
force: Flags.boolean({
description: 'Delete and recreate existing secrets',
default: false,
}),
@@ -48,7 +44,7 @@ export default class EnvironmentSecretCreate extends EasCommand {
const actor = await ensureLoggedInAsync();
let {
flags: { name, value: secretValue, scope, force },
} = this.parse(EnvironmentSecretCreate);
} = await this.parse(EnvironmentSecretCreate);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -57,12 +53,6 @@ export default class EnvironmentSecretCreate extends EasCommand {
const { slug } = exp;
const projectId = await getProjectIdAsync(exp);
if (!(await isEasEnabledForProjectAsync(projectId))) {
warnEasUnavailable();
process.exitCode = 1;
return;
}
if (!scope) {
const validationMessage = 'Secret scope may not be empty.';
+3 -13
View File
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
@@ -10,10 +10,6 @@ import {
EnvironmentSecretsQuery,
} from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log';
import {
isEasEnabledForProjectAsync,
warnEasUnavailable,
} from '../../project/isEasEnabledForProject';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
@@ -26,7 +22,7 @@ export default class EnvironmentSecretDelete extends EasCommand {
Unsure where to find the secret's ID? Run ${chalk.bold('eas secret:list')}`;
static flags = {
id: flags.string({
id: Flags.string({
description: 'ID of the secret to delete',
}),
};
@@ -37,15 +33,9 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secret:list')}`;
const projectId = await getProjectIdAsync(exp);
const projectAccountName = await getProjectAccountNameAsync(exp);
if (!(await isEasEnabledForProjectAsync(projectId))) {
warnEasUnavailable();
process.exitCode = 1;
return;
}
let {
flags: { id },
} = this.parse(EnvironmentSecretDelete);
} = await this.parse(EnvironmentSecretDelete);
let secret: EnvironmentSecretWithScope | undefined;
if (!id) {
@@ -6,10 +6,6 @@ import dateFormat from 'dateformat';
import EasCommand from '../../commandUtils/EasCommand';
import { EnvironmentSecretsQuery } from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log';
import {
isEasEnabledForProjectAsync,
warnEasUnavailable,
} from '../../project/isEasEnabledForProject';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
@@ -25,12 +21,6 @@ export default class EnvironmentSecretList extends EasCommand {
const projectId = await getProjectIdAsync(exp);
const projectAccountName = await getProjectAccountNameAsync(exp);
if (!(await isEasEnabledForProjectAsync(projectId))) {
warnEasUnavailable();
process.exitCode = 1;
return;
}
if (!projectDir) {
throw new Error("Please run this command inside your project's directory");
}
+19 -30
View File
@@ -1,7 +1,5 @@
import { getConfig } from '@expo/config';
import { EasJsonReader } from '@expo/eas-json';
import { flags } from '@oclif/command';
import { error } from '@oclif/errors';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../commandUtils/EasCommand';
@@ -15,14 +13,13 @@ import {
selectRequestedPlatformAsync,
toPlatforms,
} from '../platform';
import { isEasEnabledForProjectAsync, warnEasUnavailable } from '../project/isEasEnabledForProject';
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
import { SubmitArchiveFlags, createSubmissionContextAsync } from '../submit/context';
import { submitAsync, waitToCompleteAsync } from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { getProfilesAsync } from '../utils/profiles';
interface RawFlags {
interface RawCommandFlags {
platform?: string;
profile?: string;
latest?: boolean;
@@ -34,7 +31,7 @@ interface RawFlags {
'non-interactive': boolean;
}
interface Flags {
interface CommandFlags {
requestedPlatform: RequestedPlatform;
profile?: string;
archiveFlags: SubmitArchiveFlags;
@@ -44,72 +41,64 @@ interface Flags {
}
export default class Submit extends EasCommand {
static description = `submit build archive to app store
static description = `Submit build archive to App Store Connect
See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit/eas-json/')}`;
static aliases = ['build:submit'];
static flags = {
platform: flags.enum({
platform: Flags.enum({
char: 'p',
options: ['android', 'ios', 'all'],
}),
profile: flags.string({
profile: Flags.string({
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
latest: flags.boolean({
latest: Flags.boolean({
description: 'Submit the latest build for specified platform',
exclusive: ['id', 'path', 'url'],
}),
id: flags.string({
id: Flags.string({
description: 'ID of the build to submit',
exclusive: ['latest, path, url'],
}),
path: flags.string({
path: Flags.string({
description: 'Path to the .apk/.aab/.ipa file',
exclusive: ['latest', 'id', 'url'],
}),
url: flags.string({
url: Flags.string({
description: 'App archive url',
exclusive: ['latest', 'id', 'path'],
}),
verbose: flags.boolean({
verbose: Flags.boolean({
description: 'Always print logs from Submission Service',
default: false,
}),
wait: flags.boolean({
wait: Flags.boolean({
description: 'Wait for submission to complete',
default: true,
allowNo: true,
}),
'non-interactive': flags.boolean({
'non-interactive': Flags.boolean({
default: false,
description: 'Run command in non-interactive mode',
}),
};
async runAsync(): Promise<void> {
const { flags: rawFlags } = this.parse(Submit);
const { flags: rawFlags } = await this.parse(Submit);
const flags = await this.sanitizeFlagsAsync(rawFlags);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
if (!(await isEasEnabledForProjectAsync(projectId))) {
warnEasUnavailable();
process.exitCode = 1;
return;
}
const platforms = toPlatforms(flags.requestedPlatform);
const easJsonReader = new EasJsonReader(projectDir);
const submissionProfiles = await getProfilesAsync({
type: 'submit',
projectDir,
platforms,
profileName: flags.profile,
async readProfileAsync(platform, profileName) {
return await easJsonReader.readSubmitProfileAsync(platform, profileName);
},
});
const submissions: SubmissionFragment[] = [];
@@ -145,7 +134,7 @@ See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit
}
}
private async sanitizeFlagsAsync(flags: RawFlags): Promise<Flags> {
private async sanitizeFlagsAsync(flags: RawCommandFlags): Promise<CommandFlags> {
const {
platform,
verbose,
@@ -156,13 +145,13 @@ See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit
} = flags;
if (!flags.platform && nonInteractive) {
error('--platform is required when building in non-interactive mode', { exit: 1 });
Errors.error('--platform is required when building in non-interactive mode', { exit: 1 });
}
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
if (requestedPlatform === RequestedPlatform.All) {
if (archiveFlags.id || archiveFlags.path || archiveFlags.url) {
error(
Errors.error(
'--id, --path, and --url params are only supported when performing a single-platform submit',
{ exit: 1 }
);
@@ -0,0 +1,109 @@
import { ExpoConfig, getConfig, modifyConfigAsync } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import chalk from 'chalk';
import { getEASUpdateURL } from '../../api';
import EasCommand from '../../commandUtils/EasCommand';
import Log, { learnMore } from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
const DEFAULT_MANAGED_RUNTIME_VERSION = { policy: 'sdkVersion' } as const;
const DEFAULT_BARE_RUNTIME_VERSION = '1.0.0';
async function configureProjectForEASUpdateAsync(
projectDir: string,
exp: ExpoConfig,
isBare: boolean
): Promise<void> {
const projectId = await getProjectIdAsync(exp);
const easUpdateURL = getEASUpdateURL(projectId);
const preexistingRuntimeVersion = exp.runtimeVersion;
const defaultRuntimeVersion = isBare
? DEFAULT_BARE_RUNTIME_VERSION
: DEFAULT_MANAGED_RUNTIME_VERSION;
const result = await modifyConfigAsync(projectDir, {
runtimeVersion: preexistingRuntimeVersion ?? defaultRuntimeVersion,
updates: { ...exp.updates, url: easUpdateURL },
});
switch (result.type) {
case 'success':
if (exp.updates?.url) {
if (exp.updates.url !== easUpdateURL) {
Log.withTick(
`Overwrote "${exp.updates?.url}" with "${easUpdateURL}" for the updates.url value in app.json`
);
}
} else {
Log.withTick(`Set updates.url value, to "${easUpdateURL}" in app.json`);
}
if (!preexistingRuntimeVersion) {
Log.withTick(
`Set runtimeVersion to "${JSON.stringify(defaultRuntimeVersion)}" in app.json`
);
}
break;
case 'warn': {
Log.addNewLineIfNone();
Log.warn(
`It looks like you are using a dynamic configuration! ${learnMore(
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs)'
)}`
);
Log.warn(
`In order to finish configuring your project for EAS Update, you are going to need manually add the following to your app.config.js:\n${learnMore(
'https://expo.fyi/eas-update-config.md'
)}\n`
);
Log.log(
chalk.bold(
`{\n updates": {\n "url": "${easUpdateURL}"\n },\n "runtimeVersion": {\n "policy": "sdkVersion"\n }\n}`
)
);
Log.addNewLineIfNone();
throw new Error(result.message);
}
case 'fail':
throw new Error(result.message);
default:
throw new Error('Unexpected result type from modifyConfigAsync');
}
}
export default class UpdateConfigure extends EasCommand {
static description = 'Configure the project to support EAS Update.';
async runAsync(): Promise<void> {
Log.log(
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
});
const hasAndroidNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
const hasIosNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.IOS)) === Workflow.GENERIC;
const isBare = hasAndroidNativeProject || hasIosNativeProject;
await configureProjectForEASUpdateAsync(projectDir, exp, isBare);
Log.addNewLineIfNone();
if (isBare) {
Log.log(
`🧐 It seems you are on the bare workflow! Please also update your native files. You can do this by either running ${chalk.bold(
'eas build:configure'
)} or manually editing Expo.plist/AndroidManifest.xml. ${learnMore(
'https://expo.fyi/eas-update-config.md#native-configuration'
)}`
);
} else {
Log.log(`🎉 Your app is configured to run EAS Update!`);
}
}
}
@@ -1,4 +1,4 @@
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
@@ -35,7 +35,6 @@ async function deleteUpdateGroupAsync({
}
export default class UpdateDelete extends EasCommand {
static hidden = true;
static description = 'Delete all the updates in an update Group.';
static args = [
@@ -47,7 +46,7 @@ export default class UpdateDelete extends EasCommand {
];
static flags = {
json: flags.boolean({
json: Flags.boolean({
description: `Return a json with the group ID of the deleted updates.`,
default: false,
}),
@@ -57,7 +56,7 @@ export default class UpdateDelete extends EasCommand {
const {
args: { groupId: group },
flags: { json: jsonFlag },
} = this.parse(UpdateDelete);
} = await this.parse(UpdateDelete);
if (!jsonFlag) {
const shouldAbort = await confirmAsync({
@@ -0,0 +1,515 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import dateFormat from 'dateformat';
import gql from 'graphql-tag';
import { getEASUpdateURL } from '../../api';
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 '../branch/create';
import { listBranchesAsync } from '../branch/list';
import { viewUpdateBranchAsync } from '../branch/view';
import { createUpdateChannelOnAppAsync } from '../channel/create';
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 ensureChannelExistsAsync({
appId,
branchId,
channelName,
}: {
appId: string;
branchId: string;
channelName: string;
}): Promise<void> {
try {
await createUpdateChannelOnAppAsync({
appId,
channelName,
branchId,
});
Log.withTick(
`Created a channel: ${chalk.bold(channelName)} pointed at branch: ${chalk.bold(channelName)}.`
);
} catch (e: any) {
const isIgnorableError =
e.graphQLErrors?.length === 1 &&
e.graphQLErrors[0].extensions.errorCode === 'CHANNEL_ALREADY_EXISTS';
if (!isIgnorableError) {
throw e;
}
}
}
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;
await ensureChannelExistsAsync({ appId, branchId: id, channelName: branchName });
return { id, updates };
}
const newUpdateBranch = await createUpdateBranchOnAppAsync({ appId, name: branchName });
Log.withTick(`Created branch: ${chalk.bold(branchName)}`);
await ensureChannelExistsAsync({ appId, branchId: newUpdateBranch.id, channelName: branchName });
return { id: newUpdateBranch.id, updates: [] };
}
export default class UpdatePublish extends EasCommand {
static description = 'Publish an update group.';
static flags = {
branch: Flags.string({
description: 'Branch to publish the update group on',
required: false,
}),
message: Flags.string({
description: 'A short message describing the update',
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',
options: [...defaultPublishPlatforms, 'all'],
default: 'all',
required: false,
}),
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
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 {
flags: {
branch: branchName,
json: jsonFlag,
auto: autoFlag,
message,
republish,
group,
'input-dir': inputDir,
'skip-bundler': skipBundler,
platform,
},
} = await this.parse(UpdatePublish);
const platformFlag = 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);
await checkEASUpdateURLIsSetAsync(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 });
if (branches.length === 0) {
({ name: branchName } = await promptAsync({
type: 'text',
name: 'name',
message: 'No branches found. Creating a new one. Please name the new branch:',
initial:
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substr(2, 4)}`,
validate: value => (value ? true : validationMessage),
}));
} else {
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;
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 an update message.`,
initial: `Republish "${oldMessage!}" - group: ${group}`,
validate: (value: any) => (value ? true : validationMessage),
}));
}
} else {
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 an update message.`,
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
validate: (value: any) => (value ? true : validationMessage),
}));
}
// build bundle and upload assets for a new publish
if (!skipBundler) {
const bundleSpinner = ora().start('Building bundle...');
try {
await buildBundlesAsync({ projectDir, inputDir });
bundleSpinner.succeed('Built bundle!');
} catch (e) {
bundleSpinner.fail('Failed to build bundle!');
throw e;
}
}
const assetSpinner = ora().start('Uploading assets...');
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;
}
}
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}`;
}
async function checkEASUpdateURLIsSetAsync(exp: ExpoConfig): Promise<void> {
const configuredURL = exp.updates?.url;
const projectId = await getProjectIdAsync(exp);
const expectedURL = getEASUpdateURL(projectId);
if (configuredURL !== expectedURL) {
throw new Error(
`The update URL is incorrectly configured for EAS Update. Please set updates.url to ${expectedURL} in your app.json.`
);
}
}
+3 -4
View File
@@ -1,4 +1,4 @@
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import Table from 'cli-table3';
import gql from 'graphql-tag';
@@ -50,7 +50,6 @@ export async function viewUpdateAsync({
return data;
}
export default class UpdateView extends EasCommand {
static hidden = true;
static description = 'Update group details.';
static args = [
@@ -62,7 +61,7 @@ export default class UpdateView extends EasCommand {
];
static flags = {
json: flags.boolean({
json: Flags.boolean({
description: `Return a json with the updates belonging to the group.`,
default: false,
}),
@@ -72,7 +71,7 @@ export default class UpdateView extends EasCommand {
const {
args: { groupId },
flags: { json: jsonFlag },
} = this.parse(UpdateView);
} = await this.parse(UpdateView);
const { updatesByGroup } = await viewUpdateAsync({ groupId });
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import { WebhookType } from '../../graphql/generated';
@@ -12,22 +12,21 @@ export default class WebhookCreate extends EasCommand {
static description = 'Create a webhook on the current project.';
static flags = {
event: flags.enum({
event: Flags.enum({
description: 'Event type that triggers the webhook',
options: [WebhookType.Build],
default: WebhookType.Build,
options: [WebhookType.Build, WebhookType.Submit],
}),
url: flags.string({
url: Flags.string({
description: 'Webhook URL',
}),
secret: flags.string({
secret: Flags.string({
description:
"Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature' header.",
}),
};
async runAsync(): Promise<void> {
const { flags } = this.parse(WebhookCreate);
const { flags } = await this.parse(WebhookCreate);
const webhookInputParams = await prepareInputParamsAsync(flags);
const projectDir = await findProjectRootAsync();
@@ -27,7 +27,7 @@ export default class WebhookDelete extends EasCommand {
async runAsync(): Promise<void> {
let {
args: { ID: webhookId },
} = this.parse(WebhookDelete);
} = await this.parse(WebhookDelete);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
@@ -18,16 +18,16 @@ export default class WebhookList extends EasCommand {
static description = 'List webhooks on the current project.';
static flags = {
event: flags.enum({
event: Flags.enum({
description: 'Event type that triggers the webhook',
options: [WebhookType.Build],
options: [WebhookType.Build, WebhookType.Submit],
}),
};
async runAsync(): Promise<void> {
const {
flags: { event },
} = this.parse(WebhookList);
} = await this.parse(WebhookList);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -1,4 +1,4 @@
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import { WebhookType } from '../../graphql/generated';
@@ -12,26 +12,25 @@ export default class WebhookUpdate extends EasCommand {
static description = 'Create a webhook on the current project.';
static flags = {
id: flags.string({
id: Flags.string({
description: 'Webhook ID',
required: true,
}),
event: flags.enum({
event: Flags.enum({
description: 'Event type that triggers the webhook',
options: [WebhookType.Build],
default: WebhookType.Build,
options: [WebhookType.Build, WebhookType.Submit],
}),
url: flags.string({
url: Flags.string({
description: 'Webhook URL',
}),
secret: flags.string({
secret: Flags.string({
description:
"Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature' header.",
}),
};
async runAsync(): Promise<void> {
const { flags } = this.parse(WebhookUpdate);
const { flags } = await this.parse(WebhookUpdate);
const webhookId = flags.id;
@@ -18,7 +18,7 @@ export default class WebhookView extends EasCommand {
async runAsync(): Promise<void> {
const {
args: { ID: webhookId },
} = this.parse(WebhookView);
} = await this.parse(WebhookView);
const spinner = ora(`Fetching the webhook details for ID ${webhookId}`).start();
try {
@@ -5,7 +5,7 @@ import nullthrows from 'nullthrows';
import { AndroidAppBuildCredentialsFragment } from '../../graphql/generated';
import { CredentialsContext } from '../context';
import * as credentialsJsonReader from '../credentialsJson/read';
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
import { SetUpBuildCredentials } from './actions/SetUpBuildCredentials';
import { AppLookupParams } from './api/GraphqlClient';
import { Keystore } from './credentials';
@@ -34,7 +34,7 @@ export default class AndroidCredentialsProvider {
}
private async getRemoteAsync(): Promise<AndroidCredentials> {
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: this.options.app });
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: this.options.app });
const buildCredentials = await setupBuildCredentialsAction.runAsync(this.ctx);
return this.toAndroidCredentials(buildCredentials);
}
@@ -9,7 +9,7 @@ export class RemoveFcm {
async runAsync(ctx: CredentialsContext): Promise<void> {
if (ctx.nonInteractive) {
throw new Error(
"Deleting an FCM Api Key is a destructive operation. Start the CLI without the '--non-interactive' flag to delete the credentials."
"Deleting an FCM API Key is a destructive operation. Start the CLI without the '--non-interactive' flag to delete the credentials."
);
}
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(
@@ -18,7 +18,7 @@ export class RemoveFcm {
const fcm = appCredentials?.androidFcm;
if (!fcm) {
Log.warn(
`There is no valid FCM Api Key defined for ${formatProjectFullName(this.app)}, ${
`There is no valid FCM API Key defined for ${formatProjectFullName(this.app)}, ${
this.app.androidApplicationIdentifier
}`
);
@@ -26,7 +26,7 @@ export class RemoveFcm {
}
const confirm = await confirmAsync({
message: 'Permanently delete the FCM Api Key from Expo servers?',
message: 'Permanently delete the FCM API Key from Expo servers?',
initial: false,
});
if (!confirm) {
@@ -34,6 +34,6 @@ export class RemoveFcm {
}
await ctx.android.deleteFcmAsync(fcm);
Log.succeed('FCM Api Key removed');
Log.succeed('FCM API Key removed');
}
}
@@ -25,7 +25,7 @@ interface Options {
* Sets up Build Credentials for Android
* @name: sets up build credentials for the specified configuration. If no name is specified, the default configuration is setup
*/
export class SetupBuildCredentials {
export class SetUpBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: CredentialsContext): Promise<AndroidAppBuildCredentialsFragment> {
@@ -14,7 +14,7 @@ import { AppLookupParams } from '../api/GraphqlClient';
import { getKeystoreWithType } from '../utils/keystoreNew';
import { BackupKeystore } from './DownloadKeystore';
export class SetupBuildCredentialsFromCredentialsJson {
export class SetUpBuildCredentialsFromCredentialsJson {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: CredentialsContext): Promise<AndroidAppBuildCredentialsFragment | null> {
@@ -13,7 +13,7 @@ import { AssignGoogleServiceAccountKey } from './AssignGoogleServiceAccountKey';
import { CreateGoogleServiceAccountKey } from './CreateGoogleServiceAccountKey';
import { UseExistingGoogleServiceAccountKey } from './UseExistingGoogleServiceAccountKey';
export class SetupGoogleServiceAccountKey {
export class SetUpGoogleServiceAccountKey {
constructor(private app: AppLookupParams) {}
public async runAsync(ctx: CredentialsContext): Promise<CommonAndroidAppCredentialsFragment> {
@@ -12,7 +12,7 @@ jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(RemoveFcm, () => {
it('removes an FCM Api Key', async () => {
it('removes an FCM API Key', async () => {
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -9,7 +9,7 @@ import { createCtxMock } from '../../../__tests__/fixtures-context';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { generateRandomKeystoreAsync } from '../../utils/keystore';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetupBuildCredentials } from '../SetupBuildCredentials';
import { SetUpBuildCredentials } from '../SetUpBuildCredentials';
jest.mock('../../../../ora');
jest.mock('../../../../project/ensureProjectExists');
@@ -17,7 +17,7 @@ jest.mock('../../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() =
jest.mock('../../../../prompts', () => ({ confirmAsync: jest.fn(() => true) }));
jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn() }));
describe('SetupBuildCredentials', () => {
describe('SetUpBuildCredentials', () => {
beforeEach(() => {
asMock(generateRandomKeystoreAsync).mockReset();
});
@@ -33,7 +33,7 @@ describe('SetupBuildCredentials', () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
await setupBuildCredentialsAction.runAsync(ctx);
// expect keystore not to be created
@@ -48,7 +48,7 @@ describe('SetupBuildCredentials', () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
await setupBuildCredentialsAction.runAsync(ctx);
// expect keystore to be created
@@ -63,7 +63,7 @@ describe('SetupBuildCredentials', () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
);
@@ -14,7 +14,7 @@ import {
SelectAndroidBuildCredentialsResultType,
} from '../../../manager/SelectAndroidBuildCredentials';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetupBuildCredentialsFromCredentialsJson } from '../SetupBuildCredentialsFromCredentialsJson';
import { SetUpBuildCredentialsFromCredentialsJson } from '../SetUpBuildCredentialsFromCredentialsJson';
jest.mock('fs');
@@ -26,7 +26,7 @@ beforeEach(() => {
vol.reset();
});
describe(SetupBuildCredentialsFromCredentialsJson, () => {
describe(SetUpBuildCredentialsFromCredentialsJson, () => {
it('sets up a new build configuration from credentials.json upon user request', async () => {
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
return {
@@ -59,7 +59,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
'keystore.jks': 'some-binary-content',
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
appLookupParams
);
await setupBuildCredentialsAction.runAsync(ctx);
@@ -109,7 +109,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
'keystore.jks': 'some-binary-content',
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
appLookupParams
);
await setupBuildCredentialsAction.runAsync(ctx);
@@ -132,7 +132,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
nonInteractive: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
appLookupParams
);
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError();
@@ -10,7 +10,7 @@ import {
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetupGoogleServiceAccountKey } from '../SetupGoogleServiceAccountKey';
import { SetUpGoogleServiceAccountKey } from '../SetUpGoogleServiceAccountKey';
jest.mock('../../../../prompts');
jest.mock('fs');
@@ -22,7 +22,7 @@ beforeEach(() => {
vol.reset();
});
describe(SetupGoogleServiceAccountKey, () => {
describe(SetUpGoogleServiceAccountKey, () => {
it('skips setup when there is a Google Service Account Key already assigned to the project', async () => {
const ctx = createCtxMock({
nonInteractive: false,
@@ -34,7 +34,7 @@ describe(SetupGoogleServiceAccountKey, () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
await setupGoogleServiceAccountKeyAction.runAsync(ctx);
expect(ctx.android.createGoogleServiceAccountKeyAsync).not.toHaveBeenCalled();
@@ -57,7 +57,7 @@ describe(SetupGoogleServiceAccountKey, () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
await setupGoogleServiceAccountKeyAction.runAsync(ctx);
expect(ctx.android.createGoogleServiceAccountKeyAsync).toHaveBeenCalledTimes(1);
@@ -68,7 +68,7 @@ describe(SetupGoogleServiceAccountKey, () => {
nonInteractive: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
await expect(setupGoogleServiceAccountKeyAction.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
);
@@ -1,31 +1,35 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import crypto from 'crypto';
import fs from 'fs-extra';
// (dsokal) We actually want to use node-fetch but the change is in progress.
// See https://github.com/expo/eas-cli/issues/32 for context.
// eslint-disable-next-line no-restricted-imports
import fetch from 'node-fetch';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import Analytics, { Event } from '../../../build/utils/analytics';
import { Analytics, BuildEvent } from '../../../analytics/events';
import { AndroidKeystoreType } from '../../../graphql/generated';
import { KeystoreGenerationUrlMutation } from '../../../graphql/mutations/KeystoreGenerationUrlMutation';
import Log from '../../../log';
import { ora } from '../../../ora';
import { getTmpDirectory } from '../../../utils/paths';
import { KeystoreWithType } from '../credentials';
interface KeystoreParams {
keystorePassword: string;
keyAlias: string;
keyPassword: string;
}
export async function keytoolCommandExistsAsync(): Promise<boolean> {
try {
await spawnAsync('keytool');
return true;
} catch (error) {
return false;
}
return true;
}
async function ensureKeytoolCommandExistsAsync(): Promise<void> {
if (!(await keytoolCommandExistsAsync())) {
Log.error('keytool is required to run this command, make sure you have it installed?');
Log.warn('keytool is a part of OpenJDK: https://openjdk.java.net/');
Log.warn('Also make sure that keytool is in your PATH after installation.');
throw new Error('keytool not found');
}
}
enum KeystoreCreateStep {
@@ -35,7 +39,7 @@ enum KeystoreCreateStep {
}
export async function generateRandomKeystoreAsync(projectId: string): Promise<KeystoreWithType> {
const keystoreData = {
const keystoreData: KeystoreParams = {
keystorePassword: crypto.randomBytes(16).toString('hex'),
keyPassword: crypto.randomBytes(16).toString('hex'),
keyAlias: crypto.randomBytes(16).toString('hex'),
@@ -44,23 +48,38 @@ export async function generateRandomKeystoreAsync(projectId: string): Promise<Ke
}
async function createKeystoreAsync(
credentials: {
keystorePassword: string;
keyAlias: string;
keyPassword: string;
},
keystoreParams: KeystoreParams,
projectId: string
): Promise<KeystoreWithType> {
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Attempt,
type: AndroidKeystoreType.Jks,
});
try {
await ensureKeytoolCommandExistsAsync();
let keystore: KeystoreWithType | undefined;
let localAttempt = false;
if (await keytoolCommandExistsAsync()) {
try {
localAttempt = true;
keystore = await createKeystoreLocallyAsync(keystoreParams);
} catch {
Log.error('Failed to generate keystore locally. Falling back to cloud generation.');
}
}
if (!keystore) {
keystore = await createKeystoreInCloudAsync(keystoreParams, {
showKeytoolDetectionMsg: !localAttempt,
});
}
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Success,
type: AndroidKeystoreType.Jks,
});
return keystore;
} catch (error: any) {
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Fail,
reason: error.message,
@@ -68,7 +87,11 @@ async function createKeystoreAsync(
});
throw error;
}
}
async function createKeystoreLocallyAsync(
keystoreParams: KeystoreParams
): Promise<KeystoreWithType> {
await fs.mkdirp(getTmpDirectory());
const keystorePath = path.join(getTmpDirectory(), `${uuidv4()}-keystore.jks`);
try {
@@ -78,13 +101,13 @@ async function createKeystoreAsync(
'-storetype',
'JKS',
'-storepass',
credentials.keystorePassword,
keystoreParams.keystorePassword,
'-keypass',
credentials.keyPassword,
keystoreParams.keyPassword,
'-keystore',
keystorePath,
'-alias',
credentials.keyAlias,
keystoreParams.keyAlias,
'-keyalg',
'RSA',
'-keysize',
@@ -94,27 +117,49 @@ async function createKeystoreAsync(
'-dname',
`CN=,OU=,O=,L=,S=,C=US`,
]);
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Success,
type: AndroidKeystoreType.Jks,
});
return {
...credentials,
...keystoreParams,
keystore: await fs.readFile(keystorePath, 'base64'),
type: AndroidKeystoreType.Jks,
};
} catch (error: any) {
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Fail,
reason: error.message,
type: AndroidKeystoreType.Jks,
});
throw error;
} finally {
await fs.remove(keystorePath);
}
}
interface KeystoreServiceResult {
keystoreBase64: string;
keystorePassword: string;
keyPassword: string;
keyAlias: string;
}
async function createKeystoreInCloudAsync(
keystoreParams: KeystoreParams,
{ showKeytoolDetectionMsg = true } = {}
): Promise<KeystoreWithType> {
if (showKeytoolDetectionMsg) {
Log.log(`Detected that you do not have ${chalk.bold('keytool')} installed locally.`);
}
const spinner = ora('Generating keystore in the cloud...').start();
try {
const url = await KeystoreGenerationUrlMutation.createKeystoreGenerationUrlAsync();
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(keystoreParams),
headers: { 'Content-Type': 'application/json' },
});
const result: KeystoreServiceResult = await response.json();
spinner.succeed();
return {
type: AndroidKeystoreType.Jks,
keystore: result.keystoreBase64,
keystorePassword: result.keystorePassword,
keyAlias: result.keyAlias,
keyPassword: result.keyPassword,
};
} catch (err: any) {
spinner.fail();
throw err;
}
}
Loaded 100 of 219 files, more files were not shown because too many files have changed in this diff. Show more