Compare commits

...
120 Commits
Author SHA1 Message Date
Dominik Sokal 5ed4b65133 v0.46.0 2022-01-26 10:33:41 +01:00
Dominik Sokal 10aca5208f fix url placeholder for iOS submits (#941)
* fix url placeholder for iOS submits

* update CHANGELOG.md
2022-01-25 15:26:20 +01:00
dependabot[bot] b2da80d5ea Bump node-fetch from 2.6.1 to 2.6.7 (#940)
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.1 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.1...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: direct:production
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-25 15:21:45 +01:00
Dominik Sokal b9457d71f9 upgrade deps (#939)
* upgrade deps

* update CHANGELOG.md
2022-01-25 14:48:02 +01:00
Dominik Sokalandjkh 5fd3149a95 unify command descriptions (#925)
* unify command descriptions

* fix lint warnings

* Apply suggestions from code review

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

* address PR feedback

* update CHANGELOG.md

Co-authored-by: jkh <[email protected]>
2022-01-21 12:13:40 +01:00
dependabot[bot] 5f83724833 Bump trim-off-newlines from 1.0.1 to 1.0.3 (#923)
Bumps [trim-off-newlines](https://github.com/stevemao/trim-off-newlines) from 1.0.1 to 1.0.3.
- [Release notes](https://github.com/stevemao/trim-off-newlines/releases)
- [Commits](https://github.com/stevemao/trim-off-newlines/compare/v1.0.1...v1.0.3)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-20 09:56:09 +01:00
Expo CI 7df558d713 update CHANGELOG.md 2022-01-19 15:50:30 +00:00
Wojciech Kozyra 543e8ce26d v0.45.1 2022-01-19 16:31:12 +01:00
Kim Brandwijk 355fada383 add json output for started builds (#921) 2022-01-19 13:48:37 +01:00
Kim Brandwijk b6625a4cba add support for prebuildCommand in eas.json (#919) 2022-01-19 13:27:31 +01:00
Wojciech Kozyra c1ef20afab [eas-cli] add Monterey image (#922)
* [eas-cli] add Monterey image

* update CHANGELOG.md
2022-01-19 12:45:19 +01:00
Expo CI d8321e1f06 update CHANGELOG.md 2022-01-18 12:28:19 +00:00
kbrandwijk d8906e085e v0.45.0 2022-01-18 13:21:25 +01:00
Dominik Sokal 3b823865d3 upgrade dependencies (#872)
* upgrade dependencies

* fix TS error

* regenerate graphql code

* update CHANGELOG.md
2022-01-18 10:33:50 +01:00
dependabot[bot] a29ed19653 Bump follow-redirects from 1.14.3 to 1.14.7 (#912)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.3 to 1.14.7.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.3...v1.14.7)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-17 10:15:58 +01:00
Dominik Sokal e70ecaa9b5 upgrade axios to 0.24.0 2022-01-17 10:14:13 +01:00
dependabot[bot] 9886fcb3ef Bump shelljs from 0.8.4 to 0.8.5 (#911)
Bumps [shelljs](https://github.com/shelljs/shelljs) from 0.8.4 to 0.8.5.
- [Release notes](https://github.com/shelljs/shelljs/releases)
- [Changelog](https://github.com/shelljs/shelljs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/shelljs/shelljs/compare/v0.8.4...v0.8.5)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-17 10:09:38 +01:00
kbrandwijk 4f5739b2dc docs: update readme 2022-01-13 14:59:48 +01:00
Kim Brandwijk c7a40b06a5 delete device functionality (#890)
* feat: device delete command
2022-01-12 12:31:10 +01:00
Dominik Sokal 3f88dc8690 fix webhook:update description 2022-01-12 11:43:19 +01:00
Expo CI 874db1c125 update CHANGELOG.md 2022-01-11 20:34:32 +00:00
Brent Vatne 91a147b40b v0.44.1 2022-01-11 12:28:58 -08:00
Brent Vatne cc4553b318 [eas-cli] Only configure updates in generic projects (#904) 2022-01-11 11:52:20 -08:00
Dominik Sokal dd2303dbb2 don't use forked @oclif/plugin-autocomplete (#903)
* upgrade oclif deps

* update CHANGELOG.md
2022-01-11 14:28:49 +01:00
dependabot[bot]andDominik Sokal 967b5d9985 Bump node-forge from 0.10.0 to 1.0.0 (#902)
* Bump node-forge from 0.10.0 to 1.0.0

Bumps [node-forge](https://github.com/digitalbazaar/forge) from 0.10.0 to 1.0.0.
- [Release notes](https://github.com/digitalbazaar/forge/releases)
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/0.10.0...v1.0.0)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-type: direct:production
...

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

* upgrade @types/node-forge

* update CHANGELOG.md

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dominik Sokal <[email protected]>
2022-01-11 14:27:00 +01:00
Expo CI 8439cd2141 update CHANGELOG.md 2022-01-11 11:45:09 +00:00
Dominik Sokal f46063cca5 v0.44.0 2022-01-11 12:39:39 +01:00
Dominik Sokal c0cfa93b69 upgrade eas-cli-local-build-plugin to 0.0.58 2022-01-11 12:39:30 +01:00
Dominik Sokal 7d6f620f91 Update CHANGELOG.md 2022-01-11 12:38:53 +01:00
Dominik Sokal c973a1f603 refactor build configuration + don't configure Android projects locally (#888)
* remove 'eas build:configure' and refactor project config synchronization

* simplify updates configuration

* remove --skip-project-configuration flag

* add build:configure back

* better ux

* run Android cleanup in build:configure

* address PR feedback

* update CHANGELOG.md
2022-01-11 12:30:39 +01:00
Evan Bacon 1b8f60fad4 Remove APNS capability by default (#797)
* Remove APNS capability by default

* Update bundleIdCapabilities-test.ts

* Update bundleIdCapabilities-test.ts

* Update CHANGELOG.md
2022-01-06 12:45:15 -07:00
Wojciech Kozyra 7a30de9ad7 [eas-cli] do not follow symlinks with fast-glob (#891)
* [eas-cli] do not follow symlinks with fast-glob

* update CHANGELOG.md
2022-01-05 17:50:36 +01:00
jkh 4247b65e09 [eas-cli] add update:list alias (#884) 2022-01-05 10:05:55 -05:00
Dominik Sokal 8c388d5173 Update CHANGELOG.md 2022-01-05 11:12:34 +01:00
Evan Bacon fce945679f Bump @expo/[email protected] (#889)
* Bump @expo/[email protected]

* Update CHANGELOG.md
2022-01-04 15:07:40 -07:00
Wojciech Kozyra 7772e199b4 [eas-cli] Add --output flag to build command (#885)
* [eas-cli] Add --output flag to build command

* update CHANGELOG.md
2022-01-04 10:56:25 +01:00
Dominik Sokal 755e105252 fix README.md 2022-01-03 11:47:59 +01:00
Expo CI 611d0c98ee update CHANGELOG.md 2022-01-03 10:42:36 +00:00
Dominik Sokal 949a90187b v0.43.0 2022-01-03 11:36:57 +01:00
Giau. Tran Minh bb14d9255a fix: fixed wrong JSON output (#878)
The current output is:

It looks like you are using a dynamic configuration! Learn more: https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs)
In order to finish configuring your project for EAS Update, you are going to need manually add the following to your app.config.js:
Learn more: https://expo.fyi/eas-update-config.md

```json
{
  updates": {
    "url": "https://u.expo.dev/1bd4bcb0-f689-11e9-abc4-2daaea3f68f6"
  },
  "runtimeVersion": {
    "policy": "sdkVersion"
  }
}
```
2022-01-03 11:35:14 +01:00
Dominik Sokal 98e35926ff Update bug_report.yml 2021-12-22 13:22:32 +01:00
Dominik Sokal cd433405cb update issue template (#871) 2021-12-22 13:21:00 +01:00
Dominik Sokal 3ad54b0fc9 add autocompletion plugin (#870)
* add autocompletion plugin

* update CHANGELOG.md
2021-12-22 11:19:07 +01:00
Wojciech Kozyra 5184117ed6 [eas-cli] improve build:inspect (#863)
* [eas-cli] improve build:inspect

* update CHANGELOG.md
2021-12-22 10:33:00 +01:00
Dominik Sokal 868df87146 Update CHANGELOG.md 2021-12-22 10:10:21 +01:00
jkh 2f0cc2361b [eas-cli] fix json output in EAS Update commands (#848) 2021-12-21 16:17:07 -05:00
jkh 4be6c51418 [eas-cli] fix merge conflict with #846 (#869) 2021-12-21 16:11:34 -05:00
Test User 99c4c618c9 Revert "[eas-cli] add channel:delete (#846)"
This reverts commit 742a1a05e5.
2021-12-21 15:57:07 -05:00
jkh 742a1a05e5 [eas-cli] add channel:delete (#846) 2021-12-21 15:16:25 -05:00
Dominik Sokal 6f806cabb1 add dev script (#868) 2021-12-21 16:27:51 +01:00
Dominik Sokal 4c84794a31 fix install.sh (#867) 2021-12-21 16:07:57 +01:00
Expo CI a1ab5a4fb5 update CHANGELOG.md 2021-12-21 14:26:16 +00:00
Dominik Sokal dc11c1c487 v0.42.4 2021-12-21 15:09:15 +01:00
Dominik Sokal 805865356c install missing @oclif/plugin-help (#865)
* install missing @oclif/plugin-help

* update CHANGELOG.md
2021-12-21 14:59:37 +01:00
Expo CI 5fc0757fb0 update CHANGELOG.md 2021-12-21 13:42:56 +00:00
Dominik Sokal 2c6d92745f v0.42.3 2021-12-21 14:34:00 +01:00
Dominik Sokal f677e038cf upgrade eas-cli-local-build-plugin to 0.0.57 2021-12-21 14:33:45 +01:00
Dominik Sokal ef13c2bf0e really fix windows release process 2021-12-21 14:33:32 +01:00
Dominik Sokal 68e552010e v0.42.2 2021-12-21 14:25:19 +01:00
Dominik Sokal c5143fabad fix windows release process 2021-12-21 14:22:53 +01:00
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
125 changed files with 30588 additions and 28102 deletions

No files matched your search

+12 -1
View File
@@ -14,6 +14,11 @@ body:
- type: markdown
attributes:
value: If you leave out sections there is a high likelihood your issue will be closed. If you have a question, not a bug report, please post it on our [forums](https://forums.expo.dev/) instead.
- type: input
attributes:
label: Build/Submit details page URL
description: If you're reporting an issue with EAS Build and/or EAS Submit, please provide the URL to the details page URL. The page can be only viewed by the Expo team members. Providing the URL is optional but it makes debugging the issue easier.
placeholder: https://expo.dev/accounts/ACCOUNT_NAME/projects/PROJECT_NAME/builds/BUILD_ID
- type: textarea
validations:
required: true
@@ -21,12 +26,18 @@ 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: input
validations:
required: true
attributes:
label: Managed or bare?
description: 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. See the [Workflows](https://docs.expo.dev/introduction/managed-vs-bare/) page for more information.
- type: textarea
validations:
required: true
attributes:
label: Environment
placeholder: Run `eas diagnostics` and paste the output here
description: Run `eas diagnostics` and paste the output here.
- type: textarea
validations:
required: false
+2
View File
@@ -15,4 +15,6 @@ jobs:
steps:
- uses: actions/checkout@v2
- run: bash bin/install.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: eas --version
+28 -13
View File
@@ -32,15 +32,23 @@ jobs:
- 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_name: eas-linux-x64.tar.gz
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
asset_name: ${{ steps.x64.outputs.filename }}
asset_content_type: application/gzip
- name: Upload linux-arm tarball
uses: actions/upload-release-asset@v1
@@ -48,8 +56,8 @@ 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_name: eas-linux-arm.tar.gz
asset_path: packages/eas-cli/dist/${{ steps.arm.outputs.filename }}
asset_name: ${{ steps.arm.outputs.filename }}
asset_content_type: application/gzip
build-mac:
name: Build for macOS
@@ -65,15 +73,20 @@ jobs:
- 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_name: eas-darwin-x64.tar.gz
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
asset_name: ${{ steps.x64.outputs.filename }}
asset_content_type: application/gzip
build-windows:
name: Build for Windows
@@ -87,21 +100,23 @@ jobs:
- 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)"
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
- id: x86
run: echo "::set-output name=filename::$(ls eas-*-x86.exe)"
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
- name: Upload x64 installer
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/win/${{ steps.x64.outputs.filename }}
asset_path: packages/eas-cli/dist/win32/${{ steps.x64.outputs.filename }}
asset_name: ${{ steps.x64.outputs.filename }}
asset_content_type: application/gzip
- name: Upload x86 installer
@@ -110,7 +125,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/win/${{ steps.x86.outputs.filename }}
asset_path: packages/eas-cli/dist/win32/${{ steps.x86.outputs.filename }}
asset_name: ${{ steps.x86.outputs.filename }}
asset_content_type: application/gzip
publish-to-npm:
+3 -1
View File
@@ -9,10 +9,12 @@ jobs:
- uses: actions/checkout@v2
- 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
View File
@@ -1,6 +1,7 @@
# Build directories
packages/*/build
packages/eas-cli/dist
packages/eas-cli/tmp
# Code editors
.idea
+157 -2
View File
@@ -6,17 +6,172 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
- Drop support for building iOS builds on macOS Catalina. ([#939](https://github.com/expo/eas-cli/pull/939) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
### 🐛 Bug fixes
- Fix URL placeholder for iOS submits. ([#941](https://github.com/expo/eas-cli/pull/941) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Unify EAS command descriptions style. ([#925](https://github.com/expo/eas-cli/pull/925) by [@dsokal](https://github.com/dsokal))
- Upgrade dependencies. ([#939](https://github.com/expo/eas-cli/pull/939) by [@dsokal](https://github.com/dsokal))
## [0.45.1](https://github.com/expo/eas-cli/releases/tag/v0.45.1) - 2022-01-19
### 🎉 New features
- Add json output for started builds when using `--no-wait`. ([#921](https://github.com/expo/eas-cli/pull/921) by [@kbrandwijk](https://github.com/kbrandwijk))
- Add support to `eas build` for specifying a custom `prebuildCommand` in `eas.json`. ([#919](https://github.com/expo/eas-cli/pull/919) by [@kbrandwijk](https://github.com/kbrandwijk))
### 🧹 Chores
- Add macOS Monterey image. ([#922](https://github.com/expo/eas-cli/pull/922) by [@wkozyra95](https://github.com/wkozyra95))
## [0.45.0](https://github.com/expo/eas-cli/releases/tag/v0.45.0) - 2022-01-18
### 🎉 New features
- Add `eas device:delete`. ([#890](https://github.com/expo/eas-cli/pull/890) by [@kbrandwijk](https://github.com/kbrandwijk))
### 🧹 Chores
- Upgrade dependencies. ([#872](https://github.com/expo/eas-cli/pull/872) by [@dsokal](https://github.com/dsokal))
## [0.44.1](https://github.com/expo/eas-cli/releases/tag/v0.44.1) - 2022-01-11
### 🐛 Bug fixes
- Fix build:configure expo-updates configuration to only run on generic projects. ([#904](https://github.com/expo/eas-cli/pull/904) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
- Upgrade `node-forge` to 1.0.0. ([#902](https://github.com/expo/eas-cli/pull/902) by [@dependabot](https://github.com/apps/dependabot) + [@dsokal](https://github.com/dsokal))
- Replace `@expo/plugin-autocomplete` with `@oclif/plugin-autocomplete`. Upgrade oclif deps. ([#903](https://github.com/expo/eas-cli/pull/903) by [@dsokal](https://github.com/dsokal))
## [0.44.0](https://github.com/expo/eas-cli/releases/tag/v0.44.0) - 2022-01-11
### 🛠 Breaking changes
- No longer enable APNS (iOS Push Notifications) capability by default anymore. ([#797](https://github.com/expo/eas-cli/pull/797) by [@EvanBacon](https://github.com/EvanBacon))
- Don't configure Android projects locally. Clean up old Gradle signing config. Streamline `expo-updates` configuration. ([#888](https://github.com/expo/eas-cli/pull/888) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Add `update:list` command. ([#884](https://github.com/expo/eas-cli/pull/884) by [@jkhales](https://github.com/jkhales))
- Improve error message for outdated Apple PLA error. ([#889](https://github.com/expo/eas-cli/pull/889) by [@EvanBacon](https://github.com/EvanBacon))
- Add `--output` flag to the build command. ([#885](https://github.com/expo/eas-cli/pull/885) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Fix creating project archive with symlink cycle. ([#891](https://github.com/expo/eas-cli/pull/891) by [@wkozyra95](https://github.com/wkozyra95))
- Sign Android debug builds with the correct keystore. Previously, all debug builds would be signed with a default debug keystore. ([#888](https://github.com/expo/eas-cli/pull/888) by [@dsokal](https://github.com/dsokal))
## [0.43.0](https://github.com/expo/eas-cli/releases/tag/v0.43.0) - 2022-01-03
### 🎉 New features
- Add `eas channel:delete`. ([#846](https://github.com/expo/eas-cli/pull/846), [#869](https://github.com/expo/eas-cli/pull/869) by [@jkhales](https://github.com/jkhales))
- Add `eas autocomplete`. ([#870](https://github.com/expo/eas-cli/pull/870) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix saving `build:inspect` results to project subdirectory. ([#863](https://github.com/expo/eas-cli/pull/863) by [@wkozyra95](https://github.com/wkozyra95))
## [0.42.4](https://github.com/expo/eas-cli/releases/tag/v0.42.4) - 2021-12-21
### 🐛 Bug fixes
- Add missing dependency - `@oclif/plugin-help`. ([#865](https://github.com/expo/eas-cli/pull/865) by [@dsokal](https://github.com/dsokal))
## [0.42.3](https://github.com/expo/eas-cli/releases/tag/v0.42.3) - 2021-12-21
### 🐛 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))
### 🧹 Chores
## [0.38.1](https://github.com/expo/eas-cli/releases/tag/v0.38.1) - 2021-11-25
### 🐛 Bug fixes
+4 -5
View File
@@ -11,7 +11,7 @@
prefix="${EAS_PREFIX:-/usr/local}"
temp_dir="$(mktemp -d)"
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]
then
# Use colors when stdout is a terminal and NO_COLOR isn't set.
@@ -67,7 +67,7 @@
if [[ ! ":$PATH:" == *":$prefix/bin:"* ]]; then
abort "Your path is missing $prefix/bin, you need to add this to \$PATH use this installer."
fi
hardware="$(uname -m)"
case "$hardware" in
x86_64)
@@ -95,13 +95,12 @@
abort "This installer is supported only on macOS, Linux, and Windows Subsystem for Linux."
;;
esac
url="https://github.com/expo/eas-cli/releases/latest/download/eas-$platform-$arch.tar.gz"
url=$(hub release -L 1 | xargs hub release show --show-downloads | grep "${platform}-${arch}.tar.gz")
echo "Installing EAS CLI from $url"
mkdir -p "$temp_dir"
cd "$temp_dir"
echo
echo "Downloading..."
if [ "$(command -v curl)" ]
+1 -1
View File
@@ -1 +1 @@
../packages/eas-cli/bin/run
../packages/eas-cli/bin/dev
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.38.2"
"version": "0.46.0"
}
+12 -11
View File
@@ -5,30 +5,31 @@
],
"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 .",
"release": "./scripts/bin/run update-local-plugin && lerna version",
"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/jest": "27.4.0",
"@types/node": "16.7.8",
"eslint": "7.32.0",
"eslint-config-universe": "8.0.0",
"eslint": "8.7.0",
"eslint-config-universe": "10.0.0",
"eslint-plugin-async-protect": "1.1.0",
"eslint-plugin-node": "11.1.0",
"jest": "27.1.0",
"jest-watch-typeahead": "0.6.4",
"jest": "27.4.7",
"jest-watch-typeahead": "1.0.0",
"lerna": "4.0.0",
"prettier": "2.4.1",
"ts-jest": "27.0.5",
"typescript": "4.5.2"
"oclif": "2.3.0",
"prettier": "2.5.1",
"ts-jest": "27.1.3",
"typescript": "4.5.5"
},
"volta": {
"node": "16.10.0",
+32 -390
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
@@ -33,413 +33,55 @@ eas --help COMMAND
# Commands
<!-- commands -->
* [`eas account:login`](#eas-accountlogin)
* [`eas account:logout`](#eas-accountlogout)
* [`eas account:view`](#eas-accountview)
* [`eas analytics [STATUS]`](#eas-analytics-status)
* [`eas build`](#eas-build)
* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
* [`eas build:configure`](#eas-buildconfigure)
* [`eas build:list`](#eas-buildlist)
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
* [`eas config`](#eas-config)
* [`eas credentials`](#eas-credentials)
* [`eas device:create`](#eas-devicecreate)
* [`eas device:list`](#eas-devicelist)
* [`eas device:view [UDID]`](#eas-deviceview-udid)
* [`eas diagnostics`](#eas-diagnostics)
* [`eas autocomplete [SHELL]`](#eas-autocomplete-shell)
* [`eas help [COMMAND]`](#eas-help-command)
* [`eas project:info`](#eas-projectinfo)
* [`eas project:init`](#eas-projectinit)
* [`eas secret:create`](#eas-secretcreate)
* [`eas secret:delete`](#eas-secretdelete)
* [`eas secret:list`](#eas-secretlist)
* [`eas submit`](#eas-submit)
* [`eas webhook:create`](#eas-webhookcreate)
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
* [`eas webhook:list`](#eas-webhooklist)
* [`eas webhook:update`](#eas-webhookupdate)
* [`eas webhook:view ID`](#eas-webhookview-id)
## `eas account:login`
## `eas autocomplete [SHELL]`
log in with your Expo account
display autocomplete installation instructions
```
USAGE
$ eas account:login
$ eas autocomplete [SHELL] [-r]
ALIASES
$ eas login
ARGUMENTS
SHELL shell type
FLAGS
-r, --refresh-cache Refresh cache (ignores displaying instructions)
DESCRIPTION
display autocomplete installation instructions
EXAMPLES
$ eas autocomplete
$ eas autocomplete bash
$ eas autocomplete zsh
$ eas autocomplete --refresh-cache
```
## `eas account:logout`
log out
```
USAGE
$ eas account:logout
ALIASES
$ eas logout
```
## `eas account:view`
show the username you are logged in as
```
USAGE
$ eas account:view
ALIASES
$ eas whoami
```
## `eas analytics [STATUS]`
view or change analytics settings
```
USAGE
$ eas analytics [STATUS]
```
## `eas build`
start a build
```
USAGE
$ eas build
OPTIONS
-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
```
## `eas build:cancel [BUILD_ID]`
Cancel a build.
```
USAGE
$ eas build:cancel [BUILD_ID]
```
## `eas build:configure`
Configure the project to support EAS Build.
```
USAGE
$ eas build:configure
OPTIONS
-p, --platform=(android|ios|all) Platform to configure
```
## `eas build:list`
list all builds for your project
```
USAGE
$ eas build:list
OPTIONS
--appBuildVersion=appBuildVersion
--appIdentifier=appIdentifier
--appVersion=appVersion
--buildProfile=buildProfile
--channel=channel
--distribution=(store|internal|simulator)
--gitCommitHash=gitCommitHash
--json Enable JSON output, non-JSON messages will be printed
to stderr
--limit=limit
--platform=(all|android|ios)
--runtimeVersion=runtimeVersion
--sdkVersion=sdkVersion
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
## `eas build:view [BUILD_ID]`
view a build for your project
```
USAGE
$ eas build:view [BUILD_ID]
OPTIONS
--json Enable JSON output, non-JSON messages will be printed to stderr
```
## `eas config`
show the eas.json config
```
USAGE
$ eas config
OPTIONS
-p, --platform=(android|ios)
--profile=profile
```
## `eas credentials`
manage your credentials
```
USAGE
$ eas credentials
```
## `eas device:create`
register new Apple Devices to use for internal distribution
```
USAGE
$ eas device:create
```
## `eas device:list`
list all registered devices for your account
```
USAGE
$ eas device:list
OPTIONS
--apple-team-id=apple-team-id
```
## `eas device:view [UDID]`
view a device for your project
```
USAGE
$ eas device:view [UDID]
```
## `eas diagnostics`
log environment info to the console
```
USAGE
$ eas diagnostics
```
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v1.1.1/src/commands/autocomplete/index.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
```
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.5/src/commands/help.ts)_
## `eas project:info`
information about the current project
```
USAGE
$ eas project:info
```
## `eas project:init`
create or link an EAS project
```
USAGE
$ eas project:init
ALIASES
$ eas init
```
## `eas secret:create`
Create an environment secret on the current project or owner account.
```
USAGE
$ eas secret:create
OPTIONS
--force Delete and recreate existing secrets
--name=name Name of the secret
--scope=(account|project) [default: project] Scope for the secret
--value=value Value of the secret
```
## `eas secret:delete`
Delete an environment secret by ID.
```
USAGE
$ eas secret:delete
OPTIONS
--id=id ID of the secret to delete
FLAGS
-n, --nested-commands Include all nested commands in the output.
DESCRIPTION
Unsure where to find the secret's ID? Run eas secret:list
Display help for eas.
```
## `eas secret:list`
Lists environment secrets available for your current app
```
USAGE
$ eas secret:list
```
## `eas submit`
submit build archive to app store
```
USAGE
$ eas submit
OPTIONS
-p, --platform=(android|ios|all)
--id=id ID of the build to submit
--latest Submit the latest build for specified platform
--non-interactive Run command in non-interactive mode
--path=path Path to the .apk/.aab/.ipa file
--profile=profile Name of the submit profile from eas.json. Defaults to "production" if defined in
eas.json.
--url=url 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/
ALIASES
$ eas build:submit
```
## `eas webhook:create`
Create a webhook on the current project.
```
USAGE
$ eas webhook:create
OPTIONS
--event=(BUILD|SUBMIT) Event type that triggers the webhook
--secret=secret Secret used to create a hash signature of the request payload, provided in the
'Expo-Signature' header.
--url=url Webhook URL
```
## `eas webhook:delete [ID]`
Delete a webhook on the current project.
```
USAGE
$ eas webhook:delete [ID]
ARGUMENTS
ID ID of the webhook to delete
```
## `eas webhook:list`
List webhooks on the current project.
```
USAGE
$ eas webhook:list
OPTIONS
--event=(BUILD|SUBMIT) Event type that triggers the webhook
```
## `eas webhook:update`
Create a webhook on the current project.
```
USAGE
$ eas webhook:update
OPTIONS
--event=(BUILD|SUBMIT) Event type that triggers the webhook
--id=id (required) Webhook ID
--secret=secret Secret used to create a hash signature of the request payload, provided in the
'Expo-Signature' header.
--url=url Webhook URL
```
## `eas webhook:view ID`
View a webhook on the current project.
```
USAGE
$ eas webhook:view ID
ARGUMENTS
ID ID of the webhook to view
```
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.1.10/src/commands/help.ts)_
<!-- commandsstop -->
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env node
const oclif = require('@oclif/core');
const path = require('path');
const project = path.join(__dirname, '..', 'tsconfig.json');
// In dev mode -> use ts-node and dev plugins
process.env.NODE_ENV = 'development';
require('ts-node').register({ project });
// In dev mode, always show stack traces
oclif.settings.disableJsonFlag = true
// Start the CLI
oclif.run().then(oclif.flush).catch(oclif.Errors.handle);
+3
View File
@@ -0,0 +1,3 @@
@echo off
node "%~dp0\dev" %*
+3 -3
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
require('@oclif/command')
require('@oclif/core')
.run()
.then(require('@oclif/command/flush'))
.catch(require('@oclif/errors/handle'));
.then(require('@oclif/core/flush'))
.catch(require('@oclif/core/handle'));
+20507 -20089
View File
File diff suppressed because it is too large. Load diff
+62 -52
View File
@@ -1,62 +1,61 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.38.2",
"version": "0.46.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.26",
"@expo/config": "6.0.9",
"@expo/config-plugins": "4.0.9",
"@expo/eas-build-job": "0.2.57",
"@expo/eas-json": "^0.38.2",
"@expo/apple-utils": "0.0.0-alpha.27",
"@expo/config": "6.0.16",
"@expo/config-plugins": "4.0.16",
"@expo/eas-build-job": "0.2.65",
"@expo/eas-json": "0.46.0",
"@expo/json-file": "8.2.34",
"@expo/pkcs12": "0.0.6",
"@expo/plist": "0.0.16",
"@expo/plugin-warn-if-update-available": "1.7.0",
"@expo/prebuild-config": "3.0.9",
"@expo/plist": "0.0.17",
"@expo/plugin-warn-if-update-available": "2.0.4",
"@expo/prebuild-config": "3.0.16",
"@expo/results": "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.3",
"@oclif/config": "1.17.1",
"@oclif/errors": "1.3.5",
"@oclif/plugin-help": "3.2.5",
"@urql/core": "2.3.1",
"@urql/exchange-retry": "0.3.0",
"@oclif/core": "1.1.2",
"@oclif/plugin-autocomplete": "1.1.1",
"@oclif/plugin-help": "5.1.10",
"@urql/core": "2.3.6",
"@urql/exchange-retry": "0.3.2",
"chalk": "4.1.2",
"cli-table3": "0.6.0",
"dateformat": "4.5.1",
"cli-table3": "0.6.1",
"dateformat": "4.6.3",
"env-paths": "2.2.0",
"envinfo": "7.8.1",
"fast-glob": "3.2.7",
"fast-glob": "3.2.11",
"figures": "3.2.0",
"form-data": "3.0.0",
"form-data": "4.0.0",
"fs-extra": "10.0.0",
"getenv": "1.0.0",
"got": "11.8.2",
"gradle-to-js": "2.0.0",
"graphql": "15.5.1",
"graphql-tag": "2.12.5",
"ignore": "5.1.8",
"graphql": "16.2.0",
"graphql-tag": "2.12.6",
"ignore": "5.2.0",
"indent-string": "4.0.0",
"jks-js": "1.0.1",
"joi": "17.4.2",
"jks-js": "1.0.2",
"joi": "17.5.0",
"keychain": "1.3.0",
"log-symbols": "4.1.0",
"mime": "2.5.2",
"mime": "3.0.0",
"minimatch": "3.0.4",
"nanoid": "3.1.25",
"node-fetch": "2.6.1",
"node-forge": "0.10.0",
"nanoid": "3.2.0",
"node-fetch": "2.6.7",
"node-forge": "1.2.1",
"nullthrows": "1.1.1",
"ora": "5.1.0",
"pkg-dir": "4.2.0",
"prompts": "2.3.2",
"prompts": "2.4.2",
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
"semver": "7.3.5",
@@ -71,31 +70,31 @@
"wrap-ansi": "7.0.0"
},
"devDependencies": {
"@graphql-codegen/cli": "^1.19.2",
"@graphql-codegen/introspection": "^1.18.0",
"@graphql-codegen/typescript": "^1.17.11",
"@graphql-codegen/typescript-operations": "^1.17.10",
"@graphql-codegen/cli": "2.4.0",
"@graphql-codegen/introspection": "2.1.1",
"@graphql-codegen/typescript": "2.4.2",
"@graphql-codegen/typescript-operations": "2.2.2",
"@types/dateformat": "3.0.1",
"@types/envinfo": "7.8.1",
"@types/form-data": "^2.5.0",
"@types/fs-extra": "9.0.12",
"@types/fs-extra": "9.0.13",
"@types/getenv": "^1.0.0",
"@types/mime": "2.0.3",
"@types/node-fetch": "^2.5.7",
"@types/node-forge": "^0.9.5",
"@types/prompts": "^2.0.9",
"@types/semver": "7.3.8",
"@types/tar": "4.0.5",
"@types/node-forge": "1.0.0",
"@types/prompts": "2.0.14",
"@types/semver": "7.3.9",
"@types/tar": "6.1.1",
"@types/tough-cookie": "4.0.1",
"@types/uuid": "8.3.1",
"@types/uuid": "8.3.4",
"@types/wrap-ansi": "3.0.0",
"axios": "0.21.4",
"axios": "0.25.0",
"eslint-plugin-graphql": "4.0.0",
"memfs": "3.2.2",
"mockdate": "^3.0.2",
"nock": "^13.0.5",
"memfs": "3.4.1",
"mockdate": "3.0.5",
"nock": "13.2.2",
"tempy": "0.7.0",
"ts-deepmerge": "1.0.8"
"ts-deepmerge": "2.0.1",
"typescript": "4.5.5"
},
"engines": {
"node": ">=12.0.0"
@@ -116,12 +115,16 @@
"bin": "eas",
"commands": "./build/commands",
"plugins": [
"@oclif/plugin-autocomplete",
"@oclif/plugin-help",
"@expo/plugin-warn-if-update-available"
],
"topics": {
"account": {
"description": "manage your account"
"description": "manage account"
},
"branch": {
"description": "manage update branches"
},
"build": {
"description": "build app binaries"
@@ -130,10 +133,10 @@
"description": "manage update channels"
},
"device": {
"description": "manage your Apple devices for internal distribution"
"description": "manage Apple devices for Internal Distribution"
},
"project": {
"description": "manage your project"
"description": "manage project"
},
"release": {
"description": "manage update releases"
@@ -164,18 +167,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
+10
View File
@@ -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;
}
}
@@ -1,63 +1,29 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig, AndroidManifest } from '@expo/config-plugins';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { ensureValidVersions } from '../utils/updates';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
const androidManifest = await getAndroidManifestAsync(projectDir);
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, accountName)) {
const result = AndroidConfig.Updates.setUpdatesConfig(exp, androidManifest, accountName);
await AndroidConfig.Manifest.writeAndroidManifestAsync(androidManifestPath, result);
}
}
export async function syncUpdatesConfigurationAsync(
projectDir: string,
exp: ExpoConfig
): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
try {
await ensureUpdatesConfiguredAsync(projectDir);
} catch (error) {
Log.error(
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
);
throw error;
}
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
let androidManifest = await getAndroidManifestAsync(projectDir);
if (!AndroidConfig.Updates.areVersionsSynced(exp, androidManifest)) {
androidManifest = AndroidConfig.Updates.setVersionsConfig(exp, androidManifest);
await AndroidConfig.Manifest.writeAndroidManifestAsync(androidManifestPath, androidManifest);
}
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, accountName)) {
Log.warn(
'Native project configuration is not synced with values present in your app.json, run "eas build:configure" to make sure all values are applied in the native project'
);
}
}
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in AndroidManifest.xml
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
const androidManifest = await getAndroidManifestAsync(projectDir);
if (!AndroidConfig.Updates.isMainApplicationMetaDataSet(androidManifest)) {
throw new Error('Missing values in AndroidManifest.xml');
}
const updatedAndroidManifest = AndroidConfig.Updates.setUpdatesConfig(
exp,
androidManifest,
accountName
);
await AndroidConfig.Manifest.writeAndroidManifestAsync(
androidManifestPath,
updatedAndroidManifest
);
}
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
+3 -3
View File
@@ -24,9 +24,9 @@ import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
import { transformMetadata } from '../graphql';
import { logCredentialsSource } from '../utils/credentials';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
export async function createAndroidContextAsync(
ctx: CommonContext<Platform.ANDROID>
@@ -71,8 +71,8 @@ export async function prepareAndroidBuildAsync(
ensureCredentialsAsync: async (ctx: BuildContext<Platform.ANDROID>) => {
return await ensureAndroidCredentialsAsync(ctx);
},
ensureProjectConfiguredAsync: async () => {
await validateAndSyncProjectConfigurationAsync({
syncProjectConfigurationAsync: async () => {
await syncProjectConfigurationAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile: ctx.buildProfile,
@@ -18,6 +18,7 @@ export function transformJob(job: Android.Job): AndroidJobInput {
username: job.username,
buildType: job.buildType && transformBuildType(job.buildType),
developmentClient: job.developmentClient,
experimental: job.experimental,
};
}
@@ -75,6 +75,9 @@ export async function prepareJobAsync(
artifactPath: buildProfile.artifactPath,
buildType,
username,
experimental: {
prebuildCommand: ctx.buildProfile.prebuildCommand,
},
};
return sanitizeJob(job);
@@ -2,39 +2,17 @@ import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { AndroidVersionAutoIncrement, BuildProfile } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import Log from '../../log';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject,
} from '../../project/android/applicationId';
import { resolveWorkflowAsync } from '../../project/workflow';
import { getVcsClient } from '../../vcs';
import { ConfigureContext } from '../context';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
import { syncUpdatesConfigurationAsync } from './UpdatesModule';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void> {
if (!ctx.hasAndroidNativeProject) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
return;
}
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
const easGradlePath = AndroidConfig.EasBuild.getEasBuildGradlePath(ctx.projectDir);
await getVcsClient().trackFileAsync(easGradlePath);
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
}
Log.withTick('Android project configured');
}
export async function validateAndSyncProjectConfigurationAsync({
export async function syncProjectConfigurationAsync({
projectDir,
exp,
buildProfile,
@@ -48,11 +26,7 @@ export async function validateAndSyncProjectConfigurationAsync({
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
if (!(await AndroidConfig.EasBuild.isEasBuildGradleConfiguredAsync(projectDir))) {
throw new Error(
'Project is not configured. Please run "eas build:configure" to configure the project.'
);
}
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
}
@@ -73,3 +47,22 @@ function resolveVersionBumpStrategy(autoIncrement: AndroidVersionAutoIncrement):
return BumpStrategy.APP_VERSION;
}
}
// TODO: remove this after a few months
export async function cleanUpOldEasBuildGradleScriptAsync(projectDir: string): Promise<void> {
const easBuildGradlePath = path.join(projectDir, 'android', 'app', 'eas-build.gradle');
if (await fs.pathExists(easBuildGradlePath)) {
Log.withTick(`Removing ${chalk.bold('eas-build.gradle')} as it's not longer necessary`);
await fs.remove(easBuildGradlePath);
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf-8');
const buildGradleContentsWithoutApply = buildGradleContents.replace(
/apply from: ["'].\/eas-build.gradle["']\n/,
''
);
if (buildGradleContentsWithoutApply !== buildGradleContents) {
await fs.writeFile(buildGradlePath, buildGradleContentsWithoutApply);
}
}
}
+11 -12
View File
@@ -38,7 +38,7 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
ensureCredentialsAsync(
ctx: BuildContext<TPlatform>
): Promise<CredentialsResult<Credentials> | undefined>;
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
syncProjectConfigurationAsync(ctx: BuildContext<TPlatform>): Promise<void>;
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
}
@@ -60,14 +60,13 @@ export async function prepareBuildRequestForPlatformAsync<
trackingCtx: ctx.trackingCtx,
}
);
if (!ctx.skipProjectConfiguration) {
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(ctx), {
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
trackingCtx: ctx.trackingCtx,
});
}
await withAnalyticsAsync(async () => await builder.syncProjectConfigurationAsync(ctx), {
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
trackingCtx: ctx.trackingCtx,
});
if (await getVcsClient().isCommitRequiredAsync()) {
Log.addNewLineIfNone();
@@ -77,7 +76,7 @@ export async function prepareBuildRequestForPlatformAsync<
);
}
const projectArchive = ctx.local
const projectArchive = ctx.localBuildOptions.enable
? ({
type: ArchiveSourceType.PATH,
path: (await makeProjectTarballAsync()).path,
@@ -94,8 +93,8 @@ export async function prepareBuildRequestForPlatformAsync<
});
return async () => {
if (ctx.local) {
await runLocalBuildAsync(job);
if (ctx.localBuildOptions.enable) {
await runLocalBuildAsync(job, ctx.localBuildOptions);
return undefined;
} else {
try {
+36 -125
View File
@@ -1,97 +1,47 @@
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 chalk from 'chalk';
import fs from 'fs-extra';
import Log, { learnMore } from '../log';
import { RequestedPlatform } from '../platform';
import Log from '../log';
import { resolveWorkflowAsync } from '../project/workflow';
import { confirmAsync, promptAsync } from '../prompts';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs';
import { configureAndroidAsync } from './android/configure';
import { ConfigureContext } from './context';
import { configureIosAsync } from './ios/configure';
import { commitPromptAsync, maybeBailOnRepoStatusAsync } from './utils/repository';
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
const configureCommitMessage = {
[RequestedPlatform.Android]: 'Configure EAS Build for Android',
[RequestedPlatform.Ios]: 'Configure EAS Build for iOS',
[RequestedPlatform.All]: 'Configure EAS Build',
};
export async function ensureProjectConfiguredAsync(
projectDir: string,
requestedPlatform: RequestedPlatform
): Promise<void> {
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir))) {
return;
}
const message = 'This project is not configured to build with EAS. Set it up now?';
const confirm = await confirmAsync({ message });
if (confirm) {
await configureAsync({
projectDir,
platform: requestedPlatform,
});
if (await getVcsClient().isCommitRequiredAsync()) {
error(
'Build process requires clean working tree, please commit all your changes and run `eas build` again',
{ exit: 1 }
);
}
} else {
error(
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
'https://docs.expo.dev/build/eas-json'
)})`,
{ exit: 1 }
);
}
interface ConfigureParams {
projectDir: string;
nonInteractive: boolean;
}
export async function configureAsync(options: {
platform: RequestedPlatform;
projectDir: string;
}): Promise<void> {
/**
* Creates eas.json if it does not exist.
*
* Returns:
* - false - if eas.json already exists
* - true - if eas.json was created by the function
*/
export async function ensureProjectConfiguredAsync(
configureParams: ConfigureParams
): Promise<boolean> {
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(configureParams.projectDir))) {
return false;
}
await configureAsync(configureParams);
return true;
}
async function configureAsync({ projectDir, nonInteractive }: ConfigureParams): Promise<void> {
await maybeBailOnRepoStatusAsync();
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
const ctx: ConfigureContext = {
user: await ensureLoggedInAsync(),
projectDir: options.projectDir,
exp,
requestedPlatform: options.platform,
shouldConfigureAndroid: [RequestedPlatform.All, RequestedPlatform.Android].includes(
options.platform
),
shouldConfigureIos: [RequestedPlatform.All, RequestedPlatform.Ios].includes(options.platform),
hasAndroidNativeProject:
(await resolveWorkflowAsync(options.projectDir, Platform.ANDROID)) === Workflow.GENERIC,
hasIosNativeProject:
(await resolveWorkflowAsync(options.projectDir, Platform.IOS)) === Workflow.GENERIC,
};
Log.newLine();
await ensureEasJsonExistsAsync(ctx);
if (ctx.shouldConfigureAndroid) {
await configureAndroidAsync(ctx);
}
if (ctx.shouldConfigureIos) {
await configureIosAsync(ctx);
}
await createEasJsonAsync(projectDir);
if (await getVcsClient().isCommitRequiredAsync()) {
Log.newLine();
await reviewAndCommitChangesAsync(configureCommitMessage[options.platform]);
} else if (!(await getVcsClient().hasUncommittedChangesAsync())) {
Log.newLine();
Log.withTick('No changes were necessary, the project is already configured correctly.');
await reviewAndCommitChangesAsync('Configure EAS Build', {
nonInteractive,
});
}
}
@@ -138,58 +88,19 @@ const EAS_JSON_BARE_DEFAULT: EasJson = {
},
};
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
const easJsonPath = EasJsonReader.formatEasJsonPath(ctx.projectDir);
if (await fs.pathExists(easJsonPath)) {
const reader = new EasJsonReader(ctx.projectDir);
await reader.readAsync();
Log.withTick('Validated eas.json');
return;
}
async function createEasJsonAsync(projectDir: string): Promise<void> {
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
const hasAndroidNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
const hasIosNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.IOS)) === Workflow.GENERIC;
const easJson =
ctx.hasAndroidNativeProject && ctx.hasIosNativeProject
hasAndroidNativeProject || hasIosNativeProject
? EAS_JSON_BARE_DEFAULT
: EAS_JSON_MANAGED_DEFAULT;
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
await getVcsClient().trackFileAsync(easJsonPath);
Log.withTick('Generated eas.json');
}
enum ShouldCommitChanges {
Yes,
ShowDiffFirst,
Skip,
}
async function reviewAndCommitChangesAsync(
initialCommitMessage: string,
askedFirstTime: boolean = true
): Promise<void> {
const { selected } = await promptAsync({
type: 'select',
name: 'selected',
message: 'Can we commit these changes to git for you?',
choices: [
{ title: 'Yes', value: ShouldCommitChanges.Yes },
...(askedFirstTime
? [{ title: 'Show the diff and ask me again', value: ShouldCommitChanges.ShowDiffFirst }]
: []),
{
title: 'Skip committing changes, I will do it later on my own',
value: ShouldCommitChanges.Skip,
},
],
});
if (selected === ShouldCommitChanges.Yes) {
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await getVcsClient().showDiffAsync();
await reviewAndCommitChangesAsync(initialCommitMessage, false);
}
Log.withTick(`Generated ${chalk.bold('eas.json')}`);
}
+2 -14
View File
@@ -6,21 +6,10 @@ 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 { GradleBuildContext } from '../project/android/gradle';
import { XcodeBuildContext } from '../project/ios/scheme';
import { Actor } from '../user/User';
export interface ConfigureContext {
user: Actor;
projectDir: string;
exp: ExpoConfig;
requestedPlatform: RequestedPlatform;
shouldConfigureAndroid: boolean;
shouldConfigureIos: boolean;
hasAndroidNativeProject: boolean;
hasIosNativeProject: boolean;
}
import { LocalBuildOptions } from './local';
export type CommonContext<T extends Platform> = Omit<BuildContext<T>, 'android' | 'ios'>;
@@ -44,13 +33,12 @@ export interface BuildContext<T extends Platform> {
clearCache: boolean;
credentialsCtx: CredentialsContext;
exp: ExpoConfig;
local: boolean;
localBuildOptions: LocalBuildOptions;
nonInteractive: boolean;
platform: T;
projectDir: string;
projectId: string;
projectName: string;
skipProjectConfiguration: boolean;
trackingCtx: TrackingContext;
user: Actor;
workflow: Workflow;
+5 -7
View File
@@ -15,25 +15,24 @@ 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,
local,
localBuildOptions,
nonInteractive = false,
platform,
projectDir,
skipProjectConfiguration = false,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
clearCache: boolean;
local: boolean;
localBuildOptions: LocalBuildOptions;
nonInteractive: boolean;
platform: T;
projectDir: string;
skipProjectConfiguration: boolean;
}): Promise<BuildContext<T>> {
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
@@ -49,6 +48,7 @@ export async function createBuildContextAsync<T extends Platform>({
nonInteractive,
projectDir,
user,
env: buildProfile.env,
});
const devClientProperties = getDevClientEventProperties({
@@ -60,7 +60,6 @@ export async function createBuildContextAsync<T extends Platform>({
tracking_id: uuidv4(),
platform,
...(accountId && { account_id: accountId }),
account_name: accountName,
project_id: projectId,
project_type: workflow,
...devClientProperties,
@@ -74,13 +73,12 @@ export async function createBuildContextAsync<T extends Platform>({
clearCache,
credentialsCtx,
exp,
local,
localBuildOptions,
nonInteractive,
platform,
projectDir,
projectId,
projectName,
skipProjectConfiguration,
trackingCtx,
user,
workflow,
@@ -1,60 +1,21 @@
import { ExpoConfig } from '@expo/config';
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';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
let expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
expoPlist = IOSConfig.Updates.setUpdatesConfig(exp, expoPlist, accountName);
await writeExpoPlistAsync(projectDir, expoPlist);
}
// TODO: ensure ExpoPlist in pbxproj
}
export async function syncUpdatesConfigurationAsync(
projectDir: string,
exp: ExpoConfig
): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
try {
await ensureUpdatesConfiguredAsync(projectDir);
} catch (error) {
Log.error(
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
);
throw error;
}
let expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistVersionConfigurationSynced(exp, expoPlist)) {
expoPlist = IOSConfig.Updates.setVersionsConfig(exp, expoPlist);
await writeExpoPlistAsync(projectDir, expoPlist);
}
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
Log.warn(
'Native project configuration is not synced with values present in your app.json, run "eas build:configure" to make sure all values are applied in the native project'
);
}
}
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in Expo.plist
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
const expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistConfigurationSet(expoPlist)) {
throw new Error('Missing values in Expo.plist');
}
const updatedExpoPlist = IOSConfig.Updates.setUpdatesConfig(exp, expoPlist, accountName);
await writeExpoPlistAsync(projectDir, updatedExpoPlist);
}
async function readExpoPlistAsync(projectDir: string): Promise<IOSConfig.ExpoPlist> {
@@ -82,8 +43,10 @@ export async function readReleaseChannelSafelyAsync(projectDir: string): Promise
export async function readChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
const expoPlist: any = await readExpoPlistAsync(projectDir); // TODO-JJ remove any once IOSConfig.ExpoPlist is updated to include `EXUpdatesRequestHeaders : Record<string,string>`
const updatesRequestHeaders = expoPlist['EXUpdatesRequestHeaders'] ?? {}; //TODO-JJ 'EXUpdatesRequestHeaders' IosConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY once https://github.com/expo/expo-cli/pull/3571 is published
// TODO-JJ remove any once IOSConfig.ExpoPlist is updated to include `EXUpdatesRequestHeaders : Record<string,string>`
const expoPlist: any = await readExpoPlistAsync(projectDir);
//TODO-JJ 'EXUpdatesRequestHeaders' IosConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY once https://github.com/expo/expo-cli/pull/3571 is published
const updatesRequestHeaders = expoPlist['EXUpdatesRequestHeaders'] ?? {};
return updatesRequestHeaders['expo-channel-name'] ?? null;
} catch (err) {
return null;
+3 -3
View File
@@ -11,10 +11,10 @@ import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from
import { BuildContext, CommonContext, IosBuildContext } from '../context';
import { transformMetadata } from '../graphql';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { ensureIosCredentialsAsync } from './credentials';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
export async function createIosContextAsync(
ctx: CommonContext<Platform.IOS>
@@ -63,8 +63,8 @@ export async function prepareIosBuildAsync(
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
return ensureIosCredentialsAsync(ctx, ctx.ios.targets);
},
ensureProjectConfiguredAsync: async () => {
await validateAndSyncProjectConfigurationAsync({
syncProjectConfigurationAsync: async () => {
await syncProjectConfigurationAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile: ctx.buildProfile,
@@ -20,6 +20,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
username: job.username,
developmentClient: job.developmentClient,
simulator: job.simulator,
experimental: job.experimental,
};
}
@@ -66,6 +66,9 @@ export async function prepareJobAsync(
buildConfiguration: ctx.buildProfile.buildConfiguration,
artifactPath: ctx.buildProfile.artifactPath,
username,
experimental: {
prebuildCommand: ctx.buildProfile.prebuildCommand,
},
};
return sanitizeJob(job);
}
@@ -3,32 +3,12 @@ import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import Log from '../../log';
import {
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject,
} from '../../project/ios/bundleIdentifier';
import { resolveWorkflowAsync } from '../../project/workflow';
import { ConfigureContext } from '../context';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
import { syncUpdatesConfigurationAsync } from './UpdatesModule';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
if (!ctx.hasIosNativeProject) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
return;
}
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
}
Log.withTick('iOS project configured');
}
export async function validateAndSyncProjectConfigurationAsync({
export async function syncProjectConfigurationAsync({
projectDir,
exp,
buildProfile,
+47 -6
View File
@@ -1,15 +1,56 @@
import { Job } from '@expo/eas-build-job';
import spawnAsync from '@expo/spawn-async';
import { ChildProcess } from 'child_process';
import semver from 'semver';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.50';
import { ora } from '../ora';
export async function runLocalBuildAsync(job: Job): Promise<void> {
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.62';
export interface LocalBuildOptions {
enable: boolean;
skipCleanup?: boolean;
skipNativeBuild?: boolean;
artifactsDir?: string;
artifactPath?: string;
workingdir?: string;
verbose?: boolean;
}
export async function runLocalBuildAsync(job: Job, options: LocalBuildOptions): Promise<void> {
const { command, args } = await getCommandAndArgsAsync(job);
await spawnAsync(command, args, {
stdio: 'inherit',
});
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 } : {}),
...(options.artifactPath ? { EAS_LOCAL_BUILD_ARTIFACT_PATH: options.artifactPath } : {}),
},
});
childProcess = spawnPromise.child;
await spawnPromise;
} finally {
process.removeListener('SIGINT', interruptHandler);
spinner?.stop();
}
}
async function getCommandAndArgsAsync(job: Job): Promise<{ command: string; args: string[] }> {
+16
View File
@@ -1,6 +1,8 @@
import { Updates } from '@expo/config-plugins';
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
import { IosEnterpriseProvisioning } from '@expo/eas-json';
import fs from 'fs-extra';
import resolveFrom from 'resolve-from';
import Log from '../log';
import { getUsername } from '../project/projectUtils';
@@ -48,6 +50,7 @@ export async function collectMetadataAsync<T extends Platform>(
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
reactNativeVersion: await getReactNativeVersionAsync(ctx),
...channelOrReleaseChannel,
distribution,
appName: ctx.exp.name,
@@ -141,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,262 @@
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 { printJsonOnlyOutput } from '../utils/json';
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;
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,
nonInteractive: flags.nonInteractive,
});
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) {
if (flags.json) {
printJsonOnlyOutput(startedBuilds.map(buildInfo => buildInfo.build));
}
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,
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);
}
}
@@ -1,5 +1,5 @@
import { Workflow } from '@expo/eas-build-job';
import { error } from '@oclif/errors';
import { Errors } from '@oclif/core';
import chalk from 'chalk';
import resolveFrom from 'resolve-from';
@@ -64,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,
});
}
@@ -83,7 +83,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
initial: false,
});
if (!shouldContinue) {
error('Come back later', { exit: 1 });
Errors.error('Come back later', { exit: 1 });
}
}
}
@@ -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 {
@@ -21,16 +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> {
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 {
@@ -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> {
+2 -2
View File
@@ -3,14 +3,14 @@ import Log from '../log';
import UserSettings from '../user/UserSettings';
export default class AnalyticsView extends EasCommand {
static description = 'view or change analytics settings';
static description = 'display or change analytics settings';
static args = [{ name: 'STATUS', options: ['on', 'off'] }];
protected requiresAuthentication = false;
async runAsync(): Promise<void> {
const { STATUS: status } = this.parse(AnalyticsView).args;
const { STATUS: status } = (await this.parse(AnalyticsView)).args;
if (status) {
await UserSettings.setAsync('analyticsEnabled', status === 'on');
Log.withTick(`${status === 'on' ? 'Enabling' : 'Disabling'} analytics.`);
+13 -11
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 gql from 'graphql-tag';
@@ -17,6 +17,7 @@ import {
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { getVcsClient } from '../../vcs';
export async function createUpdateBranchOnAppAsync({
@@ -51,8 +52,7 @@ export async function createUpdateBranchOnAppAsync({
}
export default class BranchCreate extends EasCommand {
static hidden = true;
static description = 'Create a branch on the current project.';
static description = 'create a branch';
static args = [
{
@@ -63,7 +63,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 +73,10 @@ export default class BranchCreate extends EasCommand {
let {
args: { name },
flags,
} = this.parse(BranchCreate);
} = await this.parse(BranchCreate);
if (flags.json) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -99,12 +102,11 @@ export default class BranchCreate extends EasCommand {
const newBranch = await createUpdateBranchOnAppAsync({ appId: projectId, name });
if (flags.json) {
Log.log(newBranch);
return;
printJsonOnlyOutput(newBranch);
} else {
Log.withTick(
`️Created a new branch: ${chalk.bold(newBranch.name)} on project ${chalk.bold(fullName)}.`
);
}
Log.withTick(
`️Created a new branch: ${chalk.bold(newBranch.name)} on project ${chalk.bold(fullName)}.`
);
}
}
+13 -10
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 gql from 'graphql-tag';
@@ -19,6 +19,7 @@ import {
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
async function getBranchInfoAsync({
appId,
@@ -76,8 +77,7 @@ async function deleteBranchOnAppAsync({
}
export default class BranchDelete extends EasCommand {
static hidden = true;
static description = 'Republish an update group';
static description = 'delete a branch';
static args = [
{
@@ -87,7 +87,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 +97,10 @@ export default class BranchDelete extends EasCommand {
let {
args: { name },
flags: { json: jsonFlag },
} = this.parse(BranchDelete);
} = await this.parse(BranchDelete);
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -142,11 +145,11 @@ export default class BranchDelete extends EasCommand {
});
if (jsonFlag) {
Log.log(JSON.stringify(deletionResult));
printJsonOnlyOutput(deletionResult);
} else {
Log.withTick(
`️Deleted branch "${name}" and all of its updates on project ${chalk.bold(fullName)}.`
);
}
Log.withTick(
`️Deleted branch "${name}" and all of its updates on project ${chalk.bold(fullName)}.`
);
}
}
+10 -8
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';
@@ -16,6 +16,7 @@ import { UpdateBranchFragmentNode } from '../../graphql/types/UpdateBranch';
import Log from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
const BRANCHES_LIMIT = 10_000;
@@ -54,29 +55,30 @@ export async function listBranchesAsync({
}
export default class BranchList extends EasCommand {
static hidden = true;
static description = 'List all branches on this project.';
static description = 'list all branches';
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);
if (flags.json) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const branches = await listBranchesAsync({ projectId });
if (flags.json) {
Log.log(JSON.stringify(branches, null, 2));
printJsonOnlyOutput(branches);
} else {
const table = new CliTable({
head: ['branch', ...UPDATE_COLUMNS],
head: ['Branch', ...UPDATE_COLUMNS],
wordWrap: true,
});
+2 -485
View File
@@ -1,493 +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 { createUpdateChannelOnAppAsync } from '../channel/create';
import { createUpdateBranchOnAppAsync } from './create';
import { listBranchesAsync } from './list';
import { viewUpdateBranchAsync } from './view';
const EAS_UPDATE_URL = 'https://u.expo.dev';
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 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);
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 });
branchName = await selectAsync<string>(
'which branch would you like to publish on?',
branches.map(branch => {
return {
title: `${branch.name} ${chalk.grey(
`- current update: ${formatUpdate(branch.updates[0])}`
)}`,
value: branch.name,
};
})
);
assert(branchName, 'branch name must be specified.');
}
const { id: branchId, updates } = await ensureBranchExistsAsync({
appId: projectId,
name: branchName,
});
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
let oldMessage: string, oldRuntimeVersion: string;
if (republish) {
// If we are republishing, we don't need to worry about building the bundle or uploading the assets.
// Instead we get the `updateInfoGroup` from the update we wish to republish.
let updatesToRepublish: Pick<
Update,
'group' | 'message' | 'runtimeVersion' | 'manifestFragment' | 'platform'
>[];
if (group) {
updatesToRepublish = await getUpdateGroupAsync({ group });
} else {
// Drop into interactive mode if the user has not specified an update group to republish.
if (jsonFlag) {
throw new Error('You must specify the update group to republish.');
}
const updateGroups = uniqBy(updates, u => u.group)
.filter(update => {
// Only show groups that have updates on the specified platform(s).
return platformFlag === 'all' || update.platform === platformFlag;
})
.map(update => ({
title: formatUpdateTitle(update),
value: update.group,
}));
if (updateGroups.length === 0) {
throw new Error(
`There are no updates on branch "${branchName}" published on the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
);
}
const selectedUpdateGroup = await selectAsync<string>(
'which update would you like to republish?',
updateGroups
);
updatesToRepublish = updates.filter(update => update.group === selectedUpdateGroup);
}
const updatesToRepublishFilteredByPlatform = updatesToRepublish.filter(
// Only republish to the specified platforms
update => platformFlag === 'all' || update.platform === platformFlag
);
if (updatesToRepublishFilteredByPlatform.length === 0) {
throw new Error(
`There are no updates on branch "${branchName}" published on the platform(s) "${platformFlag}" with group ID "${
group ? group : updatesToRepublish[0].group
}". Did you mean to publish a new update instead?`
);
}
let publicationPlatformMessage: string;
if (platformFlag === 'all') {
if (updatesToRepublishFilteredByPlatform.length !== defaultPublishPlatforms.length) {
Log.warn(`You are republishing an update that wasn't published for all platforms.`);
}
publicationPlatformMessage = `The republished update will appear on the same plaforms it was originally published on: ${updatesToRepublishFilteredByPlatform
.map(update => update.platform)
.join(',')}`;
} else {
publicationPlatformMessage = `The republished update will appear only on: ${platformFlag}`;
}
Log.withTick(publicationPlatformMessage);
for (const update of updatesToRepublishFilteredByPlatform) {
const { manifestFragment } = update;
const platform = update.platform as PublishPlatform;
unsortedUpdateInfoGroups[platform] = JSON.parse(manifestFragment);
}
// These are the same for each member of an update group
group = updatesToRepublishFilteredByPlatform[0].group;
oldMessage = updatesToRepublishFilteredByPlatform[0].message ?? '';
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
} else {
// build bundle and upload assets for a new publish
if (!skipBundler) {
await buildBundlesAsync({ projectDir, inputDir });
}
const assetSpinner = ora('Uploading assets...').start();
try {
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
const assets = await collectAssetsAsync({ inputDir: inputDir!, platforms });
await uploadAssetsAsync(assets);
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
assetSpinner.succeed('Uploaded assets!');
} catch (e) {
assetSpinner.fail('Failed to upload assets');
throw e;
}
}
if (!message && autoFlag) {
message = (await getVcsClient().getLastCommitMessageAsync())?.trim();
}
if (!message) {
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ publishMessage: message } = await promptAsync({
type: 'text',
name: 'publishMessage',
message: `Please enter a publication message.`,
initial: republish
? `Republish "${oldMessage!}" - group: ${group}`
: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
validate: value => (value ? true : validationMessage),
}));
}
const runtimeToPlatformMapping: Record<string, string[]> = {};
for (const runtime of new Set(Object.values(runtimeVersions))) {
runtimeToPlatformMapping[runtime] = Object.entries(runtimeVersions)
.filter(pair => pair[1] === runtime)
.map(pair => pair[0]);
}
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups = Object.entries(runtimeToPlatformMapping).map(([runtime, platforms]) => {
const localUpdateInfoGroup = Object.fromEntries(
platforms.map(platform => [
platform,
unsortedUpdateInfoGroups[platform as keyof UpdateInfoGroup],
])
);
if (republish && !oldRuntimeVersion) {
throw new Error(
'Can not find the runtime version of the update group that is being republished.'
);
}
return {
branchId,
updateInfoGroup: localUpdateInfoGroup,
runtimeVersion: republish ? oldRuntimeVersion : runtime,
message,
};
});
let newUpdates;
const publishSpinner = ora('Publishing...').start();
try {
newUpdates = await PublishMutation.publishUpdateGroupAsync(updateGroups);
publishSpinner.succeed('Published!');
} catch (e) {
publishSpinner.fail('Failed to published updates');
throw e;
}
if (jsonFlag) {
Log.log(JSON.stringify(newUpdates));
} else {
if (new Set(newUpdates.map(update => update.group)).size > 1) {
Log.addNewLineIfNone();
Log.log(
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
);
}
Log.addNewLineIfNone();
for (const runtime of new Set(Object.values(runtimeVersions))) {
const platforms = newUpdates
.filter(update => update.runtimeVersion === runtime)
.map(update => update.platform);
const newUpdate = newUpdates.find(update => update.runtimeVersion === runtime);
if (!newUpdate) {
throw new Error(`Publish response is missing updates with runtime ${runtime}.`);
}
Log.log(
formatFields([
{ label: 'branch', value: branchName },
{ label: 'runtime version', value: runtime },
{ label: 'platform', value: platforms.join(',') },
{ label: 'update group ID', value: newUpdate.group },
{ label: 'message', value: message },
])
);
Log.addNewLineIfNone();
}
}
}
}
async function getRuntimeVersionObjectAsync(
exp: ExpoConfig,
platformFlag: PlatformFlag,
projectDir: string
): Promise<Record<string, string>> {
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
for (const platform of platforms) {
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
if (isPolicy) {
const isManaged = (await resolveWorkflowAsync(projectDir, platform)) === Workflow.MANAGED;
if (!isManaged) {
throw new Error('Runtime version policies are only supported in the managed workflow.');
}
}
}
return Object.fromEntries(
platforms.map(platform => [platform, Updates.getRuntimeVersion(exp, platform)])
);
}
function formatUpdateTitle(
update: Exclude<
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
null | undefined
>['updates'][number]
): string {
const { message, createdAt, actor, runtimeVersion } = update;
let actorName: string;
switch (actor?.__typename) {
case 'User': {
actorName = (actor as Pick<User, 'username' | 'id'>).username;
break;
}
case 'Robot': {
const { firstName, id } = actor as Pick<Robot, 'firstName' | 'id'>;
actorName = firstName ?? `robot: ${id.slice(0, 4)}...`;
break;
}
default:
actorName = 'unknown';
}
return `[${dateFormat(
createdAt,
'mmm dd HH:MM'
)} by ${actorName}, runtimeVersion: ${runtimeVersion}] ${message}`;
}
async function getEASUpdateURLAsync(exp: ExpoConfig): Promise<string> {
const projectId = await getProjectIdAsync(exp);
return new URL(projectId, EAS_UPDATE_URL).href;
}
async function checkEASUpdateURLIsSetAsync(exp: ExpoConfig): Promise<void> {
const configuredURL = exp.updates?.url;
const expectedURL = await getEASUpdateURLAsync(exp);
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.`
);
throw new Error(BranchPublish.description);
}
}
+17 -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 gql from 'graphql-tag';
@@ -18,6 +18,7 @@ import {
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
async function renameUpdateBranchOnAppAsync({
appId,
@@ -51,19 +52,18 @@ async function renameUpdateBranchOnAppAsync({
}
export default class BranchRename extends EasCommand {
static hidden = true;
static description = 'Rename a branch.';
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 +72,10 @@ 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);
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -112,14 +115,13 @@ export default class BranchRename extends EasCommand {
});
if (jsonFlag) {
Log.log(JSON.stringify(editedBranch));
return;
printJsonOnlyOutput(editedBranch);
} else {
Log.withTick(
`️Renamed branch from ${currentName} to ${chalk.bold(
editedBranch.name
)} on project ${chalk.bold(fullName)}.`
);
}
Log.withTick(
`️Renamed branch from ${currentName} to ${chalk.bold(
editedBranch.name
)} on project ${chalk.bold(fullName)}.`
);
}
}
+40 -92
View File
@@ -1,74 +1,20 @@
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';
import EasCommand from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { ViewBranchQuery, ViewBranchQueryVariables } from '../../graphql/generated';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
import groupBy from '../../utils/expodash/groupBy';
import formatFields from '../../utils/formatFields';
const PAGE_LIMIT = 10_000;
export async function viewUpdateBranchAsync({
appId,
name,
}: Pick<ViewBranchQueryVariables, 'appId' | 'name'>): Promise<ViewBranchQuery> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<ViewBranchQuery, ViewBranchQueryVariables>(
gql`
query ViewBranch($appId: String!, $name: String!, $limit: Int!) {
app {
byId(appId: $appId) {
id
updateBranchByName(name: $name) {
id
name
updates(offset: 0, limit: $limit) {
id
group
message
createdAt
runtimeVersion
platform
manifestFragment
actor {
id
... on User {
username
}
... on Robot {
firstName
}
}
}
}
}
}
}
`,
{
appId,
name,
limit: PAGE_LIMIT,
},
{ additionalTypenames: ['UpdateBranch', 'Update'] }
)
.toPromise()
);
return data;
}
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class BranchView extends EasCommand {
static hidden = true;
static description = 'View a branch.';
static description = 'view a branch';
static args = [
{
@@ -79,7 +25,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 +35,10 @@ export default class BranchView extends EasCommand {
let {
args: { name },
flags: { json: jsonFlag },
} = this.parse(BranchView);
} = await this.parse(BranchView);
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -108,7 +57,7 @@ export default class BranchView extends EasCommand {
}));
}
const { app } = await viewUpdateBranchAsync({
const { app } = await UpdateQuery.viewBranchAsync({
appId: projectId,
name,
});
@@ -122,37 +71,36 @@ export default class BranchView extends EasCommand {
);
if (jsonFlag) {
Log.log(JSON.stringify({ ...UpdateBranch, updates }));
return;
printJsonOnlyOutput({ ...UpdateBranch, updates });
} else {
const groupTable = new Table({
head: UPDATE_COLUMNS,
wordWrap: true,
});
for (const update of updates) {
groupTable.push([
formatUpdate(update),
update.runtimeVersion,
update.group,
getPlatformsForGroup({
updates: UpdateBranch.updates,
group: update.group,
}),
]);
}
Log.addNewLineIfNone();
Log.log(chalk.bold('Branch:'));
Log.log(
formatFields([
{ label: 'Name', value: UpdateBranch.name },
{ label: 'ID', value: UpdateBranch.id },
])
);
Log.addNewLineIfNone();
Log.log(chalk.bold('Recently published update groups:'));
Log.log(groupTable.toString());
}
const groupTable = new Table({
head: UPDATE_COLUMNS,
wordWrap: true,
});
for (const update of updates) {
groupTable.push([
formatUpdate(update),
update.runtimeVersion,
update.group,
getPlatformsForGroup({
updates: UpdateBranch.updates,
group: update.group,
}),
]);
}
Log.addNewLineIfNone();
Log.log(chalk.bold('Branch:'));
Log.log(
formatFields([
{ label: 'Name', value: UpdateBranch.name },
{ label: 'ID', value: UpdateBranch.id },
])
);
Log.addNewLineIfNone();
Log.log(chalk.bold('Recently published update groups:'));
Log.log(groupTable.toString());
}
}
@@ -110,12 +110,12 @@ async function ensureBuildExistsAsync(buildId: string): Promise<void> {
}
export default class BuildCancel extends EasCommand {
static description = 'Cancel a build.';
static description = 'cancel a build';
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,19 +1,26 @@
import { flags } from '@oclif/command';
import { getConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { configureAsync } from '../../build/configure';
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from '../../build/android/UpdatesModule';
import { cleanUpOldEasBuildGradleScriptAsync } from '../../build/android/syncProjectConfiguration';
import { ensureProjectConfiguredAsync } from '../../build/configure';
import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from '../../build/ios/UpdatesModule';
import { isExpoUpdatesInstalled } from '../../build/utils/updates';
import EasCommand from '../../commandUtils/EasCommand';
import Log, { learnMore } from '../../log';
import { RequestedPlatform } from '../../platform';
import { findProjectRootAsync } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { getVcsClient } from '../../vcs';
export default class BuildConfigure extends EasCommand {
static description = 'Configure the project to support EAS Build.';
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'],
@@ -21,50 +28,65 @@ 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 projectDir = await findProjectRootAsync();
const expoUpdatesIsInstalled = isExpoUpdatesInstalled(projectDir);
const platform =
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
await configureAsync({
platform,
projectDir: await findProjectRootAsync(),
// clean up old Android configuration
if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) {
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
}
// ensure eas.json exists
Log.newLine();
await ensureProjectConfiguredAsync({
projectDir,
nonInteractive: false,
});
Log.newLine();
logSuccess(platform);
}
}
// configure expo-updates
if (expoUpdatesIsInstalled) {
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
function logSuccess(platform: RequestedPlatform): void {
let platformsText = 'iOS and Android projects are';
let storesText = 'the Apple App Store or Google Play Store';
if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
await syncAndroidUpdatesConfigurationAsync(projectDir, exp);
}
}
if (platform === 'android') {
platformsText = 'Android project is';
storesText = 'the Google Play Store';
} else if (platform === 'ios') {
platformsText = 'iOS project is';
storesText = 'the Apple App Store';
}
if ([RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
await syncIosUpdatesConfigurationAsync(projectDir, exp);
}
}
}
Log.log(`🎉 Your ${platformsText} ready to build.
Log.addNewLineIfNone();
Log.log(`🎉 Your project is ready to build.
- Run ${chalk.bold('eas build')} when you are ready to create your first build.
- Once the build is completed, run ${chalk.bold('eas submit')} to upload the app to ${storesText}
- Once the build is completed, run ${chalk.bold('eas submit')} to upload the app to app stores.
- ${learnMore('https://docs.expo.dev/build/introduction', {
learnMoreMessage: 'Learn more about other capabilities of EAS Build',
})}`);
learnMoreMessage: 'Learn more about other capabilities of EAS Build',
})}`);
}
}
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
Log.addNewLineIfNone();
const { platform } = await promptAsync({
type: 'select',
message: 'Which platforms would you like to configure for EAS Build?',
+41 -268
View File
@@ -1,49 +1,19 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, EasJsonReader, SubmitProfile } 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 path from 'path';
import { prepareAndroidBuildAsync } from '../../build/android/build';
import { BuildRequestSender, waitForBuildEndAsync } from '../../build/build';
import { ensureProjectConfiguredAsync } from '../../build/configure';
import { BuildContext } from '../../build/context';
import { createBuildContextAsync } from '../../build/createContext';
import { prepareIosBuildAsync } from '../../build/ios/build';
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from '../../build/utils/devClient';
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import { ensureRepoIsCleanAsync, reviewAndCommitChangesAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
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 { 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';
@@ -54,6 +24,7 @@ interface RawBuildFlags {
profile?: string;
'non-interactive': boolean;
local: boolean;
output?: string;
wait: boolean;
'clear-cache': boolean;
json: boolean;
@@ -61,202 +32,102 @@ interface RawBuildFlags {
'auto-submit-with-profile'?: string;
}
interface BuildFlags {
requestedPlatform: RequestedPlatform;
skipProjectConfiguration: boolean;
profile?: string;
nonInteractive: boolean;
local: boolean;
wait: boolean;
clearCache: boolean;
json: boolean;
autoSubmit: boolean;
submitProfile?: string;
}
export default class Build extends EasCommand {
static description = 'start a build';
static flags = {
platform: flags.enum({
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({
output: Flags.string({
description: 'Output path for local build',
}),
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 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 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) {
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 this.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) {
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.local && flags.output) {
Errors.error('--output is allowed only for local builds', { exit: 1 });
}
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 });
}
}
@@ -271,10 +142,17 @@ export default class Build extends EasCommand {
const profile = flags['profile'];
return {
requestedPlatform,
skipProjectConfiguration: flags['skip-project-configuration'],
profile,
nonInteractive,
local: flags['local'],
localBuildOptions: flags['local']
? {
enable: true,
verbose: true,
artifactPath: flags.output && path.resolve(process.cwd(), flags.output),
}
: {
enable: false,
},
wait: flags['wait'],
clearCache: flags['clear-cache'],
json: flags['json'],
@@ -282,111 +160,6 @@ export default class Build extends EasCommand {
submitProfile: flags['auto-submit-with-profile'] ?? profile,
};
}
private async 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,
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;
}
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,
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);
}
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(
@@ -0,0 +1,140 @@
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({
char: 'o',
description: 'Output directory.',
required: true,
helpValue: 'OUTPUT_DIRECTORY',
}),
force: Flags.boolean({
description: 'Delete OUTPUT_DIRECTORY if it already exists.',
default: false,
}),
verbose: Flags.boolean({
char: 'v',
default: false,
}),
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildInspect);
const outputDirectory = path.resolve(process.cwd(), flags.output);
const tmpWorkingdir = path.join(getTmpDirectory(), uuidv4());
if (flags.force && process.cwd().startsWith(outputDirectory)) {
throw new Error(
`This operation is not allowed, it would delete all files in ${outputDirectory} including the current project.`
);
}
await this.prepareOutputDirAsync(outputDirectory, flags.force);
if (flags.stage === InspectStage.ARCHIVE) {
const vcs = getVcsClient();
await vcs.ensureRepoExistsAsync();
await vcs.makeShallowCopyAsync(tmpWorkingdir);
await this.copyToOutputDirAsync(tmpWorkingdir, outputDirectory);
} else {
const projectDir = await findProjectRootAsync();
try {
await runBuildAndSubmitAsync(projectDir, {
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 {
await this.copyToOutputDirAsync(path.join(tmpWorkingdir, 'build'), outputDirectory);
}
}
}
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);
}
private async copyToOutputDirAsync(src: string, dst: string): Promise<void> {
const spinner = ora().start(`Copying project directory to ${dst}`);
try {
if (await fs.pathExists(src)) {
await fs.copy(src, dst);
}
await fs.remove(src);
spinner.succeed(`Project directory saved to ${dst}`);
} catch (err) {
spinner.fail();
throw err;
}
}
}
+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();
}
+31 -29
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 gql from 'graphql-tag';
@@ -18,6 +18,7 @@ import {
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { createUpdateBranchOnAppAsync } from '../branch/create';
export async function createUpdateChannelOnAppAsync({
@@ -59,8 +60,7 @@ export async function createUpdateChannelOnAppAsync({
}
export default class ChannelCreate extends EasCommand {
static hidden = true;
static description = 'Create a channel on the current project.';
static description = 'create a channel';
static args = [
{
@@ -71,7 +71,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 +82,10 @@ export default class ChannelCreate extends EasCommand {
let {
args: { name: channelName },
flags: { json: jsonFlag },
} = this.parse(ChannelCreate);
} = await this.parse(ChannelCreate);
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -134,30 +137,29 @@ export default class ChannelCreate extends EasCommand {
}
if (jsonFlag) {
Log.log(JSON.stringify(newChannel));
return;
printJsonOnlyOutput(newChannel);
} else {
Log.addNewLineIfNone();
Log.withTick(
`Created a new channel on project ${chalk.bold(await getProjectFullNameAsync(exp))}`
);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: newChannel.id },
])
);
Log.addNewLineIfNone();
Log.withTick(`${branchMessage} and have pointed the channel at it.`);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: branchId },
])
);
Log.addNewLineIfNone();
Log.log(chalk.bold('You can now update your app by publishing!'));
}
Log.addNewLineIfNone();
Log.withTick(
`Created a new channel on project ${chalk.bold(await getProjectFullNameAsync(exp))}`
);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: newChannel.id },
])
);
Log.addNewLineIfNone();
Log.withTick(`${branchMessage} and have pointed the channel at it.`);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: branchId },
])
);
Log.addNewLineIfNone();
Log.log(chalk.bold('You can now update your app by publishing!'));
}
}
@@ -0,0 +1,163 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
DeleteUpdateChannelMutation,
DeleteUpdateChannelMutationVariables,
DeleteUpdateChannelResult,
GetChannelInfoQuery,
GetChannelInfoQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class ChannelDelete extends EasCommand {
static hidden = true;
static description = 'Delete a channel';
static args = [
{
name: 'name',
required: false,
description: 'Name of the channel to delete',
},
];
static flags = {
json: Flags.boolean({
description: 'print output as a JSON object',
default: false,
}),
'non-interactive': Flags.boolean({
default: false,
description: 'Run command in non-interactive mode',
}),
};
async runAsync(): Promise<void> {
const {
args: { name: nameArg },
flags: { json: jsonFlag, 'non-interactive': nonInteractiveFlag },
} = await this.parse(ChannelDelete);
if (jsonFlag && !nonInteractiveFlag) {
throw new Error('--json is allowed only in non-interactive mode');
}
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
let name;
if (nameArg) {
name = nameArg;
} else {
if (nonInteractiveFlag) {
throw new Error('Channel name must be set when running in non-interactive mode');
}
name = (
await promptAsync({
type: 'text',
name: 'name',
message: 'Please enter the name of the channel to delete:',
validate: (value: any) => (value ? true : 'Channel name may not be empty.'),
})
).name;
}
const data = await getChannelInfoAsync({ appId: projectId, name });
const channelId = data.app?.byId.updateChannelByName?.id;
if (!channelId) {
throw new Error(`Could not find channel ${name} on ${fullName}`);
}
if (!nonInteractiveFlag) {
Log.addNewLineIfNone();
Log.warn(
`You are about to permamently delete channel: "${name}".\nThis action is irreversible.`
);
Log.newLine();
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
if (!confirmed) {
Log.error(`Canceled deletion of channel: "${name}".`);
process.exit(1);
}
}
const deletionResult = await deleteChannelOnAppAsync({
channelId,
});
if (jsonFlag) {
printJsonOnlyOutput(deletionResult);
} else {
Log.withTick(`️Deleted channel "${name}".`);
}
}
}
async function getChannelInfoAsync({
appId,
name,
}: GetChannelInfoQueryVariables): Promise<GetChannelInfoQuery> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<GetChannelInfoQuery, GetChannelInfoQueryVariables>(
gql`
query GetChannelInfo($appId: String!, $name: String!) {
app {
byId(appId: $appId) {
id
updateChannelByName(name: $name) {
id
name
}
}
}
}
`,
{
appId,
name,
},
{ additionalTypenames: ['UpdateChannel'] }
)
.toPromise()
);
return data;
}
async function deleteChannelOnAppAsync({
channelId,
}: DeleteUpdateChannelMutationVariables): Promise<DeleteUpdateChannelResult> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<DeleteUpdateChannelMutation, DeleteUpdateChannelMutationVariables>(
gql`
mutation DeleteUpdateChannel($channelId: ID!) {
updateChannel {
deleteUpdateChannel(channelId: $channelId) {
id
}
}
}
`,
{
channelId,
}
)
.toPromise()
);
return data.updateChannel.deleteUpdateChannel;
}
+19 -17
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 gql from 'graphql-tag';
@@ -20,6 +20,7 @@ import {
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
async function getChannelByNameForAppAsync({
appId,
@@ -89,14 +90,13 @@ 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 description = 'point a channel at a new branch';
static args = [
{
@@ -107,10 +107,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 +120,10 @@ export default class ChannelEdit extends EasCommand {
let {
args: { name: channelName },
flags: { branch: branchName, json: jsonFlag },
} = this.parse(ChannelEdit);
} = await this.parse(ChannelEdit);
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -170,16 +173,15 @@ export default class ChannelEdit extends EasCommand {
});
if (jsonFlag) {
Log.log(JSON.stringify(channel));
return;
printJsonOnlyOutput(channel);
} else {
Log.withTick(
chalk`Channel {bold ${channel.name}} is now set to branch {bold ${branch.name}}.\n`
);
Log.addNewLineIfNone();
Log.log(
chalk`Users with builds on channel {bold ${channel.name}} will now receive the active update on {bold ${branch.name}}.`
);
}
Log.withTick(
chalk`Channel {bold ${channel.name}} is now set to branch {bold ${branch.name}}.\n`
);
Log.addNewLineIfNone();
Log.log(
chalk`Users with builds on channel {bold ${channel.name}} will now receive the active update on {bold ${branch.name}}.`
);
}
}
+20 -18
View File
@@ -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';
@@ -11,6 +11,7 @@ import {
import Log from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { logChannelDetails } from './view';
const CHANNEL_LIMIT = 10_000;
@@ -66,11 +67,10 @@ async function getAllUpdateChannelForAppAsync({
}
export default class ChannelList extends EasCommand {
static hidden = true;
static description = 'List all channels on the current project.';
static description = 'list all channels';
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 +79,10 @@ export default class ChannelList extends EasCommand {
async runAsync(): Promise<void> {
const {
flags: { json: jsonFlag },
} = this.parse(ChannelList);
} = await this.parse(ChannelList);
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -94,19 +97,18 @@ export default class ChannelList extends EasCommand {
}
if (jsonFlag) {
Log.log(JSON.stringify(channels));
return;
}
for (const channel of channels) {
Log.addNewLineIfNone();
Log.log(
formatFields([
{ label: 'Name', value: channel.name },
{ label: 'ID', value: channel.id },
])
);
logChannelDetails(channel);
printJsonOnlyOutput(channels);
} else {
for (const channel of channels) {
Log.addNewLineIfNone();
Log.log(
formatFields([
{ label: 'Name', value: channel.name },
{ label: 'ID', value: channel.id },
])
);
logChannelDetails(channel);
}
}
}
}
@@ -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';
@@ -12,6 +12,7 @@ import {
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync, selectAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { updateChannelBranchMappingAsync } from './edit';
import { BranchMapping, getBranchMapping, getUpdateChannelByNameForAppAsync } from './view';
@@ -319,19 +320,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 +341,11 @@ 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);
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -420,9 +421,9 @@ export default class ChannelRollout extends EasCommand {
}
const { newChannelInfo, logMessage } = rolloutMutationResult;
if (jsonFlag) {
Log.log(JSON.stringify(newChannelInfo));
return;
printJsonOnlyOutput(newChannelInfo);
} else {
Log.withTick(logMessage);
}
Log.withTick(logMessage);
}
}
+23 -23
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';
import Table from 'cli-table3';
@@ -21,6 +21,7 @@ import {
getPlatformsForGroup,
} from '../../update/utils';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export type BranchMapping = {
version: number;
@@ -181,7 +182,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,8 +193,7 @@ export function logChannelDetails(channel: {
}
export default class ChannelView extends EasCommand {
static hidden = true;
static description = 'View a channel on the current project.';
static description = 'view a channel';
static args = [
{
@@ -204,7 +204,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 +214,10 @@ export default class ChannelView extends EasCommand {
let {
args: { name: channelName },
flags: { json: jsonFlag },
} = this.parse(ChannelView);
} = await this.parse(ChannelView);
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -239,26 +242,23 @@ 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) {
Log.log(JSON.stringify(channel));
return;
printJsonOnlyOutput(channel);
} else {
Log.addNewLineIfNone();
Log.log(chalk.bold('Channel:'));
Log.log(
formatFields([
{ label: 'Name', value: channel.name },
{ label: 'ID', value: channel.id },
])
);
Log.addNewLineIfNone();
Log.log(chalk`{bold Branches pointed at this channel and their most recent update group:}`);
logChannelDetails(channel);
}
Log.addNewLineIfNone();
Log.log(chalk.bold('Channel:'));
Log.log(
formatFields([
{ label: 'Name', value: channel.name },
{ label: 'ID', value: channel.id },
])
);
Log.addNewLineIfNone();
Log.log(
chalk`{bold Branches, pointed at by this channel, and their most recent update group:}`
);
logChannelDetails(channel);
}
}
+13 -7
View File
@@ -1,27 +1,30 @@
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 chalk from 'chalk';
import EasCommand from '../commandUtils/EasCommand';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log from '../log';
import { appPlatformEmojis } from '../platform';
import { getExpoConfig } from '../project/expoConfig';
import { findProjectRootAsync } from '../project/projectUtils';
import { selectAsync } from '../prompts';
import { handleDeprecatedEasJsonAsync } from './build';
export default class Config extends EasCommand {
static description = 'show the eas.json config';
static description = 'display project configuration (app.json + eas.json)';
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;
@@ -58,12 +61,15 @@ export default class Config extends EasCommand {
const profile = await reader.getBuildProfileAsync(platform, profileName);
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
Log.log(getProjectConfigDescription(projectDir));
Log.addNewLineIfNone();
Log.log(chalk.bold(getProjectConfigDescription(projectDir)));
Log.newLine();
Log.log(JSON.stringify(config, null, 2));
Log.newLine();
Log.newLine();
Log.log(`Build profile "${profileName}" from eas.json for platform ${platform}`);
const appPlatform = toAppPlatform(platform);
const platformEmoji = appPlatformEmojis[appPlatform];
Log.log(`${platformEmoji} ${chalk.bold(`Build profile "${profileName}"`)}`);
Log.newLine();
Log.log(JSON.stringify(profile, null, 2));
}
+2 -8
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';
static description = 'manage credentials';
async runAsync(): Promise<void> {
const ctx = new CredentialsContext({
projectDir: process.cwd(),
user: await ensureLoggedInAsync(),
});
await new SelectPlatform().runAsync(ctx);
await new SelectPlatform().runAsync();
}
}
@@ -0,0 +1,246 @@
import { Device, DeviceStatus } from '@expo/apple-utils';
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import assert from 'assert';
import EasCommand from '../../commandUtils/EasCommand';
import { chooseDevicesToDeleteAsync } from '../../credentials/ios/actions/DeviceUtils';
import { AppleDeviceMutation } from '../../credentials/ios/api/graphql/mutations/AppleDeviceMutation';
import {
AppleDeviceQuery,
AppleDeviceQueryResult,
AppleDevicesByTeamIdentifierQueryResult,
} from '../../credentials/ios/api/graphql/queries/AppleDeviceQuery';
import { AppleTeamQuery } from '../../credentials/ios/api/graphql/queries/AppleTeamQuery';
import { authenticateAsync, getRequestContext } from '../../credentials/ios/appstore/authenticate';
import formatDevice from '../../devices/utils/formatDevice';
import { AppleDevice, Maybe } from '../../graphql/generated';
import Log from '../../log';
import { ora } from '../../ora';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
export default class DeviceDelete extends EasCommand {
static description = 'remove a registered device from your account';
static flags = {
'apple-team-id': Flags.string(),
udid: Flags.string({ multiple: true }),
};
async runAsync(): Promise<void> {
let {
flags: { 'apple-team-id': appleTeamIdentifier, udid: udids },
} = await this.parse(DeviceDelete);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = await getProjectAccountNameAsync(exp);
if (!appleTeamIdentifier) {
appleTeamIdentifier = await this.askForAppleTeamAsync(accountName);
}
assert(appleTeamIdentifier, 'No team identifier is specified');
const appleDevicesResult = await this.getDevicesForTeamAsync(accountName, appleTeamIdentifier);
if (!appleDevicesResult) {
return;
}
const { appleTeamName, appleDevices } = appleDevicesResult;
const chosenDevices = await this.chooseDevicesToDeleteAsync(appleDevices, udids);
if (chosenDevices.length === 0) {
Log.newLine();
Log.warn('No devices were chosen to be removed.');
return;
}
this.logChosenDevices(chosenDevices, appleTeamName, appleTeamIdentifier);
const hasRemoved = await this.askAndRemoveFromExpoAsync(chosenDevices);
if (!hasRemoved) {
return;
}
await this.askAndDisableOnAppleAsync(chosenDevices, appleTeamIdentifier);
}
async askAndDisableOnAppleAsync(
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
appleTeamIdentifier: string
): Promise<void> {
Log.newLine();
const deleteOnApple = await toggleConfirmAsync({
message: 'Do you want to disable these devices on your Apple account as well?',
});
if (!deleteOnApple) {
return;
}
const ctx = await authenticateAsync({ teamId: appleTeamIdentifier });
const context = getRequestContext(ctx);
Log.addNewLineIfNone();
const removeAppleSpinner = ora('Disabling devices on Apple').start();
try {
let realDevices = await Device.getAllIOSProfileDevicesAsync(context);
realDevices = realDevices.filter(d =>
chosenDevices.map(cd => cd.identifier).includes(d.attributes.udid)
);
for (const device of realDevices) {
await device.updateAsync({ status: DeviceStatus.DISABLED });
}
removeAppleSpinner.succeed('Disabled devices on Apple');
} catch (err) {
removeAppleSpinner.fail();
throw err;
}
}
async askAndRemoveFromExpoAsync(
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[]
): Promise<boolean> {
Log.warn(
`You are about to remove the Apple device${
chosenDevices.length > 1 ? 's' : ''
} listed above from your Expo account.`
);
Log.newLine();
const confirmed = await toggleConfirmAsync({
message: 'Are you sure you wish to proceed?',
});
if (confirmed) {
const removalSpinner = ora(`Removing Apple devices on Expo`).start();
try {
for (const chosenDevice of chosenDevices) {
await AppleDeviceMutation.deleteAppleDeviceAsync(chosenDevice.id);
}
removalSpinner.succeed('Removed Apple devices from Expo');
} catch (err) {
removalSpinner.fail();
throw err;
}
}
return confirmed;
}
logChosenDevices(
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
appleTeamName: Maybe<string> | undefined,
appleTeamIdentifier: string
): void {
Log.addNewLineIfNone();
chosenDevices.forEach(device => {
Log.log(
formatDevice(device, {
appleTeamName,
appleTeamIdentifier: appleTeamIdentifier!,
})
);
Log.newLine();
});
}
async chooseDevicesToDeleteAsync(
appleDevices: AppleDeviceQueryResult[],
udids: string[]
): Promise<(AppleDevice | AppleDeviceQueryResult)[]> {
let chosenDevices: (AppleDeviceQueryResult | AppleDevice)[] = [];
Log.newLine();
if (udids) {
udids.forEach(udid => {
const foundDevice = appleDevices.find(device => device.identifier === udid);
if (foundDevice) {
chosenDevices.push(foundDevice);
} else {
Log.warn(`No device found with UDID ${udid}.`);
}
});
}
if (chosenDevices.length === 0) {
Log.addNewLineIfNone();
chosenDevices = await chooseDevicesToDeleteAsync(appleDevices);
Log.newLine();
}
return chosenDevices;
}
async getDevicesForTeamAsync(
accountName: string,
appleTeamIdentifier: string
): Promise<AppleDevicesByTeamIdentifierQueryResult | undefined> {
const devicesSpinner = ora().start('Fetching the list of devices for the team…');
try {
const result = await AppleDeviceQuery.getAllForAppleTeamAsync(
accountName,
appleTeamIdentifier
);
if (result?.appleDevices.length) {
const { appleTeamName, appleDevices } = result;
devicesSpinner.succeed(
`Found ${appleDevices.length} devices for team ${appleTeamName ?? appleTeamIdentifier}`
);
return result;
} else {
devicesSpinner.fail(`Couldn't find any devices for the team ${appleTeamIdentifier}`);
return;
}
} catch (e) {
devicesSpinner.fail(`Something went wrong and we couldn't fetch the device list`);
throw e;
}
}
async askForAppleTeamAsync(accountName: string): Promise<string | undefined> {
const teamSpinner = ora().start('Fetching the list of teams for the project…');
try {
const teams = await AppleTeamQuery.getAllForAccountAsync(accountName);
if (teams.length > 0) {
teamSpinner.succeed('Fetched the list of teams for the project');
if (teams.length === 1) {
return teams[0].appleTeamIdentifier;
}
const result = await promptAsync({
type: 'select',
name: 'appleTeamIdentifier',
message: 'What Apple Team would you like to list devices for?',
choices: teams.map(team => ({
title: team.appleTeamName
? `${team.appleTeamName} (ID: ${team.appleTeamIdentifier})`
: team.appleTeamIdentifier,
value: team.appleTeamIdentifier,
})),
});
return result.appleTeamIdentifier;
} else {
teamSpinner.fail(`Couldn't find any teams for the account ${accountName}`);
return;
}
} catch (e) {
teamSpinner.fail(`Something went wrong and we couldn't fetch the list of teams`);
throw e;
}
}
}
+4 -4
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 });
@@ -34,7 +34,7 @@ export default class BuildList extends EasCommand {
try {
const teams = await AppleTeamQuery.getAllForAccountAsync(accountName);
if (teams.length) {
if (teams.length > 0) {
spinner.succeed();
if (teams.length === 1) {
+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(
+23 -2
View File
@@ -1,11 +1,14 @@
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 {
static description = 'log environment info to the console';
static description = 'display environment info';
protected requiresAuthentication = false;
@@ -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}`);
}
}
}
@@ -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';
@@ -20,21 +20,21 @@ import { getActorDisplayName } from '../../user/User';
import { ensureLoggedInAsync } from '../../user/actions';
export default class EnvironmentSecretCreate extends EasCommand {
static description = 'Create an environment secret on the current project or owner account.';
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,
}),
@@ -44,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 });
@@ -1,6 +1,5 @@
import { getConfig } from '@expo/config';
import { flags } from '@oclif/command';
import chalk from 'chalk';
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
@@ -18,11 +17,10 @@ import {
import { promptAsync, toggleConfirmAsync } from '../../prompts';
export default class EnvironmentSecretDelete extends EasCommand {
static description = `Delete an environment secret by ID.
Unsure where to find the secret's ID? Run ${chalk.bold('eas secret:list')}`;
static description = 'delete an environment secret by ID';
static flags = {
id: flags.string({
id: Flags.string({
description: 'ID of the secret to delete',
}),
};
@@ -35,7 +33,7 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secret:list')}`;
let {
flags: { id },
} = this.parse(EnvironmentSecretDelete);
} = await this.parse(EnvironmentSecretDelete);
let secret: EnvironmentSecretWithScope | undefined;
if (!id) {
+1 -1
View File
@@ -13,7 +13,7 @@ import {
} from '../../project/projectUtils';
export default class EnvironmentSecretList extends EasCommand {
static description = 'Lists environment secrets available for your current app';
static description = 'list environment secrets available for your current app';
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
+18 -20
View File
@@ -1,12 +1,11 @@
import { getConfig } from '@expo/config';
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';
import { SubmissionFragment } from '../graphql/generated';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log, { link } from '../log';
import Log from '../log';
import {
RequestedPlatform,
appPlatformDisplayNames,
@@ -20,7 +19,7 @@ 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;
@@ -32,7 +31,7 @@ interface RawFlags {
'non-interactive': boolean;
}
interface Flags {
interface CommandFlags {
requestedPlatform: RequestedPlatform;
profile?: string;
archiveFlags: SubmitArchiveFlags;
@@ -42,52 +41,51 @@ interface Flags {
}
export default class Submit extends EasCommand {
static description = `submit build archive to app store
See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit/eas-json/')}`;
static description = 'submit app binary to app store';
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();
@@ -135,7 +133,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,
@@ -146,13 +144,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!`);
}
}
}
+11 -10
View File
@@ -1,4 +1,4 @@
import { flags } from '@oclif/command';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
@@ -10,6 +10,7 @@ import {
} from '../../graphql/generated';
import Log from '../../log';
import { confirmAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
async function deleteUpdateGroupAsync({
group,
@@ -35,8 +36,7 @@ async function deleteUpdateGroupAsync({
}
export default class UpdateDelete extends EasCommand {
static hidden = true;
static description = 'Delete all the updates in an update Group.';
static description = 'delete all the updates in an update group';
static args = [
{
@@ -47,7 +47,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,9 +57,11 @@ export default class UpdateDelete extends EasCommand {
const {
args: { groupId: group },
flags: { json: jsonFlag },
} = this.parse(UpdateDelete);
} = await this.parse(UpdateDelete);
if (!jsonFlag) {
if (jsonFlag) {
enableJsonOutput();
} else {
const shouldAbort = await confirmAsync({
message:
`🚨${chalk.red('CAUTION')}🚨\n\n` +
@@ -80,10 +82,9 @@ export default class UpdateDelete extends EasCommand {
await deleteUpdateGroupAsync({ group });
if (jsonFlag) {
Log.log(JSON.stringify({ group }));
return;
printJsonOnlyOutput({ group });
} else {
Log.withTick(`Deleted update group ${group}`);
}
Log.withTick(`Deleted update group ${group}`);
}
}
@@ -0,0 +1,519 @@
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,
ViewBranchUpdatesQuery,
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
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 { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { getVcsClient } from '../../vcs';
import { createUpdateBranchOnAppAsync } from '../branch/create';
import { listBranchesAsync } from '../branch/list';
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<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
null | undefined
>['updates'];
}> {
const { app } = await UpdateQuery.viewBranchAsync({
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);
if (jsonFlag) {
enableJsonOutput();
}
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) {
printJsonOnlyOutput(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<ViewBranchUpdatesQuery['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.`
);
}
}
@@ -0,0 +1,139 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import CliTable from 'cli-table3';
import EasCommand from '../../commandUtils/EasCommand';
import { ViewAllUpdatesQuery } from '../../graphql/generated';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { FormatUpdateParameter, UPDATE_COLUMNS, formatUpdate } from '../../update/utils';
import groupBy from '../../utils/expodash/groupBy';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { getVcsClient } from '../../vcs';
type UpdateGroupDescription = FormatUpdateParameter & {
branch: string;
group: string;
platforms: string;
runtimeVersion: string;
};
export default class BranchView extends EasCommand {
static description = 'view the recent updates for a branch';
static flags = {
branch: Flags.string({
description: 'List all updates on this branch',
exclusive: ['all'],
}),
all: Flags.boolean({
description: 'List all updates associated with this project',
exclusive: ['branch'],
default: false,
}),
json: Flags.boolean({
description: `Return a json with all of the recent update groups.`,
default: false,
}),
};
async runAsync(): Promise<void> {
const {
flags: { branch: branchFlag, all, json: jsonFlag },
} = await this.parse(BranchView);
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
let updateGroupDescriptions: UpdateGroupDescription[];
if (all) {
const branchesAndUpdates = await UpdateQuery.viewAllAsync({ appId: projectId });
updateGroupDescriptions = getUpdateGroupDescriptions(
branchesAndUpdates.app.byId.updateBranches
);
} else {
let branchInteractive: string | undefined;
if (!branchFlag) {
const validationMessage = 'Branch name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ name: branchInteractive } = await promptAsync({
type: 'text',
name: 'name',
message: 'Please enter the name of the branch whose updates you wish to view:',
initial: (await getVcsClient().getBranchNameAsync()) ?? undefined,
validate: (value: any) => (value ? true : validationMessage),
}));
}
const branch = branchFlag ?? branchInteractive;
assert(branch, 'Branch name may not be empty.');
const branchesAndUpdates = await UpdateQuery.viewBranchAsync({
appId: projectId,
name: branch,
});
const UpdateBranch = branchesAndUpdates.app?.byId.updateBranchByName;
if (!UpdateBranch) {
throw new Error(`Could not find branch "${branch}"`);
}
updateGroupDescriptions = getUpdateGroupDescriptions([UpdateBranch]);
}
if (jsonFlag) {
printJsonOnlyOutput(updateGroupDescriptions);
} else {
logAsTable(updateGroupDescriptions);
}
}
}
function getUpdateGroupDescriptions(
branchesAndUpdates: ViewAllUpdatesQuery['app']['byId']['updateBranches']
): UpdateGroupDescription[] {
const flattenedBranchesAndUpdates = branchesAndUpdates.flatMap(branch =>
branch.updates.map(update => {
return { branch: branch.name, ...update };
})
);
const updateGroupDescriptions = Object.values(
groupBy(flattenedBranchesAndUpdates, update => update.group)
).map(updateGroup => {
const platforms = updateGroup
.map(update => update.platform)
.sort()
.join(', ');
return { ...updateGroup[0], platforms };
});
updateGroupDescriptions.sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
return updateGroupDescriptions;
}
function logAsTable(updateGroupDescriptions: UpdateGroupDescription[]): void {
const table = new CliTable({
head: ['Branch', ...UPDATE_COLUMNS],
wordWrap: true,
});
table.push(
...updateGroupDescriptions.map(updateGroupDescription => [
updateGroupDescription.branch,
formatUpdate(updateGroupDescription),
updateGroupDescription.runtimeVersion,
updateGroupDescription.group,
updateGroupDescription.platforms,
])
);
Log.addNewLineIfNone();
Log.log(chalk.bold('Recently published update groups:'));
Log.log(table.toString());
}
+27 -25
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';
@@ -7,6 +7,7 @@ import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { UpdatesByGroupQuery, UpdatesByGroupQueryVariables } from '../../graphql/generated';
import Log from '../../log';
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export async function viewUpdateAsync({
groupId,
@@ -50,8 +51,7 @@ export async function viewUpdateAsync({
return data;
}
export default class UpdateView extends EasCommand {
static hidden = true;
static description = 'Update group details.';
static description = 'update group details';
static args = [
{
@@ -62,7 +62,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,31 +72,33 @@ export default class UpdateView extends EasCommand {
const {
args: { groupId },
flags: { json: jsonFlag },
} = this.parse(UpdateView);
} = await this.parse(UpdateView);
if (jsonFlag) {
enableJsonOutput();
}
const { updatesByGroup } = await viewUpdateAsync({ groupId });
if (jsonFlag) {
Log.log(JSON.stringify(updatesByGroup));
return;
printJsonOnlyOutput(updatesByGroup);
} else {
const groupTable = new Table({
head: [...UPDATE_COLUMNS],
wordWrap: true,
});
const representativeUpdate = updatesByGroup[0];
groupTable.push([
formatUpdate(representativeUpdate),
representativeUpdate.runtimeVersion,
representativeUpdate.group,
getPlatformsForGroup({
updates: updatesByGroup,
group: updatesByGroup[0]?.group,
}),
]);
Log.log(groupTable.toString());
}
const groupTable = new Table({
head: [...UPDATE_COLUMNS],
wordWrap: true,
});
const representativeUpdate = updatesByGroup[0];
groupTable.push([
formatUpdate(representativeUpdate),
representativeUpdate.runtimeVersion,
representativeUpdate.group,
getPlatformsForGroup({
updates: updatesByGroup,
group: updatesByGroup[0]?.group,
}),
]);
Log.log(groupTable.toString());
}
}
@@ -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';
@@ -9,24 +9,24 @@ import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUt
import { prepareInputParamsAsync } from '../../webhooks/input';
export default class WebhookCreate extends EasCommand {
static description = 'Create a webhook on the current project.';
static description = 'create a webhook';
static flags = {
event: flags.enum({
event: Flags.enum({
description: 'Event type that triggers the webhook',
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();
@@ -14,7 +14,7 @@ import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { formatWebhook } from '../../webhooks/formatWebhook';
export default class WebhookDelete extends EasCommand {
static description = 'Delete a webhook on the current project.';
static description = 'delete a webhook';
static args = [
{
@@ -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';
@@ -15,10 +15,10 @@ import {
import { formatWebhook } from '../../webhooks/formatWebhook';
export default class WebhookList extends EasCommand {
static description = 'List webhooks on the current project.';
static description = 'list webhooks';
static flags = {
event: flags.enum({
event: Flags.enum({
description: 'Event type that triggers the webhook',
options: [WebhookType.Build, WebhookType.Submit],
}),
@@ -27,7 +27,7 @@ export default class WebhookList extends EasCommand {
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';
@@ -9,28 +9,28 @@ import pick from '../../utils/expodash/pick';
import { prepareInputParamsAsync } from '../../webhooks/input';
export default class WebhookUpdate extends EasCommand {
static description = 'Create a webhook on the current project.';
static description = 'update a webhook';
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, 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;
@@ -5,7 +5,7 @@ import { ora } from '../../ora';
import { formatWebhook } from '../../webhooks/formatWebhook';
export default class WebhookView extends EasCommand {
static description = 'View a webhook on the current project.';
static description = 'view a webhook';
static args = [
{
@@ -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 {
+17 -5
View File
@@ -1,4 +1,5 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { Env } from '@expo/eas-build-job';
import chalk from 'chalk';
import Log from '../log';
@@ -27,6 +28,7 @@ export class CredentialsContext {
nonInteractive?: boolean;
projectDir: string;
user: Actor;
env?: Env;
}
) {
this.projectDir = options.projectDir;
@@ -35,11 +37,21 @@ export class CredentialsContext {
this.resolvedExp = options.exp;
if (!this.resolvedExp) {
try {
this.resolvedExp = getExpoConfig(options.projectDir);
} catch (error) {
// ignore error, context might be created outside of expo project
}
this.resolvedExp =
CredentialsContext.getExpoConfigInProject(this.projectDir, { env: options.env }) ??
undefined;
}
}
static getExpoConfigInProject(
projectDir: string,
{ env }: { env?: Env } = {}
): ExpoConfig | null {
try {
return getExpoConfig(projectDir, { env });
} catch (error) {
// ignore error, context might be created outside of expo project
return null;
}
}
@@ -119,7 +119,9 @@ async function promptForAscApiKeyAsync(ctx: CredentialsContext): Promise<Minimal
async function promptForKeyP8AndIdAsync(): Promise<Pick<AscApiKeyPath, 'keyP8Path' | 'keyId'>> {
Log.log(
chalk.bold('An App Store Connect Api key is required to upload your app to the Apple App Store')
chalk.bold(
'An App Store Connect Api key is required to upload your app to the Apple App Store Connect'
)
);
Log.log(
`If you're not sure what this is or how to create one, ${learnMore(
@@ -26,6 +26,23 @@ export async function chooseDevicesAsync(
return devices;
}
export async function chooseDevicesToDeleteAsync(
allDevices: AppleDeviceFragment[]
): Promise<AppleDevice[]> {
const { devices } = await promptAsync({
type: 'multiselect',
name: 'devices',
message: 'Which devices do you want to remove?',
choices: allDevices.map(device => ({
value: device,
title: formatDeviceLabel(device),
selected: false,
})),
instructions: false,
});
return devices;
}
export function formatDeviceLabel(device: AppleDeviceFragment): string {
const deviceDetails = formatDeviceDetails(device);
return `${device.name ?? device.identifier}${deviceDetails !== '' ? ` ${deviceDetails}` : ''}`;
@@ -6,6 +6,7 @@ import {
AppleDeviceFragment,
AppleDeviceInput,
CreateAppleDeviceMutation,
DeleteAppleDeviceResult,
} from '../../../../../graphql/generated';
import { AppleDeviceFragmentNode } from '../../../../../graphql/types/credentials/AppleDevice';
@@ -40,4 +41,25 @@ export const AppleDeviceMutation = {
);
return data.appleDevice.createAppleDevice;
},
async deleteAppleDeviceAsync(deviceId: string): Promise<string> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<DeleteAppleDeviceResult>(
gql`
mutation DeleteAppleDeviceMutation($deviceId: ID!) {
appleDevice {
deleteAppleDevice(id: $deviceId) {
id
}
}
}
`,
{
deviceId,
}
)
.toPromise()
);
return data.id;
},
};
@@ -1,4 +1,4 @@
import { BundleIdCapability, CapabilityType } from '@expo/apple-utils';
import { BundleIdCapability } from '@expo/apple-utils';
import {
CapabilityMapping,
@@ -222,10 +222,6 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
const bundleId = {
getBundleIdCapabilitiesAsync: jest.fn(() => [
new BundleIdCapability({}, 'XXX_IN_APP_PURCHASE', { settings: null }),
new BundleIdCapability({}, 'FOOBAR', {
settings: null,
capabilityType: CapabilityType.PUSH_NOTIFICATIONS,
}),
new BundleIdCapability({}, 'XXX_GAME_CENTER', { settings: null }),
]),
updateBundleIdCapabilityAsync: jest.fn(),
@@ -214,11 +214,9 @@ function getCapabilitiesToDisable(
// might be enabling capabilities in modifiers.
for (const existingCapability of currentCapabilities) {
// Special case APNS because it's always enabled in Expo,
// GC and IAP are always enabled in apps by default so we should avoid modifying them.
if (
existingCapability.isType(CapabilityType.IN_APP_PURCHASE) ||
existingCapability.isType(CapabilityType.PUSH_NOTIFICATIONS) ||
existingCapability.isType(CapabilityType.GAME_CENTER)
) {
continue;
@@ -67,7 +67,11 @@ export async function ensureBundleIdExistsWithNameAsync(
spinner.fail(`Failed to register bundle identifier ${chalk.dim(bundleIdentifier)}`);
// Assert contract errors for easier resolution when the user has an expired developer account.
if (err.message.match(/forbidden for security reasons/)) {
if (
err.message.match(/forbidden for security reasons/) ||
// Unable to process request - PLA Update available - You currently don't have access to this membership resource. To resolve this issue, agree to the latest Program License Agreement in your developer account.
err.message.match(/agree/)
) {
await assertContractMessagesAsync(context);
}
}
@@ -162,11 +166,13 @@ export async function ensureAppExistsAsync(
}
): Promise<App> {
const context = getRequestContext(authCtx);
const spinner = ora(`Linking to App Store ${chalk.dim(bundleIdentifier)}`).start();
const spinner = ora(`Linking to App Store Connect ${chalk.dim(bundleIdentifier)}`).start();
let app = await App.findAsync(context, { bundleId: bundleIdentifier });
if (!app) {
spinner.text = `Creating App Store app ${chalk.bold(name)} ${chalk.dim(bundleIdentifier)}`;
spinner.text = `Creating App Store Connect app ${chalk.bold(name)} ${chalk.dim(
bundleIdentifier
)}`;
try {
// Assert contract errors when the user needs to create an app.
await assertContractMessagesAsync(context, spinner);
@@ -194,6 +200,8 @@ export async function ensureAppExistsAsync(
} else {
// TODO: Update app name when API gives us that possibility
}
spinner.succeed(`Prepared App Store for ${chalk.bold(name)} ${chalk.dim(bundleIdentifier)}`);
spinner.succeed(
`Prepared App Store Connect for ${chalk.bold(name)} ${chalk.dim(bundleIdentifier)}`
);
return app;
}
@@ -10,7 +10,7 @@ import {
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { findAccountByName } from '../../user/Account';
import { ensureActorHasUsername } from '../../user/actions';
import { ensureActorHasUsername, ensureLoggedInAsync } from '../../user/actions';
import { AssignFcm } from '../android/actions/AssignFcm';
import { AssignGoogleServiceAccountKey } from '../android/actions/AssignGoogleServiceAccountKey';
import {
@@ -51,12 +51,18 @@ import {
import { SelectBuildProfileFromEasJson } from './SelectBuildProfileFromEasJson';
export class ManageAndroid {
constructor(private callingAction: Action) {}
constructor(private callingAction: Action, private projectDir: string) {}
async runAsync(
ctx: CredentialsContext,
currentActions: ActionInfo[] = highLevelActions
): Promise<void> {
async runAsync(currentActions: ActionInfo[] = highLevelActions): Promise<void> {
const hasProjectContext = !!CredentialsContext.getExpoConfigInProject(this.projectDir);
const buildProfile = hasProjectContext
? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.ANDROID).runAsync()
: null;
const ctx = new CredentialsContext({
projectDir: process.cwd(),
user: await ensureLoggedInAsync(),
env: buildProfile?.env,
});
const accountName = ctx.hasProjectContext
? getProjectAccountName(ctx.exp, ctx.user)
: ensureActorHasUsername(ctx.user);
@@ -64,7 +70,12 @@ export class ManageAndroid {
if (!account) {
throw new Error(`You do not have access to account: ${accountName}`);
}
const { gradleContext } = await this.createProjectContextAsync(ctx);
let gradleContext;
if (ctx.hasProjectContext) {
assert(buildProfile, 'buildProfile must be defined in a project context');
gradleContext = await this.createProjectContextAsync(ctx, buildProfile);
}
while (true) {
try {
@@ -140,30 +151,18 @@ export class ManageAndroid {
}
}
private async createProjectContextAsync(ctx: CredentialsContext): Promise<{
gradleContext?: GradleBuildContext;
buildProfile?: BuildProfile<Platform.ANDROID>;
}> {
if (!ctx.hasProjectContext) {
return {};
}
private async createProjectContextAsync(
ctx: CredentialsContext,
buildProfile: BuildProfile<Platform.ANDROID>
): Promise<GradleBuildContext | undefined> {
assert(ctx.hasProjectContext, 'createProjectContextAsync: must have project context.');
const maybeProjectId = await promptToCreateProjectIfNotExistsAsync(ctx.exp);
if (!maybeProjectId) {
throw new Error(
'Your project must be registered with EAS in order to use the credentials manager.'
);
}
const buildProfile = await new SelectBuildProfileFromEasJson(
ctx.projectDir,
Platform.ANDROID
).runAsync(ctx);
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
return {
gradleContext,
buildProfile,
};
return await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
}
private async runProjectSpecificActionAsync(
@@ -17,7 +17,7 @@ import {
} from '../../project/projectUtils';
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
import { Account, findAccountByName } from '../../user/Account';
import { ensureActorHasUsername } from '../../user/actions';
import { ensureActorHasUsername, ensureLoggedInAsync } from '../../user/actions';
import { CredentialsContext } from '../context';
import {
AppStoreApiKeyPurpose,
@@ -59,12 +59,18 @@ import { SelectBuildProfileFromEasJson } from './SelectBuildProfileFromEasJson';
import { SelectIosDistributionTypeGraphqlFromBuildProfile } from './SelectIosDistributionTypeGraphqlFromBuildProfile';
export class ManageIos {
constructor(private callingAction: Action) {}
constructor(private callingAction: Action, private projectDir: string) {}
async runAsync(
ctx: CredentialsContext,
currentActions: ActionInfo[] = highLevelActions
): Promise<void> {
async runAsync(currentActions: ActionInfo[] = highLevelActions): Promise<void> {
const hasProjectContext = !!CredentialsContext.getExpoConfigInProject(this.projectDir);
const buildProfile = hasProjectContext
? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.IOS).runAsync()
: null;
const ctx = new CredentialsContext({
projectDir: process.cwd(),
user: await ensureLoggedInAsync(),
env: buildProfile?.env,
});
const buildCredentialsActions = getBuildCredentialsActions(ctx);
const pushKeyActions = getPushKeyActions(ctx);
const ascApiKeyActions = getAscApiKeyActions(ctx);
@@ -78,7 +84,16 @@ export class ManageIos {
if (!account) {
throw new Error(`You do not have access to account: ${accountName}`);
}
const { app, targets, buildProfile } = await this.createProjectContextAsync(ctx, account);
let app = null;
let targets = null;
if (ctx.hasProjectContext) {
assert(buildProfile, 'buildProfile must be defined in project context');
const projectContext = await this.createProjectContextAsync(ctx, account, buildProfile);
app = projectContext.app;
targets = projectContext.targets;
}
while (true) {
try {
if (ctx.hasProjectContext) {
@@ -154,20 +169,13 @@ export class ManageIos {
private async createProjectContextAsync(
ctx: CredentialsContext,
account: Account
account: Account,
buildProfile: BuildProfile<Platform.IOS>
): Promise<{
app: App | null;
targets: Target[] | null;
buildProfile: BuildProfile<Platform.IOS> | null;
app: App;
targets: Target[];
}> {
if (!ctx.hasProjectContext) {
return {
app: null,
targets: null,
buildProfile: null,
};
}
assert(ctx.hasProjectContext, 'createProjectContextAsync: must have project context.');
const maybeProjectId = await promptToCreateProjectIfNotExistsAsync(ctx.exp);
if (!maybeProjectId) {
throw new Error(
@@ -176,10 +184,6 @@ export class ManageIos {
}
const app = { account, projectName: ctx.exp.slug };
const buildProfile = await new SelectBuildProfileFromEasJson(
ctx.projectDir,
Platform.IOS
).runAsync(ctx);
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
projectDir: ctx.projectDir,
@@ -195,7 +199,6 @@ export class ManageIos {
return {
app,
targets,
buildProfile,
};
}
@@ -3,7 +3,6 @@ import { BuildProfile, EasJsonReader } from '@expo/eas-json';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { CredentialsContext } from '../context';
export class SelectBuildProfileFromEasJson<T extends Platform> {
private easJsonReader: EasJsonReader;
@@ -12,14 +11,14 @@ export class SelectBuildProfileFromEasJson<T extends Platform> {
this.easJsonReader = new EasJsonReader(projectDir);
}
async runAsync(ctx: CredentialsContext): Promise<BuildProfile<T>> {
const profileName = await this.getProfileNameFromEasConfigAsync(ctx);
async runAsync(): Promise<BuildProfile<T>> {
const profileName = await this.getProfileNameFromEasConfigAsync();
const easConfig = await this.easJsonReader.getBuildProfileAsync<T>(this.platform, profileName);
Log.succeed(`Using build profile: ${profileName}`);
return easConfig;
}
async getProfileNameFromEasConfigAsync(ctx: CredentialsContext): Promise<string> {
async getProfileNameFromEasConfigAsync(): Promise<string> {
const buildProfileNames = await this.easJsonReader.getBuildProfileNamesAsync();
if (buildProfileNames.length === 0) {
throw new Error(
@@ -28,11 +27,6 @@ export class SelectBuildProfileFromEasJson<T extends Platform> {
} else if (buildProfileNames.length === 1) {
return buildProfileNames[0];
}
if (ctx.nonInteractive) {
throw new Error(
'You have multiple build profiles. Please run this command in interactive mode.'
);
}
const { profileName } = await promptAsync({
type: 'select',
name: 'profileName',
@@ -1,10 +1,9 @@
import { promptAsync } from '../../prompts';
import { CredentialsContext } from '../context';
import { ManageAndroid } from './ManageAndroid';
import { ManageIos } from './ManageIos';
export class SelectPlatform {
async runAsync(ctx: CredentialsContext): Promise<void> {
async runAsync(): Promise<void> {
const { platform } = await promptAsync({
type: 'select',
name: 'platform',
@@ -16,8 +15,8 @@ export class SelectPlatform {
});
if (platform === 'ios') {
return await new ManageIos(new SelectPlatform()).runAsync(ctx);
return await new ManageIos(new SelectPlatform(), process.cwd()).runAsync();
}
return await new ManageAndroid(new SelectPlatform()).runAsync(ctx);
return await new ManageAndroid(new SelectPlatform(), process.cwd()).runAsync();
}
}
+3039 -4312
View File
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,102 @@
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import {
ViewAllUpdatesQuery,
ViewAllUpdatesQueryVariables,
ViewBranchUpdatesQuery,
ViewBranchUpdatesQueryVariables,
} from '../generated';
const PAGE_LIMIT = 10_000;
export const UpdateQuery = {
async viewAllAsync({ appId }: { appId: string }): Promise<ViewAllUpdatesQuery> {
return withErrorHandlingAsync(
graphqlClient
.query<ViewAllUpdatesQuery, ViewAllUpdatesQueryVariables>(
gql`
query ViewAllUpdates($appId: String!, $limit: Int!) {
app {
byId(appId: $appId) {
id
updateBranches(offset: 0, limit: $limit) {
id
name
updates(offset: 0, limit: $limit) {
id
group
message
createdAt
runtimeVersion
platform
actor {
id
... on User {
username
}
... on Robot {
firstName
}
}
}
}
}
}
}
`,
{
appId,
limit: PAGE_LIMIT,
},
{ additionalTypenames: ['UpdateBranch', 'Update'] }
)
.toPromise()
);
},
async viewBranchAsync({ appId, name }: Pick<ViewBranchUpdatesQueryVariables, 'appId' | 'name'>) {
return withErrorHandlingAsync<ViewBranchUpdatesQuery>(
graphqlClient
.query<ViewBranchUpdatesQuery, ViewBranchUpdatesQueryVariables>(
gql`
query ViewBranchUpdates($appId: String!, $name: String!, $limit: Int!) {
app {
byId(appId: $appId) {
id
updateBranchByName(name: $name) {
id
name
updates(offset: 0, limit: $limit) {
id
group
message
createdAt
runtimeVersion
platform
manifestFragment
actor {
id
... on User {
username
}
... on Robot {
firstName
}
}
}
}
}
}
}
`,
{
appId,
name,
limit: PAGE_LIMIT,
},
{ additionalTypenames: ['UpdateBranch', 'Update'] }
)
.toPromise()
);
},
};
@@ -8,6 +8,7 @@ export const SubmissionFragmentNode = gql`
app {
id
name
slug
ownerAccount {
id
name
+1 -1
View File
@@ -1 +1 @@
export { run } from '@oclif/command';
export { run } from '@oclif/core';
@@ -3,7 +3,7 @@ import mockdate from 'mockdate';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { defaultPublishPlatforms } from '../../commands/branch/publish';
import { defaultPublishPlatforms } from '../../commands/update';
import { AssetMetadataStatus } from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { PublishQuery } from '../../graphql/queries/PublishQuery';
+35
View File
@@ -0,0 +1,35 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { Errors } from '@oclif/core';
import assert from 'assert';
import semver from 'semver';
import { BuildContext } from '../build/context';
import Log from '../log';
import { confirmAsync } from '../prompts';
const SUPPORTED_EXPO_SDK_VERSIONS = '>= 41.0.0';
assert(semver.validRange(SUPPORTED_EXPO_SDK_VERSIONS), 'Must be a valid version range');
export async function checkExpoSdkIsSupportedAsync(ctx: BuildContext<Platform>): Promise<void> {
assert(ctx.workflow === Workflow.MANAGED, 'Must be a managed workflow project');
if (ctx.exp.sdkVersion && semver.satisfies(ctx.exp.sdkVersion, SUPPORTED_EXPO_SDK_VERSIONS)) {
return;
}
const unsupportedSdkMessage =
'EAS Build does not officially support building managed project with Expo SDK < 41.';
if (ctx.nonInteractive) {
Log.warn(
`${unsupportedSdkMessage} Proceeding because you are running in non-interactive mode.`
);
return;
}
const proceed = await confirmAsync({
message: `${unsupportedSdkMessage} Do you want to proceed?`,
});
if (!proceed) {
Errors.exit(1);
}
}
+46 -6
View File
@@ -24,17 +24,57 @@ export async function resolveTargetsAsync(
buildConfiguration,
});
if (applicationTarget.dependencies && applicationTarget.dependencies.length > 0) {
for (const dependency of applicationTarget.dependencies) {
const dependencies = await resolveDependenciesAsync({
exp,
projectDir,
buildConfiguration,
target: applicationTarget,
bundleIdentifier,
});
if (dependencies.length > 0) {
result.push(...dependencies);
}
return result;
}
async function resolveDependenciesAsync({
exp,
projectDir,
buildConfiguration,
target,
bundleIdentifier,
}: {
exp: ExpoConfig;
projectDir: string;
buildConfiguration?: string;
target: IOSConfig.Target.Target;
bundleIdentifier: string;
}): Promise<Target[]> {
const result: Target[] = [];
if (target.dependencies && target.dependencies.length > 0) {
for (const dependency of target.dependencies) {
const dependencyBundleIdentifier = await getBundleIdentifierAsync(projectDir, exp, {
targetName: dependency.name,
buildConfiguration,
});
result.push({
targetName: dependency.name,
buildConfiguration,
bundleIdentifier: await getBundleIdentifierAsync(projectDir, exp, {
targetName: dependency.name,
buildConfiguration,
}),
bundleIdentifier: dependencyBundleIdentifier,
parentBundleIdentifier: bundleIdentifier,
});
const dependencyDependencies = await resolveDependenciesAsync({
exp,
projectDir,
buildConfiguration,
target: dependency,
bundleIdentifier: dependencyBundleIdentifier,
});
if (dependencyDependencies.length > 0) {
result.push(...dependencyDependencies);
}
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { exit } from '@oclif/errors';
import { Errors } from '@oclif/core';
import chalk from 'chalk';
import type MetroConfig from 'metro-config';
import resolveFrom from 'resolve-from';
@@ -49,7 +49,7 @@ export async function validateMetroConfigForManagedWorkflowAsync(
});
if (shouldAbort) {
Log.error('Aborting...');
exit(1);
Errors.exit(1);
}
}
}
Loaded 100 of 125 files, more files were not shown because too many files have changed in this diff. Show more