Compare commits

...
91 Commits
Author SHA1 Message Date
Kirby Cofino f5191b2da6 [eas-cli] Add pagination to supported GQL queries 2022-07-05 18:39:13 -07:00
Cedric van Putten d0b9af2553 [eas-cli] Add review details to metadata store configuration (#1184)
* [eas-cli] Tweak the store configuration validationf or review

* [eas-cli] Implement review details in metadata

* [eas-cli] Use valid fixtures in config tests

* [eas-cli] Add markdown description for review notes

* [eas-cli] Add app review detail task description

* update CHANGELOG.md
2022-06-29 06:17:10 -07:00
Cedric van Putten 80674a3451 [eas-cli] Improve store configuration defaults and schema configuration (#1183)
* [eas-cli] Avoid setting automaticRelease for manual releases in config

* [eas-cli] Use same default app name as schema

* [eas-cli] Prefer releaseNotes over whatsNew in apple info

* [eas-cli] Drop deprecated gamblingAndContest from schema

* [eas-cli] Use better defaults for advisory

* [eas-cli] Remove type from enum as its not required by spec

* [eas-cli] Remove $ref and description workaround for draft 6

* [eas-cli] Update kids age rating description

* [eas-cli] Remove default app title

* update CHANGELOG.md
2022-06-28 05:24:32 -07:00
kirb 41b48a80be [eas-cli] truncates long messages for eas update command (#1178) 2022-06-24 15:27:52 -07:00
Dominik Sokal 3d5b77a263 [ENG-5262] fix URLs returned from AWS S3 (#1179)
* fix URLs returned from AWS S3

* update CHANGELOG.md
2022-06-24 11:17:49 +02:00
Chris WalterandDominik Sokal fa25d259d9 [1/1] Support resource-class flag in eas-cli (#1138)
* [1/1] Support build-resource-class flag in eas-cli

# Why
We want to add a flag to allow users to specify resource-class when they
run `eas build`. The flag is experimental for now, and we haven't
decided exactly how we'll incorporate the resource classes into plan
pricing. But this PR doesn't deal with that - it just makes the
necessary changes to eas-cli to support specifying a resource class when
running `eas build`.

# How
Add a new enum for BuildResourceClass that matches the enum created
for GraphQL in universe/server/www. Then create a new flag
`build-resource-class` whose options are the values of the
BuildResourceClass enum. Then add some validation to prevent resource
classes being used for incompatible platforms (e.g. prevent `-p ios
--build-resource-class=ANDROID_LARGE`). Finally, pass along the flag
value to the call to the WWW graphql mutations for BuildAndroid and
BuildiOS.

# Test Plan
```
yarn test
```
See test plan on this PR for more thorough e2e testing summary:
https://github.com/expo/universe/pull/9800

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

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

* Apply suggestions from code review

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

* Address PR comments

* Regenerate graphql from staging

* update CHANGELOG.md

Co-authored-by: Dominik Sokal <[email protected]>
2022-06-23 20:11:20 -07:00
Cedric van PuttenandEvan Bacon 26596f32e9 [eas-cli] Allow users to skip metadata validation (#1175)
* [eas-cli] Allow users to skip validation when schema has an issue

* [eas-cli] Make sure instructions are visible

* update CHANGELOG.md

* [eas-cli] Remove instructions from confirm async

* [eas-cli] Fix typo in confirm question

* [eas-cli] Reword warning about ignoring store config issues

Co-authored-by: Evan Bacon <[email protected]>

* [eas-cli] Reword confirmation question of ignoring issues

Co-authored-by: Evan Bacon <[email protected]>

Co-authored-by: Evan Bacon <[email protected]>
2022-06-23 05:13:39 -07:00
Cedric van Putten 031c610d58 [eas-cli] Update validation rules for metadata info locales (#1174)
* [eas-cli] Update validation rules for metadata info locales

* Add missing codeowner for metadata schema

* update CHANGELOG.md

* Fix path in codeowners
2022-06-23 04:43:28 -07:00
Dominik Sokal fcb5beccb4 update CHANGELOG.md 2022-06-23 09:16:57 +02:00
kirb 3003a712f6 [eas-cli] include bundler output in terminal for eas update (#1171) 2022-06-22 10:46:31 -07:00
Cedric van Putten e89f030c29 fix CHANGELOG 2022-06-17 16:02:19 +02:00
Expo CI 3e224f9583 update CHANGELOG.md 2022-06-15 21:56:43 +00:00
Cedric van Putten db338b2b78 v0.54.1 2022-06-15 23:42:44 +02:00
Cedric van Putten 971c8e4010 [eas-cli] Add link to ASC after successful metadata sync (#1168)
* [eas-cli] Add link to ASC after successful metadata sync

* update CHANGELOG.md
2022-06-15 23:26:36 +02:00
Cedric van Putten df64a59354 [eas-cli] Surface metadata cause errors in logging output (#1167)
* [eas-cli] Surface metadata cause errors in logging output

* update CHANGELOG.md
2022-06-15 23:26:16 +02:00
Cedric van Putten e5d80fcb6d [eas-cli] Add schemas to the bundled files (#1166)
* [eas-cli] Add schemas to the bundled files

* update CHANGELOG.md
2022-06-15 22:05:05 +02:00
Expo CI 4088ea3e43 update CHANGELOG.md 2022-06-15 17:01:30 +00:00
Kirby Cofino edd42186d9 v0.54.0 2022-06-15 09:53:45 -07:00
Wojciech Kozyra e6ba63cfd0 [eas-cli] Fix regression for default submit profile (#1163)
* [eas-cli] Fix regression for default submit profile

* Update CHANGELOG
2022-06-15 13:02:24 +02:00
Wojciech Kozyra 7b60901bb1 [eas-cli] update @expo/eas-build-job (#1162)
* update eas-build-job

* update CHANGELOG.md
2022-06-15 12:01:15 +02:00
Wojciech Kozyra a551e9010a add default codeowners (#1161) 2022-06-15 10:54:02 +02:00
Wojciech KozyraandDominik Sokal 95441798ed Update issue template (expo doctor output) (#1160)
* Update issue template (expo doctor output)

* Update .github/ISSUE_TEMPLATE/bug_report.yml

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

Co-authored-by: Dominik Sokal <[email protected]>
2022-06-15 10:49:08 +02:00
Wojciech Kozyra 814f863a63 fix build error 2022-06-15 10:44:22 +02:00
Wojciech Kozyra 283f086b25 [eas-cli] Remove fallback for legacy format in eas.json (#1158)
* remove support for deprecated eas.json formats

* update CHANGELOG.md
2022-06-15 10:39:48 +02:00
Dominik Sokal 7da01333c6 Update CHANGELOG.md 2022-06-15 09:30:07 +02:00
Cedric van PuttenandWojciech Kozyra c057eaa098 [eas-cli] Add metadata commands (#1136)
* [eas-cli] Add metadata commands

* Add myself as codeowner to the metadata

* [eas-cli] Hide the metadata commands for now

* Add metadata changelog entry

* [eas-cli] Update metadata configure command description

* [eas-cli] Drop non-interactive due to limitations in AppStoreApi

* [eas-cli] Reimplement credentials context for metadata

* [eas-cli] Drop non-interactive flag in metadata command

* [eas-cli] Drop args from metadata:configure

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

* [eas-cli] Add retroactive code review changes (#1147)

* [eas-cli] Add public access modifiers to classes

* [eas-cli] Add specific types to date precision remover

* [eas-cli] Add try catch around writing json to always unsubscribe

* [eas-cli] Add async return await to uploadAsync log methods

* [eas-cli] Fix date precision type and test

* [eas-cli] Use submission profile bundle id if defined

* [eas-cli] Drop catch from IO try catch block

* [eas-cli] Move unique helper to global utils

* [eas-cli] Prefer nullish coalescing

* [eas-cli] Move metadata commands to pull and push

* [eas-cli] Drop the platform option from the push command

* [eas-cli] Remove alias and reword descriptions

* [eas-cli] Auto-configure eas.json with metadata when missing (#1156)

* [eas-cli] Polish the command output for release (#1159)

* [eas-cli] Finalize metadata logging output

* [eas-cli] Fix logging validation errors when store.config.json is not found

* [eas-cli] Make all localized entites bold

* [eas-cli] Make version string visible in logs

* [eas-cli] Add note about early beta

* [eas-cli] Remove hidden from meta commands

* [eas-cli] Add metadata topic

* [eas-cli] Rename eas metadata to eas metadata:push in logging

* [eas-cli] Remove unused arg flag from push

Co-authored-by: Wojciech Kozyra <[email protected]>
2022-06-14 17:46:59 +02:00
Will Schurman 968fdd221c [eas-cli] Limit concurrent asset uploads (#1153) 2022-06-13 13:27:51 -07:00
Dominik Sokal 6d28e0eecc Update CHANGELOG.md 2022-06-13 08:50:03 +02:00
kirb a2b4d2dbd2 [eas-cli] add fetching additional pages for update --republish (#1148)
- add update/getUpdatesToRepublishInteractiveAsync and tests
2022-06-10 19:05:26 -07:00
kirb 660c996602 [eas-cli] improve update terminal output (#1152)
- add additional information to publish command output such as update ids and website links
- https://linear.app/expo/issue/ENG-4589/improve-publish-command
- add additional context to uploadAssetsAsync return type
- add real-time text to upload spinner showing upload progress
- remove ) from website link on unsupported terminals
2022-06-10 18:06:50 -07:00
kirb 35ede265ed [eas-cli] make viewBranchAsync updates limit configurable (#1144)
* make viewBranchAsync limit configurable
* add tests for updates/ensureChannelExistsAsync
* rename PAGE_LIMIT to viewBranchUpdatesQueryUpdateLimit
* generate graphql
2022-06-08 14:06:45 -07:00
Dominik Sokal cf02f075d7 Update CHANGELOG.md 2022-06-08 12:05:02 +02:00
Dominik Sokal a2027eabd2 fix CHANGELOG 2022-06-08 12:02:29 +02:00
Expo CI 4ef239c59c update CHANGELOG.md 2022-06-07 19:11:58 +00:00
Kirby Cofino 3610eee2b5 v0.53.1 2022-06-07 12:00:02 -07:00
Kirby Cofino 54ae94f532 upgrade eas-cli-local-build-plugin to 0.0.85 2022-06-07 11:59:37 -07:00
kirb c68bf3e26f [eas-cli] lower page limit for UpdateQueries (#1119)
* [eas-cli] lower page limit for UpdateQueries

- lower page limit from 10_000 to 300 to avoid timeouts on branches with many updates.
  we do not fetch the full history of updates anymore for `--republish`, etc.
2022-06-07 11:41:33 -07:00
Cedric van Putten 940e3f6d40 [eas-cli] Add metadata root actions and error handling (#1135)
* [eas-cli] Add metadata root actions and error handling

* [eas-cli] Update download and upload metadata methods to new ctx
2022-06-04 16:22:26 +02:00
Cedric van PuttenandEvan Bacon 4c6226b90b [eas-cli] Add metadata app store task runner (#1133)
* [eas-cli] Add metadata app store task runner

* [eas-cli] Use expect error over ignore

Co-authored-by: Evan Bacon <[email protected]>

* [eas-cli] Remove unused ts directive

* [eas-cli] Add download task tests

Co-authored-by: Evan Bacon <[email protected]>
2022-06-02 17:04:59 -07:00
Cedric van Putten 1341bb18d1 [eas-cli] Add metadata telemetry increase visibility on potential issues (#1132) 2022-06-02 13:59:34 -07:00
Cedric van Putten beac65ce53 [eas-cli] Add metadata config validation using json schema (#1131)
* [eas-cli] Add metadata config validation using json schema

* [eas-cli] Fix typo in privacy choices validation

* [eas-cli] Fix typo in fixture test
2022-06-02 13:58:51 -07:00
Cedric van Putten a8cc54d409 [eas-cli] Add metadata context based on submission contexts (#1134)
* [eas-cli] Add metadata context based on submission contexts

* [eas-json] Add submission meta property to config

* [eas-cli] Make exp optional in the metdata context

* [eas-cli] Fix misleading docblock on metadata context

* [eas-cli] Fix pass through exp config in metadata context

* [eas-cli] Import ios submission profile type through entrypoint

* [eas-cli] Rename metadata file to filename in context

* [eas-cli] Move credential context outside metadata context

* [eas-cli] Rename meta to metadataPath in profile config

* [eas-cli] Drop nonInteractive from metadata because of AppStoreApi limitations

* [eas-cli] Use singular store in bundle identifier doc
2022-06-02 17:06:04 +02:00
Cedric van PuttenandEvan Bacon 6a639bd556 [eas-cli] Add metadata store config serializers (#1130)
* [eas-cli] Add metadata store config serializers

* [eas-cli] Add version reader doc to avoid ambiguity

Co-authored-by: Evan Bacon <[email protected]>

* [eas-cli] Add tests for date precision utility

Co-authored-by: Evan Bacon <[email protected]>
2022-06-02 05:31:56 -07:00
Dominik Sokal dde9685b38 add common error code - fastlane timeout (#1129)
* add common error code - fastlane timeout

* address PR feedback

* update CHANGELOG.md
2022-06-01 11:55:53 +02:00
Dominik Sokal 5b3a76a5b5 [ENG-5022] print learn more link before uploading archive to S3 (#1127)
* print learn more link before uploading archive to S3

* update CHANGELOG.md
2022-05-31 09:54:25 +02:00
Dominik Sokal 513e09157b Update CHANGELOG.md 2022-05-30 12:28:49 +02:00
Expo CI 69a939c635 update CHANGELOG.md 2022-05-30 10:22:18 +00:00
Dominik Sokal 58156545d0 v0.53.0 2022-05-30 12:14:58 +02:00
Dominik Sokal 8a52b5c401 upgrade eas-cli-local-build-plugin to 0.0.82 2022-05-30 12:14:52 +02:00
Dominik Sokal 14d4c7b234 use single version of @oclif/core (#1115)
* upgrade @oclif/core

* Use forked @oclif/plugin-autocomplete and @oclif/plugin-help

* upgrade @expo/plugin-warn-if-update-available

* update CHANGELOG
2022-05-30 12:09:32 +02:00
Wojciech Kozyra fd17c96b3b [eas-cli] use getExpoConfig wrapper (#1122)
* [eas-cli] use getConfig wrapper

* update CHANGELOG.md
2022-05-26 11:05:09 +02:00
Evan Bacon 833a5465c5 fix: match capability types more accurately (#1112)
* fix: match capability types more accurately and prevent disabling unrelated types

* update CHANGELOG.md
2022-05-23 11:20:54 -05:00
Dominik Sokal f0c958e58b Drop support for node <14 (#1098)
* drop support for node 12 and 13

* update volta pinned node and yarn versions

* update node version in CI

* upgrade memfs to latest
2022-05-19 10:21:09 +02:00
Dominik Sokal 607838c8e1 [ENG-4968] better spinner placement for multiple builds (#1105)
* better spinner placement for multiple builds

* update CHANGELOG.md
2022-05-17 09:10:00 +02:00
Dominik Sokal 34ecd38304 Update dependencies (#1095)
* update outdated deps

* [eas-cli] regenerate GraphQL code

* [eas-cli] fix tests

* [scripts] upgrade deps

* [eas-cli] fix lint warnings

* update CHANGELOG.md
2022-05-11 11:20:22 +02:00
Will Schurman c4adf7056e [eas-cli] Replace asMock with jest.mocked (#1089) 2022-05-02 12:56:16 -07:00
Expo CI 0e7f326de2 update CHANGELOG.md 2022-04-26 13:10:13 +00:00
Wojciech Kozyra bbc8b1ac76 v0.52.0 2022-04-26 14:49:58 +02:00
Wojciech Kozyra 3db2a69183 upgrade eas-cli-local-build-plugin to 0.0.78 2022-04-26 14:49:48 +02:00
Wojciech Kozyra 51155bb2b4 [eas-cli] update eas-build-job (#1084) 2022-04-26 14:11:52 +02:00
Wojciech Kozyra 30ec5f967b [eas-cli] Support target specific entitlements (#1078)
* [eas-cli] Support target specific entitlements

* reiview feedback

* fix tests

* review feedback

* update CHANGELOG.md
2022-04-26 14:11:30 +02:00
Expo CI 89dda20149 update CHANGELOG.md 2022-04-19 10:40:33 +00:00
Dominik Sokal b591698c31 v0.51.0 2022-04-19 12:32:52 +02:00
Cedric van Putten 66543b658b [eas-cli] Enable update bundle output when debugging (#1070)
* [eas-cli] Enable update bundle output when debugging

* update CHANGELOG.md
2022-04-19 03:15:36 -07:00
Evan Bacon 4769048578 Bump @expo/[email protected] (#1064)
* Bump @expo/[email protected]

* Update CHANGELOG.md
2022-04-14 11:56:15 -05:00
Will Schurman 64c1e47674 [eas-cli] Add non-interactive flag to eas update command (#1066) 2022-04-14 09:39:42 -07:00
Dominik Sokal be3a86abda submit - add custom error message for expired apple certs problem (#1068)
* submit - add custom error message for expired apple certs problem

* PR feedback

* update CHANGELOG.md
2022-04-14 11:49:07 +02:00
Dominik Sokal eb2ca117fc [ENG-4615] unify google service account prompts (#1063)
* unify google service account prompt

* update CHANGELOG.md

* fix tests
2022-04-14 09:50:39 +02:00
Dominik Sokal 5043385077 fix expo-cli reference 2022-04-12 11:54:56 +02:00
Will Schurman bb4c08a52a [eas-cli] Require private-key-path to be specified for updating when code signing configured (#1059) 2022-04-11 10:42:31 -07:00
Expo CI 8b75057fa1 update CHANGELOG.md 2022-04-11 08:27:15 +00:00
Dominik Sokal 80cc849137 v0.50.0 2022-04-11 10:20:16 +02:00
Dominik SokalandBrent Vatne 2d407ea9d4 [ENG-4566] refactor status updates for multi-platform builds (#1058)
* refactor status updates for multi-platform builds

* update CHANGELOG

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

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

Co-authored-by: Brent Vatne <[email protected]>
2022-04-11 10:07:20 +02:00
Quinlan Jung 91456d1c65 [eas-cli] require userAuthCtx for methods that don't support jwt (#1055)
* [eas-cli] require userAuthCtx for methods that don't support jwt

* changelog
2022-04-08 10:49:19 -07:00
Quinlan Jung 757a0ccad7 fix changelog 2022-04-07 16:38:48 -07:00
4b7cac0392 [ENG-4419][eas-cli] use asc api token to authenticate for EAS build (#1051)
* [eas-cli] enable authentication with asc api key

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

Co-authored-by: Evan Bacon <[email protected]>

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

Co-authored-by: Evan Bacon <[email protected]>

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

Co-authored-by: Evan Bacon <[email protected]>

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

Co-authored-by: Evan Bacon <[email protected]>

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

Co-authored-by: Evan Bacon <[email protected]>

* Update packages/eas-json/src/build/types.ts

Co-authored-by: Evan Bacon <[email protected]>

* Update packages/eas-json/src/build/types.ts

Co-authored-by: Evan Bacon <[email protected]>

* pr feedback

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

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

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

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

* pr feedback

* changelog

* return instantiation back to original state

Co-authored-by: Evan Bacon <[email protected]>
Co-authored-by: Dominik Sokal <[email protected]>
2022-04-07 15:54:43 -07:00
Will Schurman 8ff8022b3f [eas-cli] Use @expo/multipart-body-parser library (#1054) 2022-04-07 12:19:15 -07:00
Wojciech Kozyra a2acea9fe9 do not delete assigned issues 2022-04-07 16:00:25 +02:00
Wojciech KozyraandKim Brandwijk 9abdfcfdbe autoclose stale issues (#1053)
* autoclose stale issues

* Update .github/workflows/stale-issues.yml

Co-authored-by: Kim Brandwijk <[email protected]>

Co-authored-by: Kim Brandwijk <[email protected]>
2022-04-07 15:51:09 +02:00
Dominik Sokal 696f9dde13 [ENG-3864] provide queue position and estimated wait time (#1049)
* update graphql

* show queue position progress + estimated wait time

* refactor

* update CHANGELOG.md

* hide estimated wait time when build is in progress

* add priority queue info
2022-04-06 13:46:06 +02:00
Expo CI 1e73c8c96f update CHANGELOG.md 2022-04-05 10:48:52 +00:00
Wojciech Kozyra 1b02a67d88 v0.49.0 2022-04-05 12:25:49 +02:00
Wojciech Kozyra d0f158a546 upgrade eas-cli-local-build-plugin to 0.0.74 2022-04-05 12:25:02 +02:00
Evan Bacon 16a2dc681b [eas-cli] Added experimental ASC provisioning profile regeneration (#1038)
* Added experimental provisioning profile regeneration

* Update CHANGELOG.md

* use utility method
2022-04-04 14:36:05 -05:00
Wojciech Kozyra 73800252ae [eas-cli] generate credentials for iOS app extensions in managed projects (#1039)
* [eas-cli] generate credentials for iOS app extensions

* update CHANGELOG.md
2022-04-04 14:40:02 +02:00
Wojciech Kozyra e7c4ba1f54 [eas-cli] make deprecation message more generic (#1047)
* [eas-cli] make deprecation message more generic

* update CHANGELOG.md
2022-04-01 14:43:28 +02:00
Dominik Sokal 84cddaf0e1 upgrade dependencies (#1043)
* upgrade dependencies

* update CHANGELOG.md
2022-04-01 12:49:20 +02:00
Wojciech Kozyra 896f7f0385 [eas-cli] fix proxy support (#1032)
* [eas-cli] fix proxy support

* update CHANGELOG.md
2022-03-25 14:47:02 +01:00
Kim Brandwijk 7f31ea6a2d chore: update bug report issue template label (#1028) 2022-03-24 22:03:13 +01:00
Evan Bacon 3ef397f96c chore: add annotations about functions that do not support App Store Connect API (#1029)
* chore: add annotations about functions that do not support App Store Connect API

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update capabilityIdentifiers.ts
2022-03-24 12:09:37 -05:00
Expo CI 680a684a8e update CHANGELOG.md 2022-03-21 11:27:13 +00:00
256 changed files with 10936 additions and 3141 deletions

No files matched your search

+2 -2
View File
@@ -1,5 +1,5 @@
name: "\U0001F41B Bug Report in EAS CLI"
labels: 'bug'
labels: 'needs review'
description: 'You want to report a reproducible bug or regression in EAS CLI.'
body:
- type: markdown
@@ -37,7 +37,7 @@ body:
required: true
attributes:
label: Environment
description: Run `npx expo-env-info` and paste the output here.
description: Run `npx expo-env-info` and `expo doctor`, and paste the output here.
- type: textarea
validations:
required: false
+1 -1
View File
@@ -1,5 +1,5 @@
name: "\U0001F64B Enhancement"
labels: 'enhancement'
labels: 'enhancement,needs review'
description: 'Want us to add something to EAS CLI?'
body:
- type: markdown
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: 18
- name: Checkout Pull Request
run: hub pr checkout ${{ github.event.issue.number }}
env:
+5 -5
View File
@@ -27,7 +27,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: 18
- run: yarn install --frozen-lockfile --check-files
- run: yarn build
- name: Build tarballs
@@ -68,7 +68,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: 18
- run: yarn install --frozen-lockfile --check-files
- run: yarn build
- name: Build tarballs
@@ -96,7 +96,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: 18
- run: yarn install --frozen-lockfile --check-files
- run: sudo apt-get install nsis
- run: yarn build
@@ -138,7 +138,7 @@ jobs:
with:
registry-url: 'https://registry.npmjs.org/'
scope: 'expo'
node-version: '16'
node-version: 18
- name: Install dependencies
run: yarn install --frozen-lockfile --check-files
- name: Build
@@ -155,7 +155,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: 18
- name: Install dependencies
working-directory: ./scripts
run: yarn install --frozen-lockfile --check-files
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16
node-version: 18
- uses: preactjs/compressed-size-action@v2
with:
build-script: 'build:tarballs:linux'
+24
View File
@@ -0,0 +1,24 @@
name: Close inactive issues
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4
with:
ascending: false
operations-per-run: 300
days-before-issue-stale: 30
days-before-issue-close: 7
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity. If there is no activity in the next 7 days, the issue will be closed."
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale. Please open a new issue if you believe you are encountering a related problem."
days-before-pr-stale: -1
days-before-pr-close: -1
enable-statistics: true
exempt-issue-labels: "needs review"
exempt-all-assignees: true
+2 -2
View File
@@ -10,9 +10,9 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: ['12', '14', '16']
node: ['14', '16', '18']
include:
- node: '16'
- node: '18'
coverage: true
name: Test with Node ${{ matrix.node }}
steps:
+1
View File
@@ -6,6 +6,7 @@ packages/eas-cli/tmp
# Code editors
.idea
.history
.vscode
# macOS
.DS_Store
+129 -2
View File
@@ -8,14 +8,141 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Show bundler output by default for eas update command. ([#1171](https://github.com/expo/eas-cli/pull/1171) by [@kgc00](https://github.com/kgc00/))
- Allow users to skip metadata validation. ([#1175](https://github.com/expo/eas-cli/pull/1175) by [@byCedric](https://github.com/byCedric))
- Add experimental `--resource-class` flag for Build commands. ([#1138](https://github.com/expo/eas-cli/pull/1138) by [@christopherwalter](https://github.com/christopherwalter))
- Truncate long messages for `eas update` command, rather than failing. ([#1178](https://github.com/expo/eas-cli/pull/1178) by [@kgc00](https://github.com/kgc00))
- Add review details to metadata store configuration. ([#1184](https://github.com/expo/eas-cli/pull/1184) by [@byCedric](https://github.com/byCedric))
### 🐛 Bug fixes
- Fix submission archive URLs. ([#1179](https://github.com/expo/eas-cli/pull/1179) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Update validation rules for metadata info locales. ([#1174](https://github.com/expo/eas-cli/pull/1174) by [@byCedric](https://github.com/byCedric))
- Improve store configuration defaults and schema documentation. ([#1183](https://github.com/expo/eas-cli/pull/1183) by [@byCedric](https://github.com/byCedric))
## [0.54.1](https://github.com/expo/eas-cli/releases/tag/v0.54.1) - 2022-06-15
### 🎉 New features
- Add App Store Connect link after `eas metadata:push`. ([#1168](https://github.com/expo/eas-cli/pull/1168) by [@byCedric](https://github.com/byCedric))
### 🐛 Bug fixes
- Surface ASC errors in metadata for normal API rejections. ([#1167](https://github.com/expo/eas-cli/pull/1167) by [@byCedric](https://github.com/byCedric))
- Add metadata schema to bundled eas-cli package. ([#1166](https://github.com/expo/eas-cli/pull/1166) by [@byCedric](https://github.com/byCedric))
## [0.54.0](https://github.com/expo/eas-cli/releases/tag/v0.54.0) - 2022-06-15
### 🛠 Breaking changes
- Remove fallback for legacy format in `eas.json`. ([#1158](https://github.com/expo/eas-cli/pull/1158), [#1163](https://github.com/expo/eas-cli/pull/1163) by [@wkozyra95](https://github.com/wkozyra95))
### 🎉 New features
- `eas update` now provides more information about the publish process including real-time feedback on asset uploads, update ids, and website links. ([#1152](https://github.com/expo/eas-cli/pull/1152) by [@kgc00](https://github.com/kgc00/))
- Added first beta of `eas metadata` to sync store information using store configuration files ([#1136](https://github.com/expo/eas-cli/pull/1136) by [@bycedric](https://github.com/bycedric))
### 🐛 Bug fixes
- Improved support for working on branches with many updates. ([#1144](https://github.com/expo/eas-cli/pull/1144), [#1148](https://github.com/expo/eas-cli/pull/1148) by [@kgc00](https://github.com/kgc00/))
- `eas update` project links no longer contain a `)` on unsupported terminals. ([#1152](https://github.com/expo/eas-cli/pull/1152) by [@kgc00](https://github.com/kgc00/))
### 🧹 Chores
- Update `eas-build-job`. ([#1162](https://github.com/expo/eas-cli/pull/1162) by [@wkozyra95](https://github.com/wkozyra95))
## [0.53.1](https://github.com/expo/eas-cli/releases/tag/v0.53.1) - 2022-06-07
### 🐛 Bug fixes
- No longer timeout on publishing updates for branches with many updates. ([#1119](https://github.com/expo/eas-cli/pull/1119) by [@kgc00](https://github.com/kgc00/))
### 🧹 Chores
- Display project archive docs before compressing/uploading to EAS Build. ([#1127](https://github.com/expo/eas-cli/pull/1127) by [@dsokal](https://github.com/dsokal))
- Add new common error code for submits. ([#1129](https://github.com/expo/eas-cli/pull/1129) by [@dsokal](https://github.com/dsokal))
## [0.53.0](https://github.com/expo/eas-cli/releases/tag/v0.53.0) - 2022-05-30
### 🛠 Breaking changes
- Drop support for Node < 14. ([#1098](https://github.com/expo/eas-cli/pull/1098) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Match bundle identifier capabilities more accurately. ([#1112](https://github.com/expo/eas-cli/pull/1112) by [@EvanBacon](https://github.com/EvanBacon))
- Limit concurrent asset uploads. ([#1153](https://github.com/expo/eas-cli/pull/1153) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Update dependencies. ([#1095](https://github.com/expo/eas-cli/pull/1095), [#1115](https://github.com/expo/eas-cli/pull/1115) by [@dsokal](https://github.com/dsokal))
- Better spinner placement for multiple builds. ([#1105](https://github.com/expo/eas-cli/pull/1105) by [@dsokal](https://github.com/dsokal))
- Use getExpoConfig to access config. ([#1122](https://github.com/expo/eas-cli/pull/1122) by [@wkozyra95](https://github.com/wkozyra95))
## [0.52.0](https://github.com/expo/eas-cli/releases/tag/v0.52.0) - 2022-04-26
### 🎉 New features
- Support target specific entitlements. ([#1078](https://github.com/expo/eas-cli/pull/1078) by [@wkozyra95](https://github.com/wkozyra95))
## [0.51.0](https://github.com/expo/eas-cli/releases/tag/v0.51.0) - 2022-04-19
### 🎉 New features
- Add non-interactive flag to eas update command. ([#1066](https://github.com/expo/eas-cli/pull/1066) by [@wschurman](https://github.com/wschurman))
- Handle new EAS Submit common error - expired Apple's certificates. ([#1068](https://github.com/expo/eas-cli/pull/1068) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Enable full bundling logging for eas update when debugging. ([#1070](https://github.com/expo/eas-cli/pull/1070) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
- Upgrade `@expo/[email protected]` (now with license). ([#1064](https://github.com/expo/eas-cli/pull/1064) by [@EvanBacon](https://github.com/EvanBacon))
- Unify Google Service Account key prompts. ([#1063](https://github.com/expo/eas-cli/pull/1063) by [@dsokal](https://github.com/dsokal))
## [0.50.0](https://github.com/expo/eas-cli/releases/tag/v0.50.0) - 2022-04-11
### 🎉 New features
- Provide queue position progress and estimated wait time. ([#1049](https://github.com/expo/eas-cli/pull/1049), [#1058](https://github.com/expo/eas-cli/pull/1058) by [@dsokal](https://github.com/dsokal))
- Enable App Store authentication for ASC Api Keys with environment variables. ([#1051](https://github.com/expo/eas-cli/pull/1051) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Prompt for user/password authentication where required. ([#1055](https://github.com/expo/eas-cli/pull/1055) by [@quinlanj](https://github.com/quinlanj))
- Require private-key-path to be specified for updating when code signing configured. ([#1059](https://github.com/expo/eas-cli/pull/1059) by [@wschurman](https://github.com/wschurman))
## [0.49.0](https://github.com/expo/eas-cli/releases/tag/v0.49.0) - 2022-04-05
### 🎉 New features
- Added experimental App Store Connect API provisioning profile regeneration. ([#1038](https://github.com/expo/eas-cli/pull/1038) by [@EvanBacon](https://github.com/EvanBacon))
- Experimental support for iOS App Extensions in managed projects. ([#1039](https://github.com/expo/eas-cli/pull/1039) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Fix proxy support. ([#1032](https://github.com/expo/eas-cli/pull/1032) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Add annotations regarding App Store Connect Token session support. ([#1029](https://github.com/expo/eas-cli/pull/1029) by [@EvanBacon](https://github.com/EvanBacon))
- Upgrade dependencies. ([#1043](https://github.com/expo/eas-cli/pull/1043) by [@dsokal](https://github.com/dsokal))
- Make default deprecation message more generic. ([#1047](https://github.com/expo/eas-cli/pull/1047) by [@wkozyra95](https://github.com/wkozyra95))
## [0.48.2](https://github.com/expo/eas-cli/releases/tag/v0.48.2) - 2022-03-21
### 🎉 New features
- Add proxy support. ([#1005](https://github.com/expo/eas-cli/pull/1005) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Do not retry build requests. ([#1024](https://github.com/expo/eas-cli/pull/1024) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
## [0.48.1](https://github.com/expo/eas-cli/releases/tag/v0.48.1) - 2022-03-15
### 🐛 Bug fixes
+6
View File
@@ -1,7 +1,13 @@
* @dsokal @wkozyra95
packages/eas-cli/src/build @dsokal @wkozyra95
packages/eas-cli/src/commands/build @dsokal @wkozyra95
packages/eas-cli/src/submit @barthap @dsokal
packages/eas-cli/src/commands/submit.ts @barthap @dsokal
packages/eas-cli/schema @bycedric
packages/eas-cli/src/metadata @bycedric
packages/eas-cli/src/commands/metadata @bycedric
packages/eas-json @dsokal @wkozyra95
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.48.2"
"version": "0.54.1"
}
+12 -12
View File
@@ -17,22 +17,22 @@
"clean": "lerna run clean && rm -rf node_modules coverage"
},
"devDependencies": {
"@types/jest": "27.4.1",
"@types/node": "16.7.8",
"eslint": "8.11.0",
"eslint-config-universe": "10.0.0",
"@types/jest": "27.5.0",
"@types/node": "17.0.32",
"eslint": "8.15.0",
"eslint-config-universe": "11.0.0",
"eslint-plugin-async-protect": "1.1.0",
"eslint-plugin-node": "11.1.0",
"jest": "27.5.1",
"jest-watch-typeahead": "1.0.0",
"jest": "28.1.0",
"jest-watch-typeahead": "1.1.0",
"lerna": "4.0.0",
"oclif": "2.5.0",
"prettier": "2.5.1",
"ts-jest": "27.1.3",
"typescript": "4.6.2"
"oclif": "3.0.1",
"prettier": "2.6.2",
"ts-jest": "28.0.2",
"typescript": "4.6.4"
},
"volta": {
"node": "16.10.0",
"yarn": "1.22.15"
"node": "18.2.0",
"yarn": "1.22.18"
}
}
+175 -45
View File
@@ -48,6 +48,7 @@ eas --help COMMAND
* [`eas build:configure`](#eas-buildconfigure)
* [`eas build:inspect`](#eas-buildinspect)
* [`eas build:list`](#eas-buildlist)
* [`eas build:submit`](#eas-buildsubmit)
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
* [`eas channel:create [NAME]`](#eas-channelcreate-name)
* [`eas channel:edit [NAME]`](#eas-channeledit-name)
@@ -61,6 +62,11 @@ eas --help COMMAND
* [`eas device:view [UDID]`](#eas-deviceview-udid)
* [`eas diagnostics`](#eas-diagnostics)
* [`eas help [COMMAND]`](#eas-help-command)
* [`eas init`](#eas-init)
* [`eas login`](#eas-login)
* [`eas logout`](#eas-logout)
* [`eas metadata:pull`](#eas-metadatapull)
* [`eas metadata:push`](#eas-metadatapush)
* [`eas project:info`](#eas-projectinfo)
* [`eas project:init`](#eas-projectinit)
* [`eas secret:create`](#eas-secretcreate)
@@ -77,6 +83,7 @@ eas --help COMMAND
* [`eas webhook:list`](#eas-webhooklist)
* [`eas webhook:update`](#eas-webhookupdate)
* [`eas webhook:view ID`](#eas-webhookview-id)
* [`eas whoami`](#eas-whoami)
## `eas account:login`
@@ -93,7 +100,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -110,7 +117,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -127,7 +134,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -141,7 +148,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -170,7 +177,7 @@ EXAMPLES
$ eas autocomplete --refresh-cache
```
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v1.2.0/src/commands/autocomplete/index.ts)_
_See code: [@expo/plugin-autocomplete](https://github.com/expo/plugin-autocomplete/blob/v1.4.0/src/commands/autocomplete/index.ts)_
## `eas branch:create [NAME]`
@@ -190,7 +197,7 @@ DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -210,7 +217,7 @@ DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -227,7 +234,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -246,7 +253,7 @@ DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -266,7 +273,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -295,7 +302,7 @@ DESCRIPTION
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -309,7 +316,7 @@ DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -326,7 +333,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:inspect`
@@ -361,7 +368,7 @@ DESCRIPTION
inspect the state of the project at specific build stages, useful for troubleshooting
```
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -394,7 +401,35 @@ DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:submit`
submit app binary to App Store and/or Play Store
```
USAGE
$ eas build:submit [-p android|ios|all] [--profile <value>] [--latest | --id <value> | --path <value> | --url
<value>] [--verbose] [--wait] [--non-interactive]
FLAGS
-p, --platform=(android|ios|all)
--id=<value> ID of the build to submit
--latest Submit the latest build for specified platform
--non-interactive Run command in non-interactive mode
--path=<value> Path to the .apk/.aab/.ipa file
--profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in
eas.json.
--url=<value> App archive url
--verbose Always print logs from Submission Service
--[no-]wait Wait for submission to complete
DESCRIPTION
submit app binary to App Store and/or Play Store
ALIASES
$ eas build:submit
```
## `eas build:view [BUILD_ID]`
@@ -411,7 +446,7 @@ DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -431,7 +466,7 @@ DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -452,7 +487,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -469,7 +504,7 @@ DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:view [NAME]`
@@ -489,7 +524,7 @@ DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -507,7 +542,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -521,7 +556,7 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -535,7 +570,7 @@ DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -553,7 +588,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -570,7 +605,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -584,7 +619,7 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -598,7 +633,7 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -618,7 +653,86 @@ DESCRIPTION
Display help for eas.
```
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.1.12/src/commands/help.ts)_
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.2.0/src/commands/help.ts)_
## `eas init`
create or link an EAS project
```
USAGE
$ eas init
DESCRIPTION
create or link an EAS project
ALIASES
$ eas init
```
## `eas login`
log in with your Expo account
```
USAGE
$ eas login
DESCRIPTION
log in with your Expo account
ALIASES
$ eas login
```
## `eas logout`
log out
```
USAGE
$ eas logout
DESCRIPTION
log out
ALIASES
$ eas logout
```
## `eas metadata:pull`
generate the local store configuration from the app stores
```
USAGE
$ eas metadata:pull [--profile <value>]
FLAGS
--profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.
DESCRIPTION
generate the local store configuration from the app stores
```
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
sync the local store configuration to the app stores
```
USAGE
$ eas metadata:push [--profile <value>]
FLAGS
--profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.
DESCRIPTION
sync the local store configuration to the app stores
```
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas project:info`
@@ -632,7 +746,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -649,7 +763,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/project/init.ts)_
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/project/init.ts)_
## `eas secret:create`
@@ -669,7 +783,7 @@ DESCRIPTION
create an environment secret on the current project or owner account
```
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -686,7 +800,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -700,7 +814,7 @@ DESCRIPTION
list environment secrets available for your current app
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/secret/list.ts)_
## `eas submit`
@@ -730,7 +844,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -739,7 +853,7 @@ publish an update group
```
USAGE
$ eas update [--branch <value>] [--message <value>] [--republish | --input-dir <value> | --skip-bundler]
[--group <value> | | ] [-p android|ios|all] [--json] [--auto] [--private-key-path <value>]
[--group <value> | | ] [-p android|ios|all] [--json] [--auto] [--private-key-path <value>] [--non-interactive]
FLAGS
-p, --platform=(android|ios|all) [default: all]
@@ -749,6 +863,7 @@ FLAGS
--input-dir=<value> [default: dist] Location of the bundle
--json Enable JSON output, non-JSON messages will be printed to stderr
--message=<value> A short message describing the update
--non-interactive Run command in non-interactive mode
--private-key-path=<value> File containing the PEM-encoded private key corresponding to the certificate in
expo-updates' configuration. Defaults to a file named "private-key.pem" in the
certificate's directory.
@@ -759,7 +874,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/update/index.ts)_
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -776,7 +891,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -796,7 +911,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:list`
@@ -815,7 +930,7 @@ DESCRIPTION
view the recent updates for a branch
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/update/list.ts)_
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:view GROUPID`
@@ -835,7 +950,7 @@ DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/update/view.ts)_
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -855,7 +970,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -872,7 +987,7 @@ DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -889,7 +1004,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -910,7 +1025,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -927,5 +1042,20 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.54.1/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
show the username you are logged in as
```
USAGE
$ eas whoami
DESCRIPTION
show the username you are logged in as
ALIASES
$ eas whoami
```
<!-- commandsstop -->
+1235 -88
View File
File diff suppressed because it is too large. Load diff
+47 -41
View File
@@ -1,50 +1,52 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.48.2",
"version": "0.54.1",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.28",
"@expo/apple-utils": "0.0.0-alpha.33",
"@expo/code-signing-certificates": "0.0.2",
"@expo/config": "6.0.19",
"@expo/config-plugins": "4.1.0",
"@expo/config-types": "44.0.0",
"@expo/eas-build-job": "0.2.66",
"@expo/eas-json": "0.48.0",
"@expo/json-file": "8.2.34",
"@expo/pkcs12": "0.0.7",
"@expo/plist": "0.0.17",
"@expo/plugin-warn-if-update-available": "2.1.0",
"@expo/prebuild-config": "3.1.0",
"@expo/config": "6.0.23",
"@expo/config-plugins": "4.1.4",
"@expo/config-types": "45.0.0",
"@expo/eas-build-job": "0.2.77",
"@expo/eas-json": "0.54.0",
"@expo/json-file": "8.2.36",
"@expo/multipart-body-parser": "1.1.0",
"@expo/pkcs12": "0.0.8",
"@expo/plist": "0.0.18",
"@expo/plugin-autocomplete": "1.4.0",
"@expo/plugin-help": "5.2.0",
"@expo/plugin-warn-if-update-available": "2.3.0",
"@expo/prebuild-config": "3.1.6",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.6.0",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "1.5.3",
"@oclif/plugin-autocomplete": "1.2.0",
"@oclif/plugin-help": "5.1.12",
"@urql/core": "2.4.3",
"@urql/exchange-retry": "0.3.2",
"@oclif/core": "1.9.0",
"@urql/core": "2.4.4",
"@urql/exchange-retry": "0.3.3",
"ajv": "^6.12.6",
"chalk": "4.1.2",
"cli-table3": "0.6.1",
"cli-progress": "3.11.0",
"cli-table3": "0.6.2",
"dateformat": "4.6.3",
"dicer": "0.3.1",
"env-paths": "2.2.0",
"envinfo": "7.8.1",
"fast-deep-equal": "3.1.3",
"fast-glob": "3.2.11",
"figures": "3.2.0",
"form-data": "4.0.0",
"fs-extra": "10.0.1",
"fs-extra": "10.1.0",
"getenv": "1.0.0",
"gradle-to-js": "2.0.0",
"graphql": "16.3.0",
"gradle-to-js": "2.0.1",
"graphql": "16.5.0",
"graphql-tag": "2.12.6",
"https-proxy-agent": "5.0.0",
"https-proxy-agent": "5.0.1",
"ignore": "5.2.0",
"indent-string": "4.0.0",
"jks-js": "1.0.2",
@@ -52,23 +54,23 @@
"keychain": "1.3.0",
"log-symbols": "4.1.0",
"mime": "3.0.0",
"minimatch": "3.0.5",
"nanoid": "3.3.1",
"minimatch": "5.0.1",
"nanoid": "3.3.4",
"node-fetch": "2.6.7",
"node-forge": "1.2.1",
"node-forge": "1.3.1",
"nullthrows": "1.1.1",
"ora": "5.1.0",
"pkg-dir": "4.2.0",
"promise-limit": "2.7.0",
"prompts": "2.4.2",
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
"semver": "7.3.5",
"semver": "7.3.7",
"slash": "3.0.0",
"strip-ansi": "6.0.0",
"structured-headers": "0.4.1",
"tar": "6.1.11",
"terminal-link": "2.1.1",
"tslib": "2.3.1",
"tslib": "2.4.0",
"turndown": "7.1.1",
"untildify": "4.0.0",
"uuid": "8.3.2",
@@ -77,39 +79,40 @@
"devDependencies": {
"@graphql-codegen/cli": "2.6.2",
"@graphql-codegen/introspection": "2.1.1",
"@graphql-codegen/typescript": "2.4.7",
"@graphql-codegen/typescript-operations": "2.3.4",
"@graphql-codegen/typescript": "2.4.11",
"@graphql-codegen/typescript-operations": "2.4.0",
"@types/cli-progress": "3.9.2",
"@types/dateformat": "3.0.1",
"@types/dicer": "0.2.2",
"@types/envinfo": "7.8.1",
"@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": "1.0.1",
"@types/node-forge": "1.0.2",
"@types/prompts": "2.0.14",
"@types/semver": "7.3.9",
"@types/tar": "6.1.1",
"@types/tough-cookie": "4.0.1",
"@types/tough-cookie": "4.0.2",
"@types/uuid": "8.3.4",
"@types/wrap-ansi": "3.0.0",
"axios": "0.26.1",
"axios": "0.27.2",
"eslint-plugin-graphql": "4.0.0",
"form-data": "4.0.0",
"memfs": "3.4.1",
"memfs": "3.4.3",
"mockdate": "3.0.5",
"nock": "13.2.4",
"tempy": "0.7.0",
"ts-deepmerge": "2.0.1",
"typescript": "4.6.2"
"typescript": "4.6.4"
},
"engines": {
"node": ">=12.0.0"
"node": ">=14.0.0"
},
"files": [
"/bin",
"/build",
"/oclif.manifest.json"
"/oclif.manifest.json",
"/schema"
],
"homepage": "https://github.com/expo/eas-cli",
"keywords": [
@@ -122,8 +125,8 @@
"bin": "eas",
"commands": "./build/commands",
"plugins": [
"@oclif/plugin-autocomplete",
"@oclif/plugin-help",
"@expo/plugin-autocomplete",
"@expo/plugin-help",
"@expo/plugin-warn-if-update-available"
],
"topics": {
@@ -142,6 +145,9 @@
"device": {
"description": "manage Apple devices for Internal Distribution"
},
"metadata": {
"description": "manage store configuration"
},
"project": {
"description": "manage project"
},
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,17 @@
import { fixArchiveUrl } from '../uploads';
describe(fixArchiveUrl, () => {
it('fixes broken links', () => {
const input =
'https://submission-service-archives.s3.amazonaws.com/production%2Fdc98ca84-1473-4cb3-ae81-8c7b291cb27e%2F4424aa95-b985-4e2f-8755-9507b1037c1c';
const expectedOutput =
'https://submission-service-archives.s3.amazonaws.com/production/dc98ca84-1473-4cb3-ae81-8c7b291cb27e/4424aa95-b985-4e2f-8755-9507b1037c1c';
expect(fixArchiveUrl(input)).toBe(expectedOutput);
});
it('does not change correct urls', () => {
const correctUrl =
'https://submission-service-archives.s3.amazonaws.com/production/dc98ca84-1473-4cb3-ae81-8c7b291cb27e/4424aa95-b985-4e2f-8755-9507b1037c1c';
expect(fixArchiveUrl(correctUrl)).toBe(correctUrl);
});
});
-3
View File
@@ -1,3 +0,0 @@
export function asMock(fn: any): jest.Mock {
return fn as jest.Mock;
}
+6 -1
View File
@@ -1,5 +1,5 @@
import { logEvent } from './rudderstackClient';
export type Event = BuildEvent | SubmissionEvent;
export type Event = BuildEvent | SubmissionEvent | MetadataEvent;
export enum SubmissionEvent {
SUBMIT_COMMAND = 'submit cli submit command',
@@ -45,6 +45,11 @@ export enum BuildEvent {
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
}
export enum MetadataEvent {
APPLE_METADATA_DOWNLOAD = 'metadata cli download apple response',
APPLE_METADATA_UPLOAD = 'metadata cli upload apple response',
}
export class Analytics {
static logEvent(name: Event, properties: Record<string, any>): void {
logEvent(name, properties);
+4 -1
View File
@@ -5,6 +5,7 @@ import nullthrows from 'nullthrows';
import AndroidCredentialsProvider, {
AndroidCredentials,
} from '../../credentials/android/AndroidCredentialsProvider';
import { BuildParamsInput } from '../../graphql/generated';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import Log from '../../log';
import {
@@ -87,7 +88,8 @@ export async function prepareAndroidBuildAsync(
sendBuildRequestAsync: async (
appId: string,
job: Android.Job,
metadata: Metadata
metadata: Metadata,
buildParams: BuildParamsInput
): Promise<BuildResult> => {
const graphqlMetadata = transformMetadata(metadata);
const graphqlJob = transformJob(job);
@@ -95,6 +97,7 @@ export async function prepareAndroidBuildAsync(
appId,
job: graphqlJob,
metadata: graphqlMetadata,
buildParams,
});
},
});
@@ -105,7 +105,7 @@ export async function maybeResolveVersionsAsync(
appBuildVersion:
resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1',
};
} catch (err: any) {
} catch {
return {};
}
} else {
+313 -137
View File
@@ -1,16 +1,30 @@
import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import cliProgress from 'cli-progress';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { withAnalyticsAsync } from '../analytics/common';
import { BuildEvent } from '../analytics/events';
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
import { getExpoWebsiteBaseUrl } from '../api';
import {
AppPlatform,
BuildFragment,
BuildParamsInput,
BuildPriority,
BuildStatus,
UploadSessionType,
} from '../graphql/generated';
import { BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log, { learnMore } from '../log';
import { ora } from '../ora';
import { requestedPlatformDisplayNames } from '../platform';
import { Ora, ora } from '../ora';
import {
appPlatformDisplayNames,
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../platform';
import { uploadAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
@@ -40,11 +54,22 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
): Promise<CredentialsResult<Credentials> | undefined>;
syncProjectConfigurationAsync(ctx: BuildContext<TPlatform>): Promise<void>;
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
sendBuildRequestAsync(
appId: string,
job: TJob,
metadata: Metadata,
buildParams: BuildParamsInput
): Promise<BuildResult>;
}
export type BuildRequestSender = () => Promise<BuildFragment | undefined>;
function resolveBuildParamsInput<T extends Platform>(ctx: BuildContext<T>): BuildParamsInput {
return {
resourceClass: ctx.resourceClass,
};
}
export async function prepareBuildRequestForPlatformAsync<
TPlatform extends Platform,
Credentials,
@@ -87,6 +112,7 @@ export async function prepareBuildRequestForPlatformAsync<
} as const);
const metadata = await collectMetadataAsync(ctx);
const buildParams = resolveBuildParamsInput(ctx);
const job = await builder.prepareJobAsync(ctx, {
projectArchive,
credentials: credentialsResult?.credentials,
@@ -98,45 +124,45 @@ export async function prepareBuildRequestForPlatformAsync<
return undefined;
} else {
try {
return await sendBuildRequestAsync(builder, job, metadata);
return await sendBuildRequestAsync(builder, job, metadata, buildParams);
} catch (error: any) {
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli version.');
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
) {
Log.error(
'EAS Build is down for maintenance, please try again later. Check https://status.expo.dev/ for updates.'
);
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED'
) {
Log.error(
'EAS Build free tier is temporarily disabled, please try again later. Check https://status.expo.dev/ for updates.'
);
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
) {
Log.error(
`You have already reached the maximum number of pending ${
requestedPlatformDisplayNames[job.platform]
} builds for your account. Try again later.`
);
throw new Error('Build request failed.');
} else if (error?.graphQLErrors) {
Log.error(
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, please report the issue.'
);
}
throw error;
handleBuildRequestError(error, job.platform);
}
}
};
}
function handleBuildRequestError(error: any, platform: Platform): never {
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli version.');
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
) {
Log.error(
'EAS Build is down for maintenance, please try again later. Check https://status.expo.dev/ for updates.'
);
throw new Error('Build request failed.');
} else if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED') {
Log.error(
'EAS Build free tier is temporarily disabled, please try again later. Check https://status.expo.dev/ for updates.'
);
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
) {
Log.error(
`You have already reached the maximum number of pending ${requestedPlatformDisplayNames[platform]} builds for your account. Try again later.`
);
throw new Error('Build request failed.');
} else if (error?.graphQLErrors) {
Log.error(
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, please report the issue.'
);
}
throw error;
}
async function uploadProjectAsync<TPlatform extends Platform>(
ctx: BuildContext<TPlatform>
): Promise<string> {
@@ -144,6 +170,12 @@ async function uploadProjectAsync<TPlatform extends Platform>(
try {
return await withAnalyticsAsync(
async () => {
Log.newLine();
Log.log(
`Compressing project files and uploading to EAS Build. ${learnMore(
'https://expo.fyi/eas-build-archive'
)}`
);
const projectTarball = await makeProjectTarballAsync();
projectTarballPath = projectTarball.path;
@@ -156,10 +188,7 @@ async function uploadProjectAsync<TPlatform extends Platform>(
`Uploading to EAS Build (${formatBytes(projectTarball.size * ratio)} / ${formatBytes(
projectTarball.size
)})`,
completedMessage: (duration: string) =>
`Uploaded to EAS ${chalk.dim(duration)} ${learnMore(
'https://expo.fyi/eas-build-archive'
)}`,
completedMessage: (duration: string) => `Uploaded to EAS ${chalk.dim(duration)}`,
})
);
return bucketKey;
@@ -181,7 +210,8 @@ async function uploadProjectAsync<TPlatform extends Platform>(
async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJob extends Job>(
builder: Builder<TPlatform, Credentials, TJob>,
job: TJob,
metadata: Metadata
metadata: Metadata,
buildParams: BuildParamsInput
): Promise<BuildFragment> {
const { ctx } = builder;
return await withAnalyticsAsync(
@@ -193,7 +223,8 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
ctx.projectId,
job,
metadata
metadata,
buildParams
);
printDeprecationWarnings(deprecationInfo);
@@ -208,105 +239,250 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
);
}
type MaybeBuildFragment = BuildFragment | null;
export async function waitForBuildEndAsync(
buildIds: string[],
{ timeoutSec = 3600, intervalSec = 30 } = {}
): Promise<(BuildFragment | null)[]> {
Log.log(
`Waiting for build${buildIds.length > 1 ? 's' : ''} to complete. You can press Ctrl+C to exit.`
);
const spinner = ora().start();
let time = new Date().getTime();
const endTime = time + timeoutSec * 1000;
while (time <= endTime) {
const builds: (BuildFragment | null)[] = await Promise.all(
buildIds.map(async buildId => {
try {
return await BuildQuery.byIdAsync(buildId, { useCache: false });
} catch (err) {
Log.debug('Failed to fetch the build status', err);
return null;
}
})
);
if (builds.length === 1) {
const [build] = builds;
if (build !== null) {
switch (build.status) {
case BuildStatus.Finished:
spinner.succeed('Build finished');
return builds;
case BuildStatus.New:
spinner.text = 'Build created';
break;
case BuildStatus.InQueue:
spinner.text = 'Build queued...';
break;
case BuildStatus.Canceled:
spinner.text = 'Build canceled';
spinner.stopAndPersist();
return builds;
case BuildStatus.InProgress:
spinner.text = 'Build in progress...';
break;
case BuildStatus.Errored:
spinner.fail('Build failed');
if (build.error) {
return builds;
} else {
throw new Error(`Standalone build failed!`);
}
default:
spinner.warn('Unknown status');
throw new Error(`Unknown build status: ${build.status} - aborting!`);
}
} else {
if (!spinner.text) {
spinner.text = 'Could not fetch the build status. Check your network connection.';
}
}
} else {
if (builds.filter(build => build?.status === BuildStatus.Finished).length === builds.length) {
spinner.succeed('All builds have finished');
return builds;
} else if (
builds.filter(build =>
build?.status
? [BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(
build.status
)
: false
).length === builds.length
) {
spinner.fail('Some of the builds were canceled or failed.');
return builds;
} else {
const newBuilds = builds.filter(build => build?.status === BuildStatus.New).length;
const inQueue = builds.filter(build => build?.status === BuildStatus.InQueue).length;
const inProgress = builds.filter(build => build?.status === BuildStatus.InProgress).length;
const errored = builds.filter(build => build?.status === BuildStatus.Errored).length;
const finished = builds.filter(build => build?.status === BuildStatus.Finished).length;
const canceled = builds.filter(build => build?.status === BuildStatus.Canceled).length;
const unknown =
builds.length - newBuilds - inQueue - inProgress - errored - finished - canceled;
spinner.text = [
newBuilds && `Builds created: ${newBuilds}`,
inQueue && `Builds in queue: ${inQueue}`,
inProgress && `Builds in progress: ${inProgress}`,
canceled && `Builds canceled: ${canceled}`,
errored && chalk.red(`Builds failed: ${errored}`),
finished && chalk.green(`Builds finished: ${finished}`),
unknown && chalk.red(`Builds with unknown status: ${unknown}`),
]
.filter(i => i)
.join('\t');
}
{ buildIds, accountName }: { buildIds: string[]; accountName: string },
{
// 2 hours (max build time limit) + 10 minutes (possible queue time)
timeoutSec = 2 * 60 * 60 + 10 * 60,
intervalSec = 10,
} = {}
): Promise<MaybeBuildFragment[]> {
let spinner;
let originalSpinnerText;
if (buildIds.length === 1) {
Log.log('Waiting for build to complete. You can press Ctrl+C to exit.');
originalSpinnerText = 'Waiting for build to complete.';
spinner = ora(originalSpinnerText).start();
} else {
originalSpinnerText = 'Waiting for builds to complete. You can press Ctrl+C to exit.';
spinner = ora('Waiting for builds to complete. You can press Ctrl+C to exit.').start();
}
const endTime = new Date().getTime() + timeoutSec * 1000;
while (new Date().getTime() <= endTime) {
const builds = await getBuildsSafelyAsync(buildIds);
const { refetch } =
builds.length === 1
? await handleSingleBuildProgressAsync({ build: builds[0], accountName }, { spinner })
: await handleMultipleBuildsProgressAsync({ builds }, { spinner, originalSpinnerText });
if (!refetch) {
return builds;
}
time = new Date().getTime();
await sleepAsync(intervalSec * 1000);
}
spinner.warn('Timed out');
spinner.fail('Timed out');
throw new Error(
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
);
}
async function getBuildsSafelyAsync(buildIds: string[]): Promise<MaybeBuildFragment[]> {
const promises = buildIds.map(async buildId => {
try {
return await BuildQuery.byIdAsync(buildId, { useCache: false });
} catch (err) {
Log.debug('Failed to fetch the build status', err);
return null;
}
});
return await Promise.all(promises);
}
interface BuildProgressResult {
refetch: boolean;
}
let queueProgressBarStarted = false;
const queueProgressBar = new cliProgress.SingleBar(
{ format: '|{bar}| {estimatedWaitTime}' },
cliProgress.Presets.rect
);
async function handleSingleBuildProgressAsync(
{ build, accountName }: { build: MaybeBuildFragment; accountName: string },
{ spinner }: { spinner: Ora }
): Promise<BuildProgressResult> {
if (build === null) {
spinner.text = 'Could not fetch the build status. Check your network connection.';
return { refetch: true };
}
if (queueProgressBarStarted && build?.status && build.status !== BuildStatus.InQueue) {
if (build.status === BuildStatus.InProgress) {
queueProgressBar.update(queueProgressBar.getTotal(), {
estimatedWaitTime: '',
});
}
queueProgressBar.stop();
Log.newLine();
queueProgressBarStarted = false;
spinner.start('Build is about to start');
}
switch (build.status) {
case BuildStatus.Finished:
spinner.succeed('Build finished');
return { refetch: false };
case BuildStatus.New:
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${chalk.underline(
formatAccountSubscriptionsUrl(accountName)
)}.`;
break;
case BuildStatus.InQueue: {
spinner.text = 'Build queued...';
const progressBarPayload =
typeof build.estimatedWaitTimeLeftSeconds === 'number'
? { estimatedWaitTime: formatEstimatedWaitTime(build.estimatedWaitTimeLeftSeconds) }
: { estimatedWaitTime: '' };
if (
!queueProgressBarStarted &&
typeof build.initialQueuePosition === 'number' &&
typeof build.queuePosition === 'number'
) {
spinner.stopAndPersist();
if (build.priority !== BuildPriority.High) {
Log.newLine();
Log.log('Start builds sooner in the priority queue.');
Log.log(
`Sign up for EAS Production or Enterprise at ${chalk.underline(
formatAccountSubscriptionsUrl(accountName)
)}`
);
}
Log.newLine();
Log.log('Waiting in queue');
queueProgressBar.start(
build.initialQueuePosition + 1,
build.initialQueuePosition - build.queuePosition + 1,
progressBarPayload
);
queueProgressBarStarted = true;
}
if (typeof build.queuePosition === 'number') {
queueProgressBar.update(build.queuePosition, progressBarPayload);
}
break;
}
case BuildStatus.Canceled:
spinner.fail('Build canceled');
return { refetch: false };
case BuildStatus.InProgress:
spinner.text = 'Build in progress...';
break;
case BuildStatus.Errored:
spinner.fail('Build failed');
if (build.error) {
return { refetch: false };
} else {
throw new Error('Standalone build failed!');
}
default:
spinner.warn('Unknown status');
throw new Error(`Unknown build status: ${build.status} - aborting!`);
}
return { refetch: true };
}
const statusToDisplayName: Record<BuildStatus, string> = {
[BuildStatus.New]: 'waiting to enter the queue (concurrency limit reached)',
[BuildStatus.InQueue]: 'in queue',
[BuildStatus.InProgress]: 'in progress',
[BuildStatus.Canceled]: 'canceled',
[BuildStatus.Finished]: 'finished',
[BuildStatus.Errored]: 'failed',
};
const platforms = [AppPlatform.Android, AppPlatform.Ios];
async function handleMultipleBuildsProgressAsync(
{ builds: maybeBuilds }: { builds: MaybeBuildFragment[] },
{ spinner, originalSpinnerText }: { spinner: Ora; originalSpinnerText: string }
): Promise<BuildProgressResult> {
const buildCount = maybeBuilds.length;
const builds = maybeBuilds.filter<BuildFragment>(isBuildFragment);
const allFinished =
builds.filter(build => build.status === BuildStatus.Finished).length === buildCount;
const allSettled =
builds.filter(build =>
[BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(build.status)
).length === buildCount;
if (allSettled) {
if (allFinished) {
spinner.succeed(formatSettledBuildsText(builds));
} else {
spinner.fail(formatSettledBuildsText(builds));
}
return { refetch: false };
} else {
spinner.text = formatPendingBuildsText(originalSpinnerText, builds);
return { refetch: true };
}
}
function formatSettledBuildsText(builds: BuildFragment[]): string {
return platforms
.map(platform => {
const build = nullthrows(
builds.find(build => build.platform === platform),
`Build for platform ${platform} must be defined in this context`
);
return `${appPlatformEmojis[platform]} ${
appPlatformDisplayNames[platform]
} build - status: ${chalk.bold(statusToDisplayName[build.status])}`;
})
.join('\n ');
}
function formatPendingBuildsText(originalSpinnerText: string, builds: BuildFragment[]): string {
return [
originalSpinnerText,
...platforms.map(platform => {
const build = builds.find(build => build.platform === platform);
const status = build ? statusToDisplayName[build.status] : 'unknown';
let extraInfo = '';
if (
build?.status === BuildStatus.InQueue &&
typeof build.initialQueuePosition === 'number' &&
typeof build.queuePosition === 'number'
) {
const percent = Math.floor(
((build.initialQueuePosition - build.queuePosition + 1) /
(build.initialQueuePosition + 1)) *
100
);
const estimatedWaitTime =
typeof build.estimatedWaitTimeLeftSeconds === 'number'
? ` - ${formatEstimatedWaitTime(build.estimatedWaitTimeLeftSeconds)}`
: '';
extraInfo = ` - queue progress: ${chalk.bold(`${percent}%`)}${estimatedWaitTime}`;
}
return `${appPlatformEmojis[platform]} ${
appPlatformDisplayNames[platform]
} build - status: ${chalk.bold(status)}${extraInfo}`;
}),
].join('\n ');
}
function isBuildFragment(maybeBuild: MaybeBuildFragment): maybeBuild is BuildFragment {
return maybeBuild !== null;
}
function formatEstimatedWaitTime(estimatedWaitTimeLeftSeconds: number): string {
if (estimatedWaitTimeLeftSeconds < 5 * 60) {
return 'starting soon...';
} else {
const n = Math.floor(estimatedWaitTimeLeftSeconds / (10 * 60)) + 1;
return `starting in about ${n}0 minutes...`;
}
}
function formatAccountSubscriptionsUrl(accountName: string): string {
return new URL(
`/accounts/${accountName}/settings/subscriptions`,
getExpoWebsiteBaseUrl()
).toString();
}
+2
View File
@@ -6,6 +6,7 @@ import type { XCBuildConfiguration } from 'xcode';
import { TrackingContext } from '../analytics/common';
import { CredentialsContext } from '../credentials/context';
import { Target } from '../credentials/ios/types';
import { BuildResourceClass } from '../graphql/generated';
import { GradleBuildContext } from '../project/android/gradle';
import { XcodeBuildContext } from '../project/ios/scheme';
import { Actor } from '../user/User';
@@ -30,6 +31,7 @@ export interface BuildContext<T extends Platform> {
accountName: string;
buildProfile: BuildProfile<T>;
buildProfileName: string;
resourceClass: BuildResourceClass;
clearCache: boolean;
credentialsCtx: CredentialsContext;
exp: ExpoConfig;
@@ -7,6 +7,7 @@ import { v4 as uuidv4 } from 'uuid';
import { TrackingContext } from '../analytics/common';
import { Analytics, BuildEvent } from '../analytics/events';
import { CredentialsContext } from '../credentials/context';
import { BuildResourceClass } from '../graphql/generated';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
@@ -25,6 +26,7 @@ export async function createBuildContextAsync<T extends Platform>({
nonInteractive = false,
platform,
projectDir,
resourceClass,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
@@ -33,6 +35,7 @@ export async function createBuildContextAsync<T extends Platform>({
nonInteractive: boolean;
platform: T;
projectDir: string;
resourceClass: BuildResourceClass;
}): Promise<BuildContext<T>> {
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
@@ -70,6 +73,7 @@ export async function createBuildContextAsync<T extends Platform>({
accountName,
buildProfile,
buildProfileName,
resourceClass,
clearCache,
credentialsCtx,
exp,
+10 -8
View File
@@ -3,6 +3,7 @@ import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import type { XCBuildConfiguration } from 'xcode';
import { IosCredentials, Target } from '../../credentials/ios/types';
import { BuildParamsInput } from '../../graphql/generated';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
@@ -36,13 +37,12 @@ export async function createIosContextAsync(
},
buildProfile
);
const targets = await resolveTargetsAsync(
{
projectDir: ctx.projectDir,
exp: ctx.exp,
},
xcodeBuildContext
);
const targets = await resolveTargetsAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
xcodeBuildContext,
env: buildProfile.env,
});
const applicationTarget = findApplicationTarget(targets);
const applicationTargetBuildSettings = resolveBuildSettings(ctx, applicationTarget);
@@ -83,7 +83,8 @@ export async function prepareIosBuildAsync(
sendBuildRequestAsync: async (
appId: string,
job: Ios.Job,
metadata: Metadata
metadata: Metadata,
buildParams: BuildParamsInput
): Promise<BuildResult> => {
const graphqlMetadata = transformMetadata(metadata);
const graphqlJob = transformJob(job);
@@ -91,6 +92,7 @@ export async function prepareIosBuildAsync(
appId,
job: graphqlJob,
metadata: graphqlMetadata,
buildParams,
});
},
});
@@ -2,7 +2,6 @@ import { Platform } from '@expo/eas-build-job';
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
import { resolveEntitlementsJsonAsync } from '../../credentials/ios/appstore/entitlements';
import { IosCredentials, Target } from '../../credentials/ios/types';
import { CredentialsResult } from '../build';
import { BuildContext } from '../context';
@@ -19,13 +18,6 @@ export async function ensureIosCredentialsAsync(
const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, {
app: getAppFromContext(buildCtx.credentialsCtx),
targets,
iosCapabilitiesOptions: {
entitlements: await resolveEntitlementsJsonAsync(
buildCtx.projectDir,
buildCtx.workflow,
buildCtx.buildProfile.env ?? {}
),
},
distribution: buildCtx.buildProfile.distribution ?? 'store',
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
});
+1 -1
View File
@@ -6,7 +6,7 @@ import semver from 'semver';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.67';
const PLUGIN_PACKAGE_VERSION = '0.0.85';
export interface LocalBuildOptions {
enable: boolean;
@@ -1,9 +1,15 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, SubmitProfile } from '@expo/eas-json';
import { BuildProfile, EasJsonReader, SubmitProfile } from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { AppPlatform, BuildFragment, BuildStatus, SubmissionFragment } from '../graphql/generated';
import {
AppPlatform,
BuildFragment,
BuildResourceClass,
BuildStatus,
SubmissionFragment,
} from '../graphql/generated';
import { toAppPlatform, toPlatform } from '../graphql/types/AppPlatform';
import Log from '../log';
import {
@@ -30,6 +36,7 @@ import { BuildContext } from './context';
import { createBuildContextAsync } from './createContext';
import { prepareIosBuildAsync } from './ios/build';
import { LocalBuildOptions } from './local';
import { UserInputResourceClass } from './types';
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
import { ensureRepoIsCleanAsync } from './utils/repository';
@@ -47,8 +54,23 @@ export interface BuildFlags {
autoSubmit: boolean;
submitProfile?: string;
localBuildOptions: LocalBuildOptions;
userInputResourceClass?: UserInputResourceClass;
}
const platformToGraphQLResourceClassMapping: Record<
Platform,
Record<UserInputResourceClass, BuildResourceClass>
> = {
[Platform.ANDROID]: {
[UserInputResourceClass.DEFAULT]: BuildResourceClass.AndroidDefault,
[UserInputResourceClass.LARGE]: BuildResourceClass.AndroidLarge,
},
[Platform.IOS]: {
[UserInputResourceClass.DEFAULT]: BuildResourceClass.IosDefault,
[UserInputResourceClass.LARGE]: BuildResourceClass.IosLarge,
},
};
export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFlags): Promise<void> {
await getVcsClient().ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
@@ -57,11 +79,12 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
projectDir,
nonInteractive: flags.nonInteractive,
});
const easJsonReader = new EasJsonReader(projectDir);
const platforms = toPlatforms(flags.requestedPlatform);
const buildProfiles = await getProfilesAsync({
type: 'build',
projectDir,
easJsonReader,
platforms,
profileName: flags.profile ?? undefined,
});
@@ -84,6 +107,10 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
flags,
moreBuilds: platforms.length > 1,
buildProfile,
resourceClass:
platformToGraphQLResourceClassMapping[buildProfile.platform][
flags.userInputResourceClass ?? UserInputResourceClass.DEFAULT
],
});
if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile });
@@ -102,7 +129,7 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) {
const submitProfiles = await getProfilesAsync({
projectDir,
easJsonReader,
platforms,
profileName: flags.submitProfile,
type: 'submit',
@@ -137,7 +164,11 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
return;
}
const builds = await waitForBuildEndAsync(startedBuilds.map(({ build }) => build.id));
const { accountName } = Object.values(buildCtxByPlatform)[0];
const builds = await waitForBuildEndAsync({
buildIds: startedBuilds.map(({ build }) => build.id),
accountName,
});
printBuildResults(builds, flags.json);
const haveAllBuildsFailedOrCanceled = builds.every(
@@ -156,14 +187,17 @@ async function prepareAndStartBuildAsync({
flags,
moreBuilds,
buildProfile,
resourceClass,
}: {
projectDir: string;
flags: BuildFlags;
moreBuilds: boolean;
buildProfile: ProfileData<'build'>;
resourceClass: BuildResourceClass;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName,
resourceClass,
clearCache: flags.clearCache,
buildProfile: buildProfile.profile,
nonInteractive: flags.nonInteractive,
+5
View File
@@ -12,3 +12,8 @@ export enum BuildDistributionType {
INTERNAL = 'internal',
SIMULATOR = 'simulator',
}
export enum UserInputResourceClass {
DEFAULT = 'default',
LARGE = 'large',
}
@@ -103,10 +103,7 @@ export function printDeprecationWarnings(deprecationInfo?: EasBuildDeprecationIn
Log.warn("Changes won't affect your project config.");
Log.warn(deprecationInfo.message);
} else if (deprecationInfo.type === EasBuildDeprecationInfoType.UserFacing) {
Log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
Log.warn(
'There might be some changes necessary to your project config, latest eas-cli will provide more specific error messages.'
);
Log.warn('This command is using API that soon will be deprecated.');
Log.warn(deprecationInfo.message);
} else {
Log.warn('An unexpected warning was encountered. Please report it as a bug:');
+13
View File
@@ -35,3 +35,16 @@ export function getInternalDistributionInstallUrl(build: BuildFragment): string
return build.artifacts.buildUrl;
}
export function getUpdateGroupUrl(
accountName: string,
projectName: string,
updateGroupId: string
): string {
return new URL(
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
projectName
)}/updates/${encodeURIComponent(updateGroupId)}`,
getExpoWebsiteBaseUrl()
).toString();
}
@@ -1,12 +1,8 @@
import { asMock } from '../../__tests__/utils';
import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClient';
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
import { getUserAsync } from '../../user/User';
import { ensureLoggedInAsync } from '../../user/actions';
import EasCommand from '../EasCommand';
import TestEasCommand from './TestEasCommand';
TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
jest.mock('../../user/User');
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
@@ -32,17 +28,30 @@ afterAll(() => {
});
beforeEach(() => {
asMock(getUserAsync)
jest
.mocked(getUserAsync)
.mockReset()
.mockImplementation(() => mockJester);
asMock(ensureLoggedInAsync)
.mockImplementation(async () => mockJester);
jest
.mocked(ensureLoggedInAsync)
.mockReset()
.mockImplementation(() => mockJester);
asMock(initAsync).mockReset();
asMock(flushAsync).mockReset();
asMock(logEvent).mockReset();
.mockImplementation(async () => mockJester);
jest.mocked(initAsync).mockReset();
jest.mocked(flushAsync).mockReset();
jest.mocked(logEvent).mockReset();
});
const createTestEasCommand = (authValue: boolean): typeof EasCommand => {
class TestEasCommand extends EasCommand {
requiresAuthentication = authValue;
async runAsync(): Promise<void> {}
}
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
return TestEasCommand;
};
describe(EasCommand.name, () => {
describe('without exceptions', () => {
// The first test in this suite should have an increased timeout
@@ -53,32 +62,36 @@ describe(EasCommand.name, () => {
// See https://github.com/oclif/command/blob/master/src/command.ts#L80
// and look for "Config.load"
it('ensures the user is logged in', async () => {
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(ensureLoggedInAsync).toHaveBeenCalled();
}, 15_000);
it('ensures the user data is read from cache', async () => {
asMock(TestEasCommand.prototype.authValue).mockImplementationOnce(() => false);
const TestEasCommand = createTestEasCommand(false);
await TestEasCommand.run();
expect(getUserAsync).toHaveReturnedWith(mockJester);
expect(jest.mocked(getUserAsync)).toBeCalledTimes(1);
expect(jest.mocked(ensureLoggedInAsync)).not.toBeCalled();
});
it('initializes analytics', async () => {
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(initAsync).toHaveBeenCalled();
});
it('flushes analytics', async () => {
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(flushAsync).toHaveBeenCalled();
});
it('logs events', async () => {
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(logEvent).toHaveBeenCalledWith('action', {
@@ -89,11 +102,12 @@ describe(EasCommand.name, () => {
describe('after exceptions', () => {
it('flushes analytics', async () => {
const TestEasCommand = createTestEasCommand(true);
try {
await TestEasCommand.run().then(() => {
throw new Error('foo');
});
} catch (error) {}
} catch {}
expect(flushAsync).toHaveBeenCalled();
});
@@ -1,13 +0,0 @@
import EasCommand from '../EasCommand';
export default class TestEasCommand extends EasCommand {
requiresAuthentication = this.authValue();
authValue(): boolean {
return false;
}
async runAsync(): Promise<void> {}
}
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
@@ -11,6 +10,7 @@ import {
UpdateBranch,
} from '../../graphql/generated';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
@@ -79,7 +79,7 @@ export default class BranchCreate extends EasCommand {
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
@@ -13,6 +12,7 @@ import {
GetBranchInfoQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
@@ -103,7 +103,7 @@ export default class BranchDelete extends EasCommand {
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
+49 -19
View File
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import CliTable from 'cli-table3';
@@ -14,26 +13,28 @@ import {
} from '../../graphql/generated';
import { UpdateBranchFragmentNode } from '../../graphql/types/UpdateBranch';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { PaginatedQueryResponse, performPaginatedQueryAsync } from '../../utils/queries';
const BRANCHES_LIMIT = 10_000;
export const BRANCHES_LIMIT = 50;
export async function listBranchesAsync({
projectId,
}: {
projectId: string;
}): Promise<UpdateBranchFragment[]> {
appId,
limit = BRANCHES_LIMIT,
offset = 0,
}: BranchesByAppQueryVariables): Promise<UpdateBranchFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<BranchesByAppQuery, BranchesByAppQueryVariables>(
gql`
query BranchesByAppQuery($appId: String!, $limit: Int!) {
query BranchesByAppQuery($appId: String!, $limit: Int!, $offset: Int!) {
app {
byId(appId: $appId) {
id
updateBranches(offset: 0, limit: $limit) {
updateBranches(offset: $offset, limit: $limit) {
id
...UpdateBranchFragment
}
@@ -43,8 +44,9 @@ export async function listBranchesAsync({
${print(UpdateBranchFragmentNode)}
`,
{
appId: projectId,
limit: BRANCHES_LIMIT,
appId,
limit,
offset,
},
{ additionalTypenames: ['UpdateBranch'] }
)
@@ -71,11 +73,30 @@ export default class BranchList extends EasCommand {
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const branches = await listBranchesAsync({ projectId });
await queryForPaginatedBranchesAsync(projectId, flags);
}
}
async function queryForPaginatedBranchesAsync(
projectId: string,
flags: { json: boolean } & { json: boolean | undefined }
): Promise<void> {
const queryAdditionalBranchesAsync = async (
pageSize: number,
offset: number
): Promise<PaginatedQueryResponse<UpdateBranchFragment>> => {
const branches = await listBranchesAsync({ appId: projectId, limit: pageSize, offset });
return {
queryResponse: branches,
queryResponseRawLength: branches.length,
};
};
const renderPageOfBranches = (currentPage: UpdateBranchFragment[]): void => {
if (flags.json) {
printJsonOnlyOutput(branches);
printJsonOnlyOutput(currentPage);
} else {
const table = new CliTable({
head: ['Branch', ...UPDATE_COLUMNS],
@@ -83,7 +104,7 @@ export default class BranchList extends EasCommand {
});
table.push(
...branches.map(branch => [
...currentPage.map(branch => [
branch.name,
formatUpdate(branch.updates[0]),
branch.updates[0]?.runtimeVersion ?? 'N/A',
@@ -96,11 +117,20 @@ export default class BranchList extends EasCommand {
);
Log.addNewLineIfNone();
Log.log(chalk.bold('Branches with their most recent update group:'));
Log.log(chalk.bold('Branches:'));
Log.addNewLineIfNone();
Log.log(table.toString());
if (branches.length >= BRANCHES_LIMIT) {
Log.warn(`Showing first ${BRANCHES_LIMIT} branches, some results might be omitted.`);
}
}
}
};
await performPaginatedQueryAsync({
pageSize: 50,
offset: 0,
queryToPerform: queryAdditionalBranchesAsync,
promptOptions: {
type: 'confirm',
title: 'Fetch next page of branches?',
renderListItems: renderPageOfBranches,
},
});
}
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
@@ -12,6 +11,7 @@ import {
UpdateBranch,
} from '../../graphql/generated';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
@@ -78,7 +78,7 @@ export default class BranchRename extends EasCommand {
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
+82 -33
View File
@@ -1,17 +1,19 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import Table from 'cli-table3';
import EasCommand from '../../commandUtils/EasCommand';
import { ViewBranchUpdatesQuery } from '../../graphql/generated';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
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';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { PaginatedQueryResponse, performPaginatedQueryAsync } from '../../utils/queries';
export default class BranchView extends EasCommand {
static description = 'view a branch';
@@ -41,7 +43,7 @@ export default class BranchView extends EasCommand {
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
if (!name) {
@@ -57,50 +59,97 @@ export default class BranchView extends EasCommand {
}));
}
await queryForPaginatedBranchesAsync(projectId, name, { json: jsonFlag });
}
}
type BranchUpdateObject = Exclude<
ViewBranchUpdatesQuery['app']['byId']['updateBranchByName'],
null | undefined
>['updates'][0];
async function queryForPaginatedBranchesAsync(
projectId: string,
name: string,
flags: { json: boolean } & { json: boolean | undefined }
): Promise<void> {
const queryAdditionalBranchesAsync = async (
pageSize: number,
offset: number
): Promise<PaginatedQueryResponse<BranchUpdateObject>> => {
const { app } = await UpdateQuery.viewBranchAsync({
appId: projectId,
name,
limit: pageSize,
offset,
});
const UpdateBranch = app?.byId.updateBranchByName;
if (!UpdateBranch) {
throw new Error(`Could not find branch "${name}"`);
}
const updates = Object.values(groupBy(UpdateBranch.updates, u => u.group)).map(
group => group[0]
const updateGroups = Object.values(groupBy(UpdateBranch.updates, u => u.group)).map(
updateGroup => ({
...updateGroup[0],
platform: getPlatformsForGroup({
updates: updateGroup,
group: updateGroup[0].group,
}),
})
);
if (jsonFlag) {
printJsonOnlyOutput({ ...UpdateBranch, updates });
return {
queryResponse: updateGroups,
queryResponseRawLength: UpdateBranch.updates.length,
};
};
const renderPageOfBranches = (currentPage: BranchUpdateObject[]): void => {
if (flags.json) {
printJsonOnlyOutput(currentPage);
} else {
const groupTable = new Table({
head: UPDATE_COLUMNS,
wordWrap: true,
});
if (flags.json) {
printJsonOnlyOutput({ currentPage });
} 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,
}),
]);
for (const update of currentPage) {
groupTable.push([
formatUpdate(update),
update.runtimeVersion,
update.group,
getPlatformsForGroup({
updates: currentPage,
group: update.group,
}),
]);
}
Log.addNewLineIfNone();
Log.log(chalk.bold('Branch:'));
Log.log(
formatFields([
{ label: 'Name', value: currentPage[0]['branch']['name'] },
{ label: 'ID', value: currentPage[0]['branch']['id'] },
])
);
Log.addNewLineIfNone();
Log.log(groupTable.toString());
}
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());
}
}
};
await performPaginatedQueryAsync({
pageSize: 50,
offset: 0,
queryToPerform: queryAdditionalBranchesAsync,
promptOptions: {
type: 'confirm',
title: 'Fetch next page of branches?',
renderListItems: renderPageOfBranches,
},
});
}
+74 -47
View File
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import chalk from 'chalk';
import gql from 'graphql-tag';
@@ -6,6 +5,7 @@ import EasCommand from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
Build,
BuildFragment,
BuildStatus,
CancelBuildMutation,
CancelBuildMutationVariables,
@@ -14,12 +14,14 @@ import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import { ora } from '../../ora';
import { appPlatformEmojis } from '../../platform';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { confirmAsync, selectAsync } from '../../prompts';
import { confirmAsync } from '../../prompts';
import { PaginatedQueryResponse, performPaginatedQueryAsync } from '../../utils/queries';
async function cancelBuildAsync(buildId: string): Promise<Pick<Build, 'id' | 'status'>> {
const data = await withErrorHandlingAsync(
@@ -59,52 +61,10 @@ function formatUnfinishedBuild(
return `${platform} Started at: ${startTime}, Status: ${status}, Id: ${build.id}`;
}
async function selectBuildToCancelAsync(
projectId: string,
projectFullName: string
): Promise<string | null> {
const spinner = ora().start('Fetching the uncompleted builds…');
let builds;
try {
const [newBuilds, inQueueBuilds, inProgressBuilds] = await Promise.all([
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.New } }),
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.InQueue } }),
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.InProgress } }),
]);
spinner.stop();
builds = [...newBuilds, ...inQueueBuilds, ...inProgressBuilds];
} catch (error) {
spinner.fail(
`Something went wrong and we couldn't fetch the builds for the project ${projectFullName}.`
);
throw error;
}
if (builds.length === 0) {
Log.warn(`There aren't any uncompleted builds for the project ${projectFullName}.`);
return null;
} else if (builds.length === 1) {
Log.log('Found one build');
Log.log(formatUnfinishedBuild(builds[0]));
await confirmAsync({
message: 'Do you want to cancel it?',
});
return builds[0].id;
} else {
const buildId = await selectAsync<string>(
'Which build do you want to cancel?',
builds.map(build => ({
title: formatUnfinishedBuild(build),
value: build.id,
}))
);
return buildId;
}
}
async function ensureBuildExistsAsync(buildId: string): Promise<void> {
try {
await BuildQuery.byIdAsync(buildId);
} catch (err) {
} catch {
throw new Error(`Couldn't find a build matching the id ${buildId}`);
}
}
@@ -118,7 +78,7 @@ export default class BuildCancel extends EasCommand {
const { BUILD_ID: buildIdFromArg } = (await this.parse(BuildCancel)).args;
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectFullName = await getProjectFullNameAsync(exp);
@@ -126,7 +86,8 @@ export default class BuildCancel extends EasCommand {
await ensureBuildExistsAsync(buildIdFromArg);
}
const buildId = buildIdFromArg || (await selectBuildToCancelAsync(projectId, projectFullName));
const buildId =
buildIdFromArg || (await queryForBuildToCancelAsync(projectId, projectFullName));
if (!buildId) {
return;
}
@@ -146,3 +107,69 @@ export default class BuildCancel extends EasCommand {
}
}
}
async function queryForBuildToCancelAsync(
projectId: string,
projectFullName: string
): Promise<string | null> {
const queryToPerformAsync = async (
pageSize: number,
offset: number
): Promise<PaginatedQueryResponse<BuildFragment>> => {
const [newBuilds, inQueueBuilds, inProgressBuilds] = await Promise.all([
BuildQuery.allForAppAsync(projectId, {
limit: pageSize,
offset,
filter: { status: BuildStatus.New },
}),
BuildQuery.allForAppAsync(projectId, {
limit: pageSize,
offset,
filter: { status: BuildStatus.InQueue },
}),
BuildQuery.allForAppAsync(projectId, {
limit: pageSize,
offset,
filter: { status: BuildStatus.InProgress },
}),
]);
// this variable controls whether performPaginatedQueryAsync will query the next page.
// force another query if we have hit the query limit for any of these 3 queries.
const queryResponseLength =
newBuilds.length < pageSize &&
inQueueBuilds.length < pageSize &&
inProgressBuilds.length < pageSize
? pageSize - 1
: pageSize;
return {
queryResponse: [...newBuilds, ...inQueueBuilds, ...inProgressBuilds],
queryResponseRawLength: queryResponseLength,
};
};
const getIdentifierForQueryItem = (buildFragment: BuildFragment): string => buildFragment.id;
const selectedBuild = (
await performPaginatedQueryAsync({
pageSize: 50,
offset: 0,
queryToPerform: queryToPerformAsync,
promptOptions: {
type: 'select',
title: 'Which build would you like to cancel?',
getIdentifierForQueryItem,
createDisplayTextForSelectionPromptListItem: formatUnfinishedBuild,
},
})
).pop();
if (!selectedBuild) {
Log.warn(`There aren't any uncompleted builds for the project ${projectFullName}.`);
return null;
}
const userConfirmedCancellation = await confirmAsync({
message: `Are you sure you want to cancel ${formatUnfinishedBuild(selectedBuild)}?`,
});
return userConfirmedCancellation ? selectedBuild.id : null;
}
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -8,6 +7,7 @@ import { ensureProjectConfiguredAsync } from '../../build/configure';
import EasCommand from '../../commandUtils/EasCommand';
import Log, { learnMore } from '../../log';
import { RequestedPlatform } from '../../platform';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
@@ -55,7 +55,7 @@ export default class BuildConfigure extends EasCommand {
// configure expo-updates
if (expoUpdatesIsInstalled) {
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
+9 -75
View File
@@ -1,21 +1,13 @@
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 path from 'path';
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import { ensureRepoIsCleanAsync, reviewAndCommitChangesAsync } from '../../build/utils/repository';
import { UserInputResourceClass } from '../../build/types';
import EasCommand from '../../commandUtils/EasCommand';
import Log, { learnMore, link } from '../../log';
import Log from '../../log';
import { RequestedPlatform, selectRequestedPlatformAsync } from '../../platform';
import { findProjectRootAsync } from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
import { easCliVersion } from '../../utils/easCli';
import { enableJsonOutput } from '../../utils/json';
import { getVcsClient, setVcsClient } from '../../vcs';
import GitClient from '../../vcs/clients/git';
interface RawBuildFlags {
platform?: string;
@@ -30,6 +22,7 @@ interface RawBuildFlags {
json: boolean;
'auto-submit': boolean;
'auto-submit-with-profile'?: string;
'resource-class'?: UserInputResourceClass;
}
export default class Build extends EasCommand {
@@ -88,6 +81,11 @@ export default class Build extends EasCommand {
helpValue: 'PROFILE_NAME',
exclusive: ['auto-submit'],
}),
'resource-class': Flags.enum({
options: Object.values(UserInputResourceClass),
hidden: true,
description: 'The instance type that will be used to run this build [experimental]',
}),
};
async runAsync(): Promise<void> {
@@ -98,7 +96,6 @@ export default class Build extends EasCommand {
const flags = await this.sanitizeFlagsAsync(rawFlags);
const projectDir = await findProjectRootAsync();
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await runBuildAndSubmitAsync(projectDir, flags);
}
@@ -165,70 +162,7 @@ export default class Build extends EasCommand {
json: flags['json'],
autoSubmit: flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined,
submitProfile: flags['auto-submit-with-profile'] ?? profile,
userInputResourceClass: flags['resource-class'] ?? UserInputResourceClass.DEFAULT,
};
}
}
export async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
if (!(await fs.pathExists(easJsonPath))) {
return;
}
const easJsonReader = new EasJsonReader(projectDir);
const rawEasJson = await easJsonReader.readAsync();
if (rawEasJson?.cli) {
return;
}
if (nonInteractive) {
Log.warn(
`${
figures.warning
} Action required: the default behavior of EAS CLI has changed and your eas.json must be updated to remove ambiguity around which Git integration workflow to use. Refer to ${link(
'https://expo.fyi/eas-vcs-workflow'
)} for more information.`
);
Log.warn(
'This warning will become an error in an upcoming EAS CLI release. For now, we will proceed with the old default behavior to avoid disruption of your builds.'
);
setVcsClient(new GitClient());
return;
}
Log.log(
`${chalk.bold(
'eas-cli@>=0.34.0 no longer requires that you commit changes to Git before starting a build.'
)} ${learnMore('https://expo.fyi/eas-vcs-workflow')}`
);
Log.log(
`If you want to continue using the Git integration, you can opt in with ${chalk.bold(
'cli.requireCommit'
)} in ${chalk.bold('eas.json')} or with the following prompt.`
);
Log.newLine();
const mode = await selectAsync('Select your preferred Git integration', [
{ title: 'Require changes to be committed in Git (old default)', value: 'requireCommit' },
{ title: 'Allow builds with dirty Git working tree (new default)', value: 'noCommit' },
]);
if (mode === 'requireCommit') {
setVcsClient(new GitClient());
await ensureRepoIsCleanAsync(nonInteractive);
}
rawEasJson.cli =
mode === 'requireCommit'
? { version: `>= ${easCliVersion}`, requireCommit: true }
: { version: `>= ${easCliVersion}` };
await fs.writeJSON(easJsonPath, rawEasJson, { spaces: 2 });
Log.withTick('Updated eas.json');
if (mode === 'requireCommit') {
await getVcsClient().trackFileAsync(easJsonPath);
await reviewAndCommitChangesAsync('Set cli.requireCommit to true in eas.json', {
nonInteractive,
});
}
}
@@ -102,7 +102,7 @@ export default class BuildInspect extends EasCommand {
if (!flags.verbose) {
Log.log(chalk.green('Build successful'));
}
} catch (err) {
} catch {
if (!flags.verbose) {
Log.error('Build failed');
Log.error(`Re-run this command with ${chalk.bold('--verbose')} flag to see the logs`);
+77 -51
View File
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -7,19 +6,21 @@ import { formatGraphQLBuild } from '../../build/utils/formatBuild';
import EasCommand from '../../commandUtils/EasCommand';
import {
AppPlatform,
BuildFragment,
DistributionType,
BuildStatus as GraphQLBuildStatus,
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { BuildQuery, BuildsQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { PaginatedQueryResponse, performPaginatedQueryAsync } from '../../utils/queries';
export default class BuildList extends EasCommand {
static description = 'list all builds for your project';
@@ -56,7 +57,6 @@ export default class BuildList extends EasCommand {
appIdentifier: Flags.string(),
buildProfile: Flags.string(),
gitCommitHash: Flags.string(),
limit: Flags.integer(),
};
async runAsync(): Promise<void> {
@@ -66,7 +66,6 @@ export default class BuildList extends EasCommand {
platform: requestedPlatform,
status: buildStatus,
distribution: buildDistribution,
limit = 10,
} = flags;
if (json) {
enableJsonOutput();
@@ -77,55 +76,28 @@ export default class BuildList extends EasCommand {
const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectName = await getProjectFullNameAsync(exp);
const spinner = ora().start('Fetching the build list for the project…');
try {
const builds = await BuildQuery.allForAppAsync(projectId, {
limit,
filter: {
platform,
status: graphqlBuildStatus,
distribution: graphqlBuildDistribution,
channel: flags.channel,
appVersion: flags.appVersion,
appBuildVersion: flags.appBuildVersion,
sdkVersion: flags.sdkVersion,
runtimeVersion: flags.runtimeVersion,
appIdentifier: flags.appIdentifier,
buildProfile: flags.buildProfile,
gitCommitHash: flags.gitCommitHash,
},
});
if (builds.length) {
if (platform || graphqlBuildStatus) {
spinner.succeed(
`Showing ${builds.length} matching builds for the project ${projectName}`
);
} else {
spinner.succeed(`Showing last ${builds.length} builds for the project ${projectName}`);
}
if (json) {
printJsonOnlyOutput(builds);
} else {
const list = builds
.map(build => formatGraphQLBuild(build))
.join(`\n\n${chalk.dim('———')}\n\n`);
Log.log(`\n${list}`);
}
} else {
spinner.fail(`Couldn't find any builds for the project ${projectName}`);
}
} catch (e) {
spinner.fail(`Something went wrong and we couldn't fetch the build list ${projectName}`);
throw e;
}
await queryForPaginatedBuildsAsync(
projectId,
projectName,
{
platform,
status: graphqlBuildStatus,
distribution: graphqlBuildDistribution,
channel: flags.channel,
appVersion: flags.appVersion,
appBuildVersion: flags.appBuildVersion,
sdkVersion: flags.sdkVersion,
runtimeVersion: flags.runtimeVersion,
appIdentifier: flags.appIdentifier,
buildProfile: flags.buildProfile,
gitCommitHash: flags.gitCommitHash,
},
flags
);
}
}
@@ -170,3 +142,57 @@ const toGraphQLBuildDistribution = (
return undefined;
}
};
async function queryForPaginatedBuildsAsync(
projectId: string,
projectName: string,
filter: BuildsQuery['filter'],
flags: { json: boolean } & { json: boolean | undefined }
): Promise<void> {
const queryAdditionalBuildsAsync = async (
pageSize: number,
offset: number
): Promise<PaginatedQueryResponse<BuildFragment>> => {
const builds = await BuildQuery.allForAppAsync(projectId, {
limit: pageSize,
offset,
filter,
});
return {
queryResponse: builds,
queryResponseRawLength: builds.length,
};
};
const renderPageOfBuilds = (currentPage: BuildFragment[]): void => {
if (flags.json) {
printJsonOnlyOutput(currentPage);
} else {
if (currentPage.length) {
const list = currentPage
.map(build => formatGraphQLBuild(build))
.join(`\n\n${chalk.dim('———')}\n\n`);
Log.addNewLineIfNone();
Log.withTick(chalk.bold(`Builds for ${projectName}:`));
Log.log(`\n${list}`);
Log.addNewLineIfNone();
} else {
Log.withCross(chalk.bold(`Couldn't find any builds for the project ${projectName}`));
Log.addNewLineIfNone();
}
}
};
await performPaginatedQueryAsync({
pageSize: 50,
offset: 0,
queryToPerform: queryAdditionalBuildsAsync,
promptOptions: {
type: 'confirm',
title: 'Fetch next page of builds?',
renderListItems: renderPageOfBuilds,
},
});
}
+2 -2
View File
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
@@ -7,6 +6,7 @@ import { BuildFragment } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
@@ -35,7 +35,7 @@ export default class BuildView extends EasCommand {
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectName = await getProjectFullNameAsync(exp);
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
@@ -11,6 +10,7 @@ import {
} from '../../graphql/generated';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
@@ -88,7 +88,7 @@ export default class ChannelCreate extends EasCommand {
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
if (!channelName) {
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import gql from 'graphql-tag';
@@ -12,6 +11,7 @@ import {
GetChannelInfoQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
@@ -55,7 +55,7 @@ export default class ChannelDelete extends EasCommand {
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
@@ -15,6 +14,7 @@ import {
} from '../../graphql/generated';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
@@ -120,7 +120,7 @@ export default class ChannelEdit extends EasCommand {
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const channelName = args.name ?? (await promptForChannelAsync());
+102 -21
View File
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
@@ -9,34 +9,45 @@ import {
GetAllChannelsForAppQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { PaginatedQueryResponse, performPaginatedQueryAsync } from '../../utils/queries';
import { BRANCHES_LIMIT } from '../branch/list';
import { logChannelDetails } from './view';
const CHANNEL_LIMIT = 10_000;
const CHANNEL_LIMIT = 50;
async function getAllUpdateChannelForAppAsync({
appId,
}: {
appId: string;
}): Promise<GetAllChannelsForAppQuery> {
channelLimit = CHANNEL_LIMIT,
channelOffset = 0,
branchLimit = BRANCHES_LIMIT,
branchOffset = 0,
}: GetAllChannelsForAppQueryVariables): Promise<GetAllChannelsForAppQuery> {
return await withErrorHandlingAsync(
graphqlClient
.query<GetAllChannelsForAppQuery, GetAllChannelsForAppQueryVariables>(
gql`
query GetAllChannelsForApp($appId: String!, $offset: Int!, $limit: Int!) {
query GetAllChannelsForApp(
$appId: String!
$channelOffset: Int!
$channelLimit: Int!
$branchLimit: Int!
$branchOffset: Int!
) {
app {
byId(appId: $appId) {
id
updateChannels(offset: $offset, limit: $limit) {
updateChannels(offset: $channelOffset, limit: $channelLimit) {
id
name
branchMapping
updateBranches(offset: 0, limit: $limit) {
updateBranches(offset: $branchOffset, limit: $branchLimit) {
id
name
updates(offset: 0, limit: 10) {
updates(offset: 0, limit: 1) {
id
group
message
@@ -59,7 +70,7 @@ async function getAllUpdateChannelForAppAsync({
}
}
`,
{ appId, offset: 0, limit: CHANNEL_LIMIT },
{ appId, channelOffset, channelLimit, branchLimit, branchOffset },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
)
.toPromise()
@@ -85,21 +96,80 @@ export default class ChannelList extends EasCommand {
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const getAllUpdateChannelForAppResult = await getAllUpdateChannelForAppAsync({
appId: projectId,
});
const channels = getAllUpdateChannelForAppResult.app?.byId.updateChannels;
if (!channels) {
throw new Error(`Could not find channels on project with id ${projectId}`);
}
await queryForChannelsAsync(projectId, jsonFlag);
}
}
type UpdateChannelObject = GetAllChannelsForAppQuery['app']['byId']['updateChannels'][0];
async function queryForChannelsAsync(projectId: string, jsonFlag: boolean): Promise<void> {
const queryToPerformAsync = async (
pageSize: number,
offset: number
): Promise<PaginatedQueryResponse<UpdateChannelObject>> => {
let queriedAllBranches = false;
let branchOffset = 0;
const branchLimit = 10;
const paginatedQuery: PaginatedQueryResponse<UpdateChannelObject> = {
queryResponse: [],
queryResponseRawLength: 0,
};
const branchesForChannels: Record<string, UpdateChannelObject['updateBranches']> = {};
// we need to paginate our paginated query. Each channel (N) can have M branches
// and we need to fetch all branches to populate our 'branch mapping' portion
// of the UI properly.
do {
const getAllUpdateChannelForAppResult = await getAllUpdateChannelForAppAsync({
appId: projectId,
channelLimit: pageSize,
channelOffset: offset,
branchLimit,
branchOffset,
});
const channels = getAllUpdateChannelForAppResult.app?.byId.updateChannels;
if (!channels) {
throw new Error(`Could not find channels on project with id ${projectId}`);
}
queriedAllBranches = true;
// save each array of branches using a record so we can lookup in O(1) later on
channels.forEach(channel => {
if (channel.updateBranches.length === branchLimit) {
queriedAllBranches = false;
}
branchesForChannels[channel.id] = [
...(branchesForChannels[channel.id] ? branchesForChannels[channel.id] : []),
...channel.updateBranches,
];
});
if (!paginatedQuery.queryResponse.length) {
paginatedQuery.queryResponse = channels;
} else {
paginatedQuery.queryResponse.forEach(channel => {
channel.updateBranches = branchesForChannels[channel.id];
});
}
paginatedQuery.queryResponseRawLength = channels.length;
branchOffset += branchLimit + 1;
} while (!queriedAllBranches);
return paginatedQuery;
};
const renderListItems = (currentPage: UpdateChannelObject[]): void => {
if (jsonFlag) {
printJsonOnlyOutput(channels);
printJsonOnlyOutput(currentPage);
} else {
for (const channel of channels) {
Log.addNewLineIfNone();
Log.log(chalk.bold('Channels:'));
Log.addNewLineIfNone();
for (const channel of currentPage) {
Log.addNewLineIfNone();
Log.log(
formatFields([
@@ -110,5 +180,16 @@ export default class ChannelList extends EasCommand {
logChannelDetails(channel);
}
}
}
};
await performPaginatedQueryAsync({
pageSize: 25, // lower, because we need to query all branches per channel O(N*M)
offset: 0,
queryToPerform: queryToPerformAsync,
promptOptions: {
type: 'confirm',
title: 'Fetch next page of channels?',
renderListItems,
},
});
}
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -7,6 +6,7 @@ import { GetChannelByNameForAppQuery, UpdateBranch } from '../../graphql/generat
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
@@ -344,13 +344,15 @@ export default class ChannelRollout extends EasCommand {
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
const channel = await ChannelQuery.getUpdateChannelByNameForAppAsync({
appId: projectId,
channelName: channelName!,
offset: 0,
limit: 50,
});
if (!channel) {
throw new Error(
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
@@ -7,6 +6,7 @@ import Table from 'cli-table3';
import EasCommand from '../../commandUtils/EasCommand';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import {
@@ -45,7 +45,7 @@ export function getBranchMapping(branchMappingString?: string): {
let branchMapping: BranchMapping;
try {
branchMapping = JSON.parse(branchMappingString);
} catch (e) {
} catch {
throw new Error(`Could not parse branchMapping string into a JSON: "${branchMappingString}"`);
}
assert(branchMapping, 'Branch Mapping must be defined.');
@@ -111,7 +111,7 @@ export function logChannelDetails(channel: {
}
const { branchId } = branchMapping.data[index];
const branch = channel.updateBranches.filter(branch => branch.id === branchId)[0];
const branch = channel.updateBranches.find(branch => branch.id === branchId);
if (!branch) {
throw new Error('Branch mapping is pointing at a missing branch.');
}
@@ -165,7 +165,7 @@ export default class ChannelView extends EasCommand {
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
if (!channelName) {
@@ -184,6 +184,8 @@ export default class ChannelView extends EasCommand {
const channel = await ChannelQuery.getUpdateChannelByNameForAppAsync({
appId: projectId,
channelName,
offset: 0,
limit: 50,
});
if (!channel) {
throw new Error(`Could not find a channel with name: ${channelName}`);
-2
View File
@@ -11,7 +11,6 @@ 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 = 'display project configuration (app.json + eas.json)';
@@ -31,7 +30,6 @@ export default class Config extends EasCommand {
};
const projectDir = await findProjectRootAsync();
await handleDeprecatedEasJsonAsync(projectDir, false);
const reader = new EasJsonReader(projectDir);
const profileName =
@@ -1,5 +1,4 @@
import { Device, DeviceStatus } from '@expo/apple-utils';
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import assert from 'assert';
@@ -17,6 +16,7 @@ import formatDevice from '../../devices/utils/formatDevice';
import { AppleDevice, Maybe } from '../../graphql/generated';
import Log from '../../log';
import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
@@ -34,7 +34,7 @@ export default class DeviceDelete extends EasCommand {
} = await this.parse(DeviceDelete);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const accountName = await getProjectAccountNameAsync(exp);
if (!appleTeamIdentifier) {
+2 -2
View File
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
@@ -9,6 +8,7 @@ import { AppleTeamQuery } from '../../credentials/ios/api/graphql/queries/AppleT
import formatDevice from '../../devices/utils/formatDevice';
import Log from '../../log';
import { Ora, ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
@@ -23,7 +23,7 @@ export default class BuildList extends EasCommand {
let appleTeamIdentifier = (await this.parse(BuildList)).flags['apple-team-id'];
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const accountName = await getProjectAccountNameAsync(exp);
let spinner: Ora;
+2 -3
View File
@@ -1,10 +1,9 @@
import { getConfig } from '@expo/config';
import EasCommand from '../../commandUtils/EasCommand';
import { AppleDeviceQuery } from '../../credentials/ios/api/graphql/queries/AppleDeviceQuery';
import formatDevice from '../../devices/utils/formatDevice';
import Log from '../../log';
import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
export default class DeviceView extends EasCommand {
@@ -30,7 +29,7 @@ If you are not sure what is the UDID of the device you are looking for, run:
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const accountName = await getProjectAccountNameAsync(exp);
const spinner = ora().start(`Fetching device details for ${UDID}…`);
@@ -0,0 +1,64 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import path from 'path';
import { ensureProjectConfiguredAsync } from '../../build/configure';
import EasCommand from '../../commandUtils/EasCommand';
import { CredentialsContext } from '../../credentials/context';
import Log, { learnMore } from '../../log';
import { createMetadataContextAsync } from '../../metadata/context';
import { downloadMetadataAsync } from '../../metadata/download';
import { handleMetadataError } from '../../metadata/errors';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
export default class MetadataPull extends EasCommand {
static description = 'generate the local store configuration from the app stores';
static flags = {
profile: Flags.string({
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
};
async runAsync(): Promise<void> {
Log.warn('EAS Metadata is in beta and subject to breaking changes.');
const { flags } = await this.parse(MetadataPull);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
await getProjectIdAsync(exp);
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
const credentialsCtx = new CredentialsContext({
exp,
projectDir,
user: await ensureLoggedInAsync(),
nonInteractive: false,
});
const metadataCtx = await createMetadataContextAsync({
credentialsCtx,
projectDir,
exp,
profileName: flags.profile,
});
try {
const filePath = await downloadMetadataAsync(metadataCtx);
const relativePath = path.relative(process.cwd(), filePath);
Log.addNewLineIfNone();
Log.log(`🎉 Your store configuration is ready.
- Update the ${chalk.bold(relativePath)} file to prepare the app information.
- Run ${chalk.bold('eas submit')} or manually upload a new app version to the app stores.
- Once the app is uploaded, run ${chalk.bold('eas metadata:push')} to sync the store configuration.
- ${learnMore('https://docs.expo.dev/eas-metadata/introduction/')}`);
} catch (error: any) {
handleMetadataError(error);
}
}
}
@@ -0,0 +1,57 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import { ensureProjectConfiguredAsync } from '../../build/configure';
import EasCommand from '../../commandUtils/EasCommand';
import { CredentialsContext } from '../../credentials/context';
import Log, { learnMore } from '../../log';
import { createMetadataContextAsync } from '../../metadata/context';
import { handleMetadataError } from '../../metadata/errors';
import { uploadMetadataAsync } from '../../metadata/upload';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
export default class MetadataPush extends EasCommand {
static description = 'sync the local store configuration to the app stores';
static flags = {
profile: Flags.string({
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
};
async runAsync(): Promise<void> {
Log.warn('EAS Metadata is in beta and subject to breaking changes.');
const { flags } = await this.parse(MetadataPush);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
await getProjectIdAsync(exp);
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
const credentialsCtx = new CredentialsContext({
exp,
projectDir,
user: await ensureLoggedInAsync(),
nonInteractive: false,
});
const metadataCtx = await createMetadataContextAsync({
credentialsCtx,
projectDir,
exp,
profileName: flags.profile,
});
try {
const { appleLink } = await uploadMetadataAsync(metadataCtx);
Log.addNewLineIfNone();
Log.log(`🎉 Store configuration is synced with the app stores.
${learnMore(appleLink, { learnMoreMessage: 'See the changes in App Store Connect' })}`);
} catch (error: any) {
handleMetadataError(error);
}
}
}
@@ -1,10 +1,10 @@
import { getConfig } from '@expo/config';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { AppInfoQuery, AppInfoQueryVariables } from '../../graphql/generated';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import formatFields from '../../utils/formatFields';
@@ -36,7 +36,7 @@ export default class ProjectInfo extends EasCommand {
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const { app } = await projectInfoByIdAsync(projectId);
if (!app) {
@@ -1,8 +1,8 @@
import { getConfig } from '@expo/config';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, setProjectIdAsync } from '../../project/projectUtils';
export default class ProjectInit extends EasCommand {
@@ -11,7 +11,7 @@ export default class ProjectInit extends EasCommand {
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
if (exp.extra?.eas?.projectId) {
Log.error(
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -9,6 +8,7 @@ import {
EnvironmentSecretsQuery,
} from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
@@ -47,7 +47,7 @@ export default class EnvironmentSecretCreate extends EasCommand {
} = await this.parse(EnvironmentSecretCreate);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const accountName = await getProjectAccountNameAsync(exp);
const { slug } = exp;
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
@@ -9,6 +8,7 @@ import {
EnvironmentSecretsQuery,
} from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
@@ -27,7 +27,7 @@ export default class EnvironmentSecretDelete extends EasCommand {
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectAccountName = await getProjectAccountNameAsync(exp);
+2 -2
View File
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import chalk from 'chalk';
import Table from 'cli-table3';
import dateFormat from 'dateformat';
@@ -6,6 +5,7 @@ import dateFormat from 'dateformat';
import EasCommand from '../../commandUtils/EasCommand';
import { EnvironmentSecretsQuery } from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
@@ -17,7 +17,7 @@ export default class EnvironmentSecretList extends EasCommand {
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectAccountName = await getProjectAccountNameAsync(exp);
+4 -3
View File
@@ -1,4 +1,4 @@
import { getConfig } from '@expo/config';
import { EasJsonReader } from '@expo/eas-json';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -13,6 +13,7 @@ import {
selectRequestedPlatformAsync,
toPlatforms,
} from '../platform';
import { getExpoConfig } from '../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
import { SubmitArchiveFlags, createSubmissionContextAsync } from '../submit/context';
import { submitAsync, waitToCompleteAsync } from '../submit/submit';
@@ -89,13 +90,13 @@ export default class Submit extends EasCommand {
const flags = await this.sanitizeFlagsAsync(rawFlags);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const platforms = toPlatforms(flags.requestedPlatform);
const submissionProfiles = await getProfilesAsync({
type: 'submit',
projectDir,
easJsonReader: new EasJsonReader(projectDir),
platforms,
profileName: flags.profile,
});
@@ -1,4 +1,4 @@
import { ExpoConfig, getConfig, modifyConfigAsync } from '@expo/config';
import { ExpoConfig, modifyConfigAsync } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Flags } from '@oclif/core';
import assert from 'assert';
@@ -9,6 +9,7 @@ import EasCommand from '../../commandUtils/EasCommand';
import { AppPlatform } from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import { RequestedPlatform, appPlatformDisplayNames } from '../../platform';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectIdAsync,
@@ -41,9 +42,7 @@ export default class UpdateConfigure extends EasCommand {
const { flags } = await this.parse(UpdateConfigure);
const platform = flags.platform as RequestedPlatform;
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
});
const exp = getExpoConfig(projectDir);
if (!isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion)) {
await installExpoUpdatesAsync(projectDir);
+225 -91
View File
@@ -1,4 +1,4 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { ExpoConfig } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Errors, Flags } from '@oclif/core';
@@ -9,6 +9,7 @@ import gql from 'graphql-tag';
import nullthrows from 'nullthrows';
import { getEASUpdateURL } from '../../api';
import { getUpdateGroupUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import fetch from '../../fetch';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
@@ -18,6 +19,7 @@ import {
Robot,
RootQueryUpdatesByGroupArgs,
Update,
UpdateBranchFragment,
UpdateInfoGroup,
UpdatePublishMutation,
User,
@@ -25,10 +27,12 @@ import {
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log from '../../log';
import Log, { link } from '../../log';
import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectAccountName,
getProjectIdAsync,
installExpoUpdatesAsync,
isExpoUpdatesInstalledOrAvailable,
@@ -41,24 +45,25 @@ import {
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
import { confirmAsync, promptAsync } from '../../prompts';
import { formatUpdate } from '../../update/utils';
import { ensureLoggedInAsync } from '../../user/actions';
import {
checkManifestBodyAgainstUpdateInfoGroup,
getCodeSigningInfoAsync,
getManifestBodyAsync,
signManifestBody,
} from '../../utils/code-signing';
import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { PaginatedQueryResponse, performPaginatedQueryAsync } from '../../utils/queries';
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';
export type PublishPlatformFlag = PublishPlatform | 'all';
async function getUpdateGroupAsync({
group,
@@ -116,12 +121,16 @@ async function ensureChannelExistsAsync({
}
}
async function ensureBranchExistsAsync({
export async function ensureBranchExistsAsync({
appId,
name: branchName,
limit,
offset,
}: {
appId: string;
name: string;
limit: number;
offset: number;
}): Promise<{
id: string;
updates: Exclude<
@@ -132,6 +141,8 @@ async function ensureBranchExistsAsync({
const { app } = await UpdateQuery.viewBranchAsync({
appId,
name: branchName,
limit,
offset,
});
const updateBranch = app?.byId.updateBranchByName;
if (updateBranch) {
@@ -194,6 +205,10 @@ export default class UpdatePublish extends EasCommand {
description: `File containing the PEM-encoded private key corresponding to the certificate in expo-updates' configuration. Defaults to a file named "private-key.pem" in the certificate's directory.`,
required: false,
}),
'non-interactive': Flags.boolean({
default: false,
description: 'Run command in non-interactive mode',
}),
};
async runAsync(): Promise<void> {
@@ -209,25 +224,39 @@ export default class UpdatePublish extends EasCommand {
'skip-bundler': skipBundler,
platform,
'private-key-path': privateKeyPath,
'non-interactive': nonInteractive,
},
} = await this.parse(UpdatePublish);
if (jsonFlag) {
enableJsonOutput();
}
const platformFlag = platform as PlatformFlag;
const platformFlag = platform as PublishPlatformFlag;
// 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,
const exp = getExpoConfig(projectDir, {
isPublicConfig: true,
});
const codeSigningInfo = await getCodeSigningInfoAsync(exp, privateKeyPath);
const expPrivate = getExpoConfig(projectDir, {
isPublicConfig: false,
});
if (!isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion)) {
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion);
if (!hasExpoUpdates && nonInteractive) {
Errors.error(
`${chalk.bold(
'expo-updates'
)} must already be installed when executing in non-interactive mode`,
{ exit: 1 }
);
}
if (!hasExpoUpdates) {
const install = await confirmAsync({
message: `You are creating an update which requires ${chalk.bold(
'expo-updates'
@@ -255,44 +284,21 @@ export default class UpdatePublish extends EasCommand {
if (!branchName) {
const validationMessage = 'Branch name may not be empty.';
if (nonInteractive) {
throw new Error('Must supply --branch or use --auto when in non-interactive mode');
}
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.');
branchName = await getBranchNameToPublishOnAsync(projectId, validationMessage);
}
const { id: branchId, updates } = await ensureBranchExistsAsync({
appId: projectId,
name: branchName,
});
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
let oldMessage: string, oldRuntimeVersion: string;
let uploadedAssetCount = 0;
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.
@@ -303,31 +309,16 @@ export default class UpdatePublish extends EasCommand {
if (group) {
updatesToRepublish = await getUpdateGroupAsync({ group });
} else {
if (nonInteractive) {
throw new Error('Must supply --group when in non-interactive mode');
}
// 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);
updatesToRepublish = await getUpdatesToRepublishAsync(projectId, branchName, platformFlag);
}
const updatesToRepublishFilteredByPlatform = updatesToRepublish.filter(
// Only republish to the specified platforms
@@ -335,7 +326,7 @@ export default class UpdatePublish extends EasCommand {
);
if (updatesToRepublishFilteredByPlatform.length === 0) {
throw new Error(
`There are no updates on branch "${branchName}" published on the platform(s) "${platformFlag}" with group ID "${
`There are no updates on branch "${branchName}" published for the platform(s) "${platformFlag}" with group ID "${
group ? group : updatesToRepublish[0].group
}". Did you mean to publish a new update instead?`
);
@@ -367,6 +358,10 @@ export default class UpdatePublish extends EasCommand {
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
if (!message) {
if (nonInteractive) {
throw new Error('Must supply --message when in non-interactive mode');
}
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
@@ -385,6 +380,9 @@ export default class UpdatePublish extends EasCommand {
}
if (!message) {
if (nonInteractive) {
throw new Error('Must supply --message or use --auto when in non-interactive mode');
}
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
@@ -414,15 +412,24 @@ export default class UpdatePublish extends EasCommand {
try {
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
const assets = await collectAssetsAsync({ inputDir: inputDir!, platforms });
await uploadAssetsAsync(assets);
const { uniqueUploadedAssetCount } = await uploadAssetsAsync(
assets,
spinnerText => (assetSpinner.text = `Uploading assets... ${spinnerText}`)
);
uploadedAssetCount = uniqueUploadedAssetCount;
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
assetSpinner.succeed('Uploaded assets!');
const uploadAssetSuccessMessage = uploadedAssetCount
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'asset' : 'assets'}!`
: `Uploading assets skipped -- no new assets found!`;
assetSpinner.succeed(uploadAssetSuccessMessage);
} catch (e) {
assetSpinner.fail('Failed to upload assets');
throw e;
}
}
const truncatedMessage = truncatePublishUpdateMessage(message!);
const runtimeToPlatformMapping: Record<string, string[]> = {};
for (const runtime of new Set(Object.values(runtimeVersions))) {
runtimeToPlatformMapping[runtime] = Object.entries(runtimeVersions)
@@ -430,6 +437,13 @@ export default class UpdatePublish extends EasCommand {
.map(pair => pair[0]);
}
const { id: branchId } = await ensureBranchExistsAsync({
appId: projectId,
name: branchName,
limit: 0,
offset: 0,
});
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups: PublishUpdateGroupInput[] = Object.entries(runtimeToPlatformMapping).map(
([runtime, platforms]) => {
@@ -449,7 +463,7 @@ export default class UpdatePublish extends EasCommand {
branchId,
updateInfoGroup: localUpdateInfoGroup,
runtimeVersion: republish ? oldRuntimeVersion : runtime,
message,
message: truncatedMessage,
awaitingCodeSigningInfo: !!codeSigningInfo,
};
}
@@ -519,20 +533,36 @@ export default class UpdatePublish extends EasCommand {
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) {
const newUpdatesForRuntimeVersion = newUpdates.filter(
update => update.runtimeVersion === runtime
);
if (!newUpdatesForRuntimeVersion.length) {
throw new Error(`Publish response is missing updates with runtime ${runtime}.`);
}
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
update => update.platform === 'android'
);
const newIosUpdate = newUpdatesForRuntimeVersion.find(update => update.platform === 'ios');
const updateGroupId = newUpdatesForRuntimeVersion[0].group;
const projectName = exp.slug;
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const updateGroupUrl = getUpdateGroupUrl(accountName, projectName, updateGroupId);
const updateGroupLink = link(updateGroupUrl, { dim: false });
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! },
{ label: 'Branch', value: branchName },
{ label: 'Runtime version', value: runtime },
{ label: 'Platform', value: platforms.join(', ') },
{ label: 'Update group ID', value: updateGroupId },
...(newAndroidUpdate
? [{ label: 'Android update ID', value: newAndroidUpdate.id }]
: []),
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
{ label: 'Message', value: truncatedMessage },
{ label: 'Website link', value: updateGroupLink },
])
);
Log.addNewLineIfNone();
@@ -541,9 +571,57 @@ export default class UpdatePublish extends EasCommand {
}
}
export async function getUpdatesToRepublishAsync(
projectId: string,
branchName: string,
platformFlag: string
): Promise<AppUpdateObject[]> {
const queryUpdatesForBranchAsync = async (
pageSize: number,
offset: number
): Promise<PaginatedQueryResponse<AppUpdateObject>> => {
const { updates } = await ensureBranchExistsAsync({
appId: projectId,
name: branchName!,
limit: pageSize,
offset,
});
if (!updates.length) {
throw new Error(
`There are no updates on branch "${branchName}" published for the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
);
}
const relevantUpdates =
platformFlag === 'all'
? updates
: updates.filter(update => {
// Only show groups that have updates on the specified platform(s).
return update.platform === platformFlag;
});
return {
queryResponse: relevantUpdates,
queryResponseRawLength: updates.length,
};
};
const getUpdateIdentifier = (update: AppUpdateObject): string => update.group;
return await performPaginatedQueryAsync({
pageSize: 50,
offset: 0,
queryToPerform: queryUpdatesForBranchAsync,
promptOptions: {
type: 'select',
title: 'Which update would you like to republish?',
createDisplayTextForSelectionPromptListItem: formatUpdateTitle,
getIdentifierForQueryItem: getUpdateIdentifier,
},
});
}
async function getRuntimeVersionObjectAsync(
exp: ExpoConfig,
platformFlag: PlatformFlag,
platformFlag: PublishPlatformFlag,
projectDir: string
): Promise<Record<string, string>> {
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
@@ -571,12 +649,24 @@ async function getRuntimeVersionObjectAsync(
);
}
function formatUpdateTitle(
update: Exclude<
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
null | undefined
>['updates'][number]
): string {
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.`
);
}
}
export type AppUpdateObject = Exclude<
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
null | undefined
>['updates'][0];
export function formatUpdateTitle(update: AppUpdateObject): string {
const { message, createdAt, actor, runtimeVersion } = update;
let actorName: string;
@@ -599,14 +689,58 @@ function formatUpdateTitle(
)} 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);
async function getBranchNameToPublishOnAsync(
projectId: string,
validationMessage: string
): Promise<string> {
const queryForBanchesToPublishOnAsync = async (
pageSize: number,
offset: number
): Promise<PaginatedQueryResponse<UpdateBranchFragment>> => {
const branches = await listBranchesAsync({ appId: projectId, limit: pageSize, offset });
if (branches.length === 0) {
const { name: newBranchName } = 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).substring(2, 7)}`,
validate: value => (value ? true : validationMessage),
});
branches.push({
id: '_defaultId',
name: newBranchName,
updates: [],
});
}
return { queryResponse: branches, queryResponseRawLength: branches.length };
};
const formatBranchTitle = (branch: UpdateBranchFragment): string =>
`${branch.name} ${chalk.grey(`- current update: ${formatUpdate(branch.updates[0])}`)}`;
const getBranchIdentifier = (branch: UpdateBranchFragment): string => branch.id;
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.`
);
}
const branches = await performPaginatedQueryAsync({
pageSize: 50,
offset: 0,
queryToPerform: queryForBanchesToPublishOnAsync,
promptOptions: {
type: 'select',
createDisplayTextForSelectionPromptListItem: formatBranchTitle,
getIdentifierForQueryItem: getBranchIdentifier,
title: 'Which branch would you like to publish on?',
},
});
const branchName = branches?.pop()?.name;
assert(branchName, validationMessage);
return branchName;
}
export const truncatePublishUpdateMessage = (originalMessage: string): string => {
if (originalMessage.length > 1024) {
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
return originalMessage.substring(0, 1021) + '...';
}
return originalMessage;
};
+107 -36
View File
@@ -1,18 +1,19 @@
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 { ViewAllUpdatesQuery, ViewBranchUpdatesQuery } from '../../graphql/generated';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
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 { PaginatedQueryResponse, performPaginatedQueryAsync } from '../../utils/queries';
import { getVcsClient } from '../../vcs';
type UpdateGroupDescription = FormatUpdateParameter & {
@@ -50,15 +51,11 @@ export default class BranchView extends EasCommand {
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
let updateGroupDescriptions: UpdateGroupDescription[];
if (all) {
const branchesAndUpdates = await UpdateQuery.viewAllAsync({ appId: projectId });
updateGroupDescriptions = getUpdateGroupDescriptions(
branchesAndUpdates.app.byId.updateBranches
);
await fetchAllUpdatesForAppAsync(projectId, jsonFlag);
} else {
let branchInteractive: string | undefined;
if (!branchFlag) {
@@ -77,46 +74,120 @@ export default class BranchView extends EasCommand {
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]);
fetchAllUpdatesForBranchAsync(projectId, branch, jsonFlag);
}
}
}
type BranchUpdateObject = Exclude<
ViewBranchUpdatesQuery['app']['byId']['updateBranchByName'],
null | undefined
>['updates'][0];
type AppUpdateObject = ViewAllUpdatesQuery['app']['byId']['updates'][0];
async function fetchAllUpdatesForBranchAsync(
projectId: string,
branchName: string,
jsonFlag: boolean
): Promise<void> {
const queryToPerformAsync = async (
pageSize: number,
offset: number
): Promise<PaginatedQueryResponse<BranchUpdateObject>> => {
const branchesAndUpdates = await UpdateQuery.viewBranchAsync({
appId: projectId,
name: branchName,
limit: pageSize,
offset,
});
const UpdateBranch = branchesAndUpdates.app?.byId.updateBranchByName;
if (!UpdateBranch) {
throw new Error(`Could not find branch "${branchName}"`);
}
return {
queryResponse: UpdateBranch.updates,
queryResponseRawLength: UpdateBranch.updates.length,
};
};
const renderListItems = (currentPage: BranchUpdateObject[]): void => {
const updateGroupDescriptions = getUpdateGroupDescriptions(currentPage);
if (jsonFlag) {
printJsonOnlyOutput(updateGroupDescriptions);
} else {
logAsTable(updateGroupDescriptions);
}
}
};
await performPaginatedQueryAsync({
pageSize: 50,
offset: 0,
queryToPerform: queryToPerformAsync,
promptOptions: {
type: 'confirm',
title: 'View next page of updates?',
renderListItems,
},
});
}
async function fetchAllUpdatesForAppAsync(projectId: string, jsonFlag: boolean): Promise<void> {
const queryToPerformAsync = async (
pageSize: number,
offset: number
): Promise<PaginatedQueryResponse<AppUpdateObject>> => {
const viewAllUpdates = await UpdateQuery.viewAllAsync({
appId: projectId,
limit: pageSize,
offset,
});
const { updates } = viewAllUpdates.app.byId;
return {
queryResponse: updates,
queryResponseRawLength: updates.length,
};
};
const renderListItems = (currentPage: AppUpdateObject[]): void => {
const updateGroupDescriptions = getUpdateGroupDescriptions(currentPage);
if (jsonFlag) {
printJsonOnlyOutput(updateGroupDescriptions);
} else {
logAsTable(updateGroupDescriptions);
}
};
await performPaginatedQueryAsync({
pageSize: 50,
offset: 0,
queryToPerform: queryToPerformAsync,
promptOptions: {
type: 'confirm',
title: 'View next page of updates?',
renderListItems,
},
});
}
function getUpdateGroupDescriptions(
branchesAndUpdates: ViewAllUpdatesQuery['app']['byId']['updateBranches']
updates: (BranchUpdateObject | AppUpdateObject)[]
): UpdateGroupDescription[] {
const flattenedBranchesAndUpdates = branchesAndUpdates.flatMap(branch =>
branch.updates.map(update => {
return { branch: branch.name, ...update };
return Object.values(groupBy(updates, update => update.group))
.map(updateGroup => {
return {
platforms: updateGroup
.map(update => update.platform)
.sort()
.join(', '),
...updateGroup[0],
branch: updateGroup[0].branch.name,
} as UpdateGroupDescription;
})
);
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;
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
}
function logAsTable(updateGroupDescriptions: UpdateGroupDescription[]): void {
@@ -1,10 +1,10 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import { WebhookType } from '../../graphql/generated';
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { prepareInputParamsAsync } from '../../webhooks/input';
@@ -30,7 +30,7 @@ export default class WebhookCreate extends EasCommand {
const webhookInputParams = await prepareInputParamsAsync(flags);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const spinner = ora('Creating a webhook').start();
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import assert from 'assert';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
@@ -9,6 +8,7 @@ import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
import Log from '../../log';
import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { formatWebhook } from '../../webhooks/formatWebhook';
@@ -30,7 +30,7 @@ export default class WebhookDelete extends EasCommand {
} = await this.parse(WebhookDelete);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
let webhook: WebhookFragment | undefined =
@@ -1,4 +1,3 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -7,6 +6,7 @@ import { WebhookType } from '../../graphql/generated';
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
import Log from '../../log';
import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
@@ -30,7 +30,7 @@ export default class WebhookList extends EasCommand {
} = await this.parse(WebhookList);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp);
const projectFullName = await getProjectFullNameAsync(exp);
@@ -1,5 +1,5 @@
import AppStoreApi from '../ios/appstore/AppStoreApi';
import { AuthCtx } from '../ios/appstore/authenticate';
import { AuthCtx } from '../ios/appstore/authenticateTypes';
export const testAuthCtx: AuthCtx = {
appleId: 'test-apple-id',
@@ -76,7 +76,9 @@ export const testPushKey: ApplePushKeyFragment = {
iosAppCredentialsList: [],
};
export const testTargets: Target[] = [{ targetName: 'testapp', bundleIdentifier: 'foo.bar.com' }];
export const testTargets: Target[] = [
{ targetName: 'testapp', bundleIdentifier: 'foo.bar.com', entitlements: {} },
];
export const testProvisioningProfileFragment: AppleProvisioningProfileFragment = {
id: 'test-prov-prof-id-1',
@@ -1,5 +1,8 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import { GoogleServiceAccountKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import Log, { learnMore } from '../../../log';
import { promptAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { CredentialsContext } from '../../context';
@@ -41,14 +44,33 @@ export class CreateGoogleServiceAccountKey {
return detectedPath;
}
const { keyJsonPath } = await promptAsync([
{
type: 'text',
name: 'keyJsonPath',
message: 'Path to Google Service Account Key JSON file:',
validate: (value: string) => value.length > 0 || "Path can't be empty",
Log.log(
`${chalk.bold(
'A Google Service Account JSON key is required to upload your app to Google Play Store'
)}.\n` +
`If you're not sure what this is or how to create one, ${learnMore(
'https://expo.fyi/creating-google-service-account',
{ learnMoreMessage: 'learn more' }
)}`
);
const { filePath } = await promptAsync({
name: 'filePath',
message: 'Path to Google Service Account file:',
initial: 'api-0000000000000000000-111111-aaaaaabbbbbb.json',
type: 'text',
// eslint-disable-next-line async-protect/async-suffix
validate: async (filePath: string) => {
try {
const stats = await fs.stat(filePath);
if (stats.isFile()) {
return true;
}
return 'Input is not a file.';
} catch {
return 'File does not exist.';
}
},
]);
return keyJsonPath;
});
return filePath;
}
}
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync, promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -17,7 +16,7 @@ import {
jest.mock('../../../../ora');
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe('BuildCredentialsUtils', () => {
describe(canCopyLegacyCredentialsAsync, () => {
@@ -84,7 +83,9 @@ describe('BuildCredentialsUtils', () => {
});
describe(promptUserAndCopyLegacyCredentialsAsync, () => {
it('copies all legacy credentials to EAS if the user is eligible', async () => {
asMock(promptAsync).mockImplementation(() => ({ providedName: 'test-provided-name' }));
jest
.mocked(promptAsync)
.mockImplementation(async () => ({ providedName: 'test-provided-name' }));
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -1,11 +1,10 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateFcm } from '../CreateFcm';
jest.mock('../../../../prompts');
asMock(promptAsync).mockImplementation(() => ({ fcmApiKey: 'blah' }));
jest.mocked(promptAsync).mockImplementation(async () => ({ fcmApiKey: 'blah' }));
describe(CreateFcm, () => {
it('creates an fcm api key in Interactive Mode', async () => {
@@ -1,6 +1,5 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
@@ -8,8 +7,8 @@ import { CreateGoogleServiceAccountKey } from '../CreateGoogleServiceAccountKey'
jest.mock('../../../../prompts');
jest.mock('fs');
asMock(promptAsync).mockImplementation(() => ({
keyJsonPath: '/google-service-account-key.json',
jest.mocked(promptAsync).mockImplementation(async () => ({
filePath: '/google-service-account-key.json',
}));
beforeEach(() => {
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { jester as mockJester } from '../../../../credentials/__tests__/fixtures-constants';
import { testKeystore } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -15,7 +14,7 @@ jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn(
describe('CreateKeystore', () => {
beforeEach(() => {
asMock(generateRandomKeystoreAsync).mockReset();
jest.mocked(generateRandomKeystoreAsync).mockReset();
});
it('creates a keystore in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
@@ -28,7 +27,7 @@ describe('CreateKeystore', () => {
expect(generateRandomKeystoreAsync).toHaveBeenCalled();
});
it('creates a keystore by uploading', async () => {
asMock(askForUserProvidedAsync).mockImplementationOnce(() => testKeystore);
jest.mocked(askForUserProvidedAsync).mockImplementationOnce(async () => testKeystore);
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -9,7 +8,7 @@ import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { RemoveFcm } from '../RemoveFcm';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(RemoveFcm, () => {
it('removes an FCM API Key', async () => {
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -8,7 +7,7 @@ import { createCtxMock } from '../../../__tests__/fixtures-context';
import { RemoveGoogleServiceAccountKey } from '../RemoveGoogleServiceAccountKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(RemoveGoogleServiceAccountKey, () => {
it('removes an Google Service Account Key', async () => {
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -7,7 +6,7 @@ import { RemoveKeystore } from '../RemoveKeystore';
jest.mock('fs-extra');
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(RemoveKeystore, () => {
it('removes a keystore', async () => {
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import {
getNewAndroidApiMock,
testAndroidBuildCredentialsFragment,
@@ -19,7 +18,7 @@ jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn(
describe('SetUpBuildCredentials', () => {
beforeEach(() => {
asMock(generateRandomKeystoreAsync).mockReset();
jest.mocked(generateRandomKeystoreAsync).mockReset();
});
it('skips setup when there are prior credentials', async () => {
@@ -1,6 +1,5 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { AndroidKeystoreType } from '../../../../graphql/generated';
import { confirmAsync } from '../../../../prompts';
import {
@@ -13,13 +12,14 @@ import {
SelectAndroidBuildCredentials,
SelectAndroidBuildCredentialsResultType,
} from '../../../manager/SelectAndroidBuildCredentials';
import { AppLookupParams } from '../../api/GraphqlClient';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetUpBuildCredentialsFromCredentialsJson } from '../SetUpBuildCredentialsFromCredentialsJson';
jest.mock('fs');
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
jest.mock('../../../manager/SelectAndroidBuildCredentials');
beforeEach(() => {
@@ -28,7 +28,7 @@ beforeEach(() => {
describe(SetUpBuildCredentialsFromCredentialsJson, () => {
it('sets up a new build configuration from credentials.json upon user request', async () => {
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
jest.mocked(SelectAndroidBuildCredentials).mockImplementation((() => {
return {
runAsync: () => {
return {
@@ -37,7 +37,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
};
},
};
});
}) as any as (app: AppLookupParams) => SelectAndroidBuildCredentials);
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -78,7 +78,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
expect(ctx.android.createAndroidAppBuildCredentialsAsync).toHaveBeenCalledTimes(1);
});
it('uses an existing build configuration upon user request', async () => {
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
jest.mocked(SelectAndroidBuildCredentials).mockImplementation((() => {
return {
runAsync: () => {
return {
@@ -87,7 +87,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
};
},
};
});
}) as any as (app: AppLookupParams) => SelectAndroidBuildCredentials);
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -1,6 +1,5 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -14,8 +13,8 @@ import { SetUpGoogleServiceAccountKey } from '../SetUpGoogleServiceAccountKey';
jest.mock('../../../../prompts');
jest.mock('fs');
asMock(promptAsync).mockImplementation(() => ({
keyJsonPath: '/google-service-account-key.json',
jest.mocked(promptAsync).mockImplementation(async () => ({
filePath: '/google-service-account-key.json',
}));
beforeEach(() => {
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -9,7 +8,9 @@ import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { UseExistingGoogleServiceAccountKey } from '../UseExistingGoogleServiceAccountKey';
jest.mock('../../../../prompts');
asMock(promptAsync).mockImplementation(() => ({ chosenKey: testGoogleServiceAccountKeyFragment }));
jest
.mocked(promptAsync)
.mockImplementation(async () => ({ chosenKey: testGoogleServiceAccountKeyFragment }));
describe(UseExistingGoogleServiceAccountKey, () => {
it('uses an existing Google Service Account Key in Interactive Mode', async () => {
@@ -1,6 +1,5 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync, promptAsync } from '../../../../prompts';
import { detectGoogleServiceAccountKeyPathAsync } from '../googleServiceAccountKey';
@@ -25,13 +24,13 @@ afterAll(() => {
});
afterEach(() => {
asMock(promptAsync).mockClear();
asMock(confirmAsync).mockClear();
jest.mocked(promptAsync).mockClear();
jest.mocked(confirmAsync).mockClear();
});
describe('Google Service Account Key path detection', () => {
it('detects a single google-services file and prompts for confirmation', async () => {
asMock(confirmAsync).mockResolvedValueOnce(true);
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir/subdir');
expect(confirmAsync).toHaveBeenCalledWith(
expect.objectContaining({ message: 'Would you like to use this file?' })
@@ -46,7 +45,7 @@ describe('Google Service Account Key path detection', () => {
});
it('returns null, when user rejects to use detected file', async () => {
asMock(confirmAsync).mockResolvedValueOnce(false);
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir/subdir');
expect(confirmAsync).toHaveBeenCalledTimes(1);
expect(confirmAsync).toHaveBeenCalledWith(
@@ -56,7 +55,7 @@ describe('Google Service Account Key path detection', () => {
});
it('displays a chooser, when multiple files are found', async () => {
asMock(promptAsync).mockResolvedValueOnce({
jest.mocked(promptAsync).mockResolvedValueOnce({
selectedPath: '/project_dir/another-service-account.json',
});
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir'); // should find 2 files here
@@ -76,7 +75,7 @@ describe('Google Service Account Key path detection', () => {
});
it('returns null, when user selects the "None of above"', async () => {
asMock(promptAsync).mockResolvedValueOnce({
jest.mocked(promptAsync).mockResolvedValueOnce({
selectedPath: false,
});
@@ -1,6 +1,5 @@
import mockdate from 'mockdate';
import { asMock } from '../../../../__tests__/utils';
import Log from '../../../../log';
import {
testAndroidAppCredentialsFragment,
@@ -25,7 +24,9 @@ describe('print credentials', () => {
appLookupParams,
appCredentials: testAndroidAppCredentialsFragment,
});
const loggedSoFar = asMock(Log.log).mock.calls.reduce((acc, mockValue) => acc + mockValue);
const loggedSoFar = jest
.mocked(Log.log)
.mock.calls.reduce((acc, mockValue) => acc + mockValue.toString(), '');
expect(loggedSoFar).toMatchSnapshot();
});
});
@@ -20,7 +20,7 @@ function fileIsServiceAccountKey(keyJsonPath: string): boolean {
try {
readAndValidateServiceAccountKey(keyJsonPath);
return true;
} catch (err: any) {
} catch {
return false;
}
}
@@ -24,7 +24,7 @@ export async function keytoolCommandExistsAsync(): Promise<boolean> {
try {
await spawnAsync('keytool');
return true;
} catch (error) {
} catch {
return false;
}
}
@@ -24,7 +24,7 @@ function isPKCSKeystore(keystore: Keystore): boolean {
try {
parsePKCS12(keystore.keystore, keystore.keystorePassword);
return true;
} catch (e) {
} catch {
return false;
}
}
@@ -33,7 +33,7 @@ function isJKSKeystore(keystore: Keystore): boolean {
try {
jks.parseJks(Buffer.from(keystore.keystore, 'base64'), keystore.keystorePassword);
return true;
} catch (e) {
} catch {
return false;
}
}
+9 -3
View File
@@ -1,4 +1,4 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { ExpoConfig } from '@expo/config';
import { Env } from '@expo/eas-build-job';
import chalk from 'chalk';
@@ -10,6 +10,7 @@ import { Actor, getActorDisplayName } from '../user/User';
import * as AndroidGraphqlClient from './android/api/GraphqlClient';
import * as IosGraphqlClient from './ios/api/GraphqlClient';
import AppStoreApi from './ios/appstore/AppStoreApi';
import { AuthenticationMode } from './ios/appstore/authenticateTypes';
export class CredentialsContext {
public readonly android = AndroidGraphqlClient;
@@ -49,7 +50,7 @@ export class CredentialsContext {
): ExpoConfig | null {
try {
return getExpoConfig(projectDir, { env });
} catch (error) {
} catch {
// ignore error, context might be created outside of expo project
return null;
}
@@ -69,7 +70,7 @@ export class CredentialsContext {
return;
}
// trigger getConfig error
getConfig(this.options.projectDir, { skipSDKVersionRequirement: true });
getExpoConfig(this.options.projectDir);
}
public logOwnerAndProject(): void {
@@ -99,6 +100,11 @@ export class CredentialsContext {
return;
}
if (this.appStore.defaultAuthenticationMode === AuthenticationMode.API_KEY) {
await this.appStore.ensureAuthenticatedAsync();
return;
}
Log.log(
chalk.green(
'If you provide your Apple account credentials we will be able to generate all necessary build credentials and fully validate them.'
@@ -2,7 +2,6 @@ import fs from 'fs-extra';
import { vol } from 'memfs';
import prompts from 'prompts';
import { asMock } from '../../../__tests__/utils';
import { IosDistributionType } from '../../../graphql/generated';
import {
testKeystore,
@@ -25,8 +24,8 @@ jest.mock('prompts');
describe('update credentials.json', () => {
beforeEach(() => {
vol.reset();
asMock(prompts).mockReset();
asMock(prompts).mockImplementation(() => {
jest.mocked(prompts).mockReset();
jest.mocked(prompts).mockImplementation(() => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
});
@@ -176,6 +175,7 @@ describe('update credentials.json', () => {
{
targetName: 'testapp',
bundleIdentifier: 'com.bundle.id',
entitlements: {},
},
];
@@ -114,7 +114,7 @@ export async function readRawAsync(
try {
const credentialsJSONContents = await fs.readFile(credentialsJsonFilePath, 'utf8');
return JSON.parse(credentialsJSONContents);
} catch (err) {
} catch {
throw new Error(
`credentials.json must exist in the project root directory and contain a valid JSON`
);
@@ -11,7 +11,6 @@ import { ensureAllTargetsAreConfigured } from '../credentialsJson/utils';
import { getAppFromContext } from './actions/BuildCredentialsUtils';
import { SetUpBuildCredentials } from './actions/SetUpBuildCredentials';
import { SetUpPushKey } from './actions/SetUpPushKey';
import { IosCapabilitiesOptions } from './appstore/ensureAppExists';
import { App, IosCredentials, Target } from './types';
import { isAdHocProfile } from './utils/provisioningProfile';
@@ -20,7 +19,6 @@ interface Options {
targets: Target[];
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
export default class IosCredentialsProvider {
@@ -63,7 +61,6 @@ export default class IosCredentialsProvider {
targets: this.options.targets,
distribution: this.options.distribution,
enterpriseProvisioning: this.options.enterpriseProvisioning,
iosCapabilitiesOptions: this.options.iosCapabilitiesOptions,
}).runAsync(this.ctx);
}
@@ -58,7 +58,13 @@ describe(IosCredentialsProvider, () => {
},
projectName: appLookupParams.projectName,
},
targets: [{ targetName: 'testapp', bundleIdentifier: appLookupParams.bundleIdentifier }],
targets: [
{
targetName: 'testapp',
bundleIdentifier: appLookupParams.bundleIdentifier,
entitlements: {},
},
],
distribution: 'store',
});
await expect(provider.getCredentialsAsync(CredentialsSource.REMOTE)).rejects.toThrowError(
@@ -95,7 +101,13 @@ describe(IosCredentialsProvider, () => {
},
projectName: appLookupParams.projectName,
},
targets: [{ targetName: 'testapp', bundleIdentifier: appLookupParams.bundleIdentifier }],
targets: [
{
targetName: 'testapp',
bundleIdentifier: appLookupParams.bundleIdentifier,
entitlements: {},
},
],
distribution: 'store',
});
@@ -94,7 +94,6 @@ async function generateAscApiKeyAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<MinimalAscApiKey> {
//console.log('debug');
await ctx.appStore.ensureAuthenticatedAsync();
const ascApiKey = await ctx.appStore.createAscApiKeyAsync({
nickname: getAscApiKeyName(purpose),
@@ -12,7 +12,7 @@ import { MissingCredentialsNonInteractiveError } from '../../errors';
import { AppLookupParams } from '../api/GraphqlClient';
import { AppleProvisioningProfileMutationResult } from '../api/graphql/mutations/AppleProvisioningProfileMutation';
import { ProvisioningProfileStoreInfo } from '../appstore/Credentials.types';
import { AuthCtx } from '../appstore/authenticate';
import { AuthCtx } from '../appstore/authenticateTypes';
export class ConfigureProvisioningProfile {
constructor(
@@ -84,7 +84,7 @@ export class ConfigureProvisioningProfile {
certP12: certificateP12,
certPassword: certificatePassword,
distCertSerialNumber: serialNumber,
teamId: authCtx.appleId,
teamId: authCtx.team.id,
}
);
@@ -9,7 +9,7 @@ import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
import { AppLookupParams } from '../api/GraphqlClient';
import { AppleProvisioningProfileMutationResult } from '../api/graphql/mutations/AppleProvisioningProfileMutation';
import { ProvisioningProfile } from '../appstore/Credentials.types';
import { AuthCtx } from '../appstore/authenticate';
import { AuthCtx } from '../appstore/authenticateTypes';
import { provisioningProfileSchema } from '../credentials';
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { generateProvisioningProfileAsync } from './ProvisioningProfileUtils';
@@ -68,7 +68,7 @@ export class CreateProvisioningProfile {
certP12: this.distributionCertificate.certificateP12,
certPassword: this.distributionCertificate.certificatePassword,
distCertSerialNumber: this.distributionCertificate.serialNumber,
teamId: this.distributionCertificate.appleTeam?.appleTeamIdentifier ?? appleAuthCtx.appleId,
teamId: this.distributionCertificate.appleTeam?.appleTeamIdentifier ?? appleAuthCtx.team.id,
});
}
}
@@ -4,7 +4,6 @@ import nullthrows from 'nullthrows';
import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
import { App, IosAppBuildCredentialsMap, IosCredentials, Target } from '../types';
import { displayProjectCredentials } from '../utils/printCredentials';
import { SetUpTargetBuildCredentials } from './SetUpTargetBuildCredentials';
@@ -14,7 +13,6 @@ interface Options {
targets: Target[];
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
export class SetUpBuildCredentials {
@@ -50,7 +48,9 @@ export class SetUpBuildCredentials {
Log.newLine();
}
const action = new SetUpTargetBuildCredentials({
...this.options,
enterpriseProvisioning: this.options.enterpriseProvisioning,
distribution: this.options.distribution,
entitlements: target.entitlements,
app: {
...this.options.app,
bundleIdentifier: target.bundleIdentifier,
@@ -1,4 +1,5 @@
import { DistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import { JSONObject } from '@expo/json-file';
import {
IosDistributionType as GraphQLIosDistributionType,
@@ -7,7 +8,6 @@ import {
import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { AppLookupParams as GraphQLAppLookupParams } from '../api/GraphqlClient';
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
import { SetUpAdhocProvisioningProfile } from './SetUpAdhocProvisioningProfile';
import { SetUpInternalProvisioningProfile } from './SetUpInternalProvisioningProfile';
import { SetUpProvisioningProfile } from './SetUpProvisioningProfile';
@@ -16,13 +16,13 @@ interface Options {
app: GraphQLAppLookupParams;
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
entitlements: JSONObject;
}
export class SetUpTargetBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
const { app, iosCapabilitiesOptions } = this.options;
const { app, entitlements } = this.options;
await ctx.bestEffortAppStoreAuthenticateAsync();
@@ -33,7 +33,7 @@ export class SetUpTargetBuildCredentials {
bundleIdentifier: app.bundleIdentifier,
projectName: app.projectName,
},
iosCapabilitiesOptions
{ entitlements }
);
}
try {
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -18,8 +17,8 @@ function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
}
afterEach(() => {
asMock(promptAsync).mockClear();
asMock(getCredentialsFromUserAsync).mockClear();
jest.mocked(promptAsync).mockClear();
jest.mocked(getCredentialsFromUserAsync).mockClear();
});
describe(getAscApiKeyName, () => {
@@ -35,10 +34,10 @@ describe(getAscApiKeyName, () => {
describe(promptForAscApiKeyPathAsync, () => {
it('prompts for keyId, keyP8Path and issuerId when user is not authenticated to Apple', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
jest.mocked(getCredentialsFromUserAsync).mockImplementation(async () => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
@@ -62,10 +61,10 @@ describe(promptForAscApiKeyPathAsync, () => {
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(2); // keyId, issuerId
});
it('prompts for keyId, keyP8Path and detects issuerId when user is authenticated to Apple', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
jest.mocked(getCredentialsFromUserAsync).mockImplementation(async () => ({
keyId: 'test-key-id',
}));
const ctx = createCtxMock({
@@ -85,6 +84,6 @@ describe(promptForAscApiKeyPathAsync, () => {
});
expect(promptAsync).toHaveBeenCalledTimes(1); // keyP8Path
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(1); // keyId
expect(asMock(ctx.appStore.getAscApiKeyAsync).mock.calls.length).toBe(1); // issuerId
expect(jest.mocked(ctx.appStore.getAscApiKeyAsync).mock.calls.length).toBe(1); // issuerId
});
});
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -15,7 +14,7 @@ import { ConfigureProvisioningProfile } from '../ConfigureProvisioningProfile';
jest.mock('../../../../ora');
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe('ConfigureProvisioningProfile', () => {
it('configures a Provisioning Profile in Interactive Mode', async () => {
@@ -43,9 +42,9 @@ describe('ConfigureProvisioningProfile', () => {
await provProfConfigurator.runAsync(ctx);
// expect provisioning profile not to be updated on expo servers
expect(asMock(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(1);
// expect provisioning profile not to be updated on apple portal
expect(asMock(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -62,8 +61,8 @@ describe('ConfigureProvisioningProfile', () => {
);
// expect provisioning profile not to be updated on expo servers
expect(asMock(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be updated on apple portal
expect(asMock(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(0);
});
});
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -9,7 +8,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreateAscApiKey } from '../CreateAscApiKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(CreateAscApiKey, () => {
it('creates a App Store API Key in Interactive Mode', async () => {
@@ -27,9 +26,9 @@ describe(CreateAscApiKey, () => {
await createAscApiKeyAction.runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
// expect api key to be created on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
// expect api key to be created on apple portal
expect(asMock(ctx.appStore.createAscApiKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.appStore.createAscApiKeyAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
Loaded 100 of 256 files, more files were not shown because too many files have changed in this diff. Show more