Compare commits

..
319 Commits
Author SHA1 Message Date
Dominik Sokal 72283cdda2 v2.8.0 2022-11-28 12:13:38 +01:00
Dominik Sokal 9874ff26b3 upgrade eas-cli-local-build-plugin to 0.0.120 2022-11-28 12:13:16 +01:00
Dominik Sokal 85d64b7ad3 [ENG-6491] remove internal expo id from device:list (#1541)
* remove internal expo id from device:list

* update CHANGELOG.md
2022-11-28 09:34:36 +01:00
Szymon Dziedzic 2dba2705b7 [ENG-6238][eas-cli] add build:run support for Android (#1485)
* [eas-cli] add build:run support for Android

* add assertions about android executables

* use spawnAsync instead of spawn

* minor refactor

* minor improvements to prompts

* improve download function name

* refactor code after the review

* apply suggestions from the review

* fix commit message formatting

* use progress tracker instead of progress bar

* improve formatting of build choices

* fix typo in function name

* requested improvements

* test using path.join for fast-lob

* fix windows glob issues

* fix glob on windows

* allow users to run ios builds only on apple device

* use path.join instead of interpolation

* address feedback

* auto select android for non mac users

* minor improvements

* improve error messages

* update CHANGELOG.md
2022-11-25 13:57:19 +01:00
Cedric van Putten d1bec6e00a [eas-cli] Only create a channel with branch name when branch is created (#1507)
* fix(update): only create a channel with branch name when branch is created

* refactor(update): rename branchIsCreated to createdBranch

* update CHANGELOG.md
2022-11-23 18:28:08 +01:00
Szymon Dziedzic 597e012ca3 [ENG-6941][eas-json] add better error message for when invalid image is set in the eas.json (#1531)
* [eas-json] add custom error message handler for invalid image validation error

* update CHANGELOG.md
2022-11-21 15:52:30 +01:00
Wojciech Kozyra 8632b8ada7 [eas-cli] Upload project sources and submit archives to GCS (#1524) 2022-11-17 11:57:41 +01:00
Wojciech Kozyra b288879838 [eas-cli] Add SDK version to tracking context (#1529) 2022-11-17 11:34:24 +01:00
Dominik Sokal 8beb8f46da change wording: "Submission Service" -> "EAS Submit" (#1527)
* change wording: "Submission Service" -> "EAS Submit"

* update CHANGELOG.md
2022-11-17 09:53:44 +01:00
Expo CI 9120bf5777 update CHANGELOG.md 2022-11-16 14:58:36 +00:00
Szymon Dziedzic 01619d4182 v2.7.1 2022-11-16 15:49:27 +01:00
Szymon Dziedzic aa4ea4ae2a update eas-build-job version in yarn.lock 2022-11-16 15:48:54 +01:00
Szymon Dziedzic c236c2ffb5 upgrade eas-cli-local-build-plugin to 0.0.119 2022-11-16 15:45:52 +01:00
Szymon Dziedzic b5a3a513de [ENG-6892][eas-json] add Xcode 14.1 image (#1511)
* [eas-json] add Xcode 14.1 image

* bump build tools version

* update CHANGELOG.md
2022-11-16 15:30:15 +01:00
Wojciech Kozyra 6058183c45 [eas-cli] Show what prompt failed when running in non interactive shell. (#1523) 2022-11-16 09:57:22 +01:00
Wojciech Kozyra a042d27573 [eas-cli] Pass env variables when resolving config in credentials manager. (#1520) 2022-11-15 11:19:26 +01:00
Dominik Sokal 88b21e476d [scripts] run yarn install before running scripts (#1519) 2022-11-14 10:34:02 +01:00
Expo CI 3d497d4f05 update CHANGELOG.md 2022-11-11 17:26:16 +00:00
Brent Vatne 9b668e3629 v2.7.0 2022-11-11 09:16:07 -08:00
Brent Vatne a670a2a3fc upgrade eas-cli-local-build-plugin to 0.0.118 2022-11-11 09:15:44 -08:00
Brent Vatne 53a407fa78 [eas-cli] Fix test 2022-11-11 09:12:30 -08:00
Brent Vatne 5ab1f637a5 [eas-cli] Remove dependency on expo package from eas init (#1517) 2022-11-11 09:01:17 -08:00
Szymon Dziedzic f59c945347 [ENG-6162][eas-cli] Change choice formatting when selecting build to submit (#1502)
* [eas-cli] change choice formatting when selecting build to submit

* fix typo

* use description with one line choice

* use eas update channel vs old release channel

* update CHANGELOG.md
2022-11-10 17:49:41 +01:00
Evan Bacon 83e50ab546 feat: support uploading different bundle combos (#1461)
* feat: support uploading web bundles

Expo Router defaults to shipping with universal Metro support (ios, android, web) which means eas update breaks by default if it asserts that web is not supported.

This PR also fixes the inability to upload single platforms.

* ensure platform filtering works

* Update publish.ts

* add tests

* update CHANGELOG.md

* style xits from PR feedback

* attempt to fix

* fix

* Update index.ts

* Update CHANGELOG.md

* Update publish-test.ts

* Update publish-test.ts
2022-11-09 15:49:28 -06:00
Dominik Sokal 9a415fe743 update CHANGELOG.md 2022-11-07 10:34:56 +01:00
Mackenzie Salisbury e722b6601c [eas-json] fix ordering of distribution enum values (#1504) 2022-11-07 10:19:56 +01:00
Dominik Sokal 170e5145f0 update CHANGELOG.md 2022-11-03 09:15:37 +01:00
Evan Bacon 405c532f13 use version check for disabling global CLI (#1474) 2022-11-02 21:22:29 -05:00
Juwan Wheatley 5bed1c3a01 [eas-cli][update] Attach Git Commit to EAS Update (#1499)
* [eas-cli][update] Attach Git Commit to EAS Update

* update changelog

* include commit in update summary log
2022-11-02 19:06:24 -04:00
Cedric van PuttenandEvan Bacon 8d0d34c497 [eas-cli] Improve configuration setup for EAS update (#1479)
* refactor(update): auto configure EAS updates when running eas update

* fix(update): make sure eas update does what it needs to do

* refactor(update): drop obsolete eas update validation errors

* refactor(update): drop duplicate truncate update message

* refactor(update): drop duplicate install expo-update message

* fix(update): use the old truncated update message limit

* refactor(update): ensure the latest changes are integrated in the PR

* fix(update): use the EAS update configured expo config

* chore(update): make the EAS Update outage a bit more readable

* fix(update): remove _internal from merged manifest if defined

* fix(update): make sure people can run `eas update:configure` without project id

* update CHANGELOG.md

* refactor(update): skip setting the sdkVersion when configuring

* fix(update): only merge nested objects when configuring

* fix(update): refetch the config whenever projects are auto-setup

* refactor(update): simplify the config object merging

* refactor(update): change logging based on review input

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

* refactor(update): drop fyi .md extension from logging

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

* refactor(update): alawys reload the project config after ensuring eas updates

* fix(update): linting issue caused by logging changes

* refactor(update): drop unused exports from updates configure code

* refactor(update): split merge config to separate method for testing

* fix(update): show full install log when debugging is enabled

* fix(update): add back the missing <remove this key> instruction

Co-authored-by: Evan Bacon <[email protected]>
2022-11-02 23:18:50 +01:00
Dominik Sokal 6270bc122b [ENG-6619] validate icon PNGs before running build (#1477)
* validate icon PNGs before running build

* address PR feedback

* address PR feedback

* address more PR feedback
2022-11-02 18:05:55 +01:00
Cedric van PuttenandWill Schurman 57f1128450 [eas-cli] Extract entity utilities from update command class (#1478)
* refactor(update): remove default non-interactive value to rely on falsy values

Co-authored-by: Will Schurman <[email protected]>

* refactor(updates): make non interactive optional to rely on falsy values

* Revert "refactor(update): remove default non-interactive value to rely on falsy values"

This reverts commit 23ac36d36e35b7a87724229234d41dcac8cf02db.

* Revert "refactor(updates): make non interactive optional to rely on falsy values"

This reverts commit 94ae7cc039c94f28b74a5cfd9857f79e5a11e82c.

* refactor(update): move ensure methods outside the update command

* fix(update): add back log about channel pointing to branch

* refactor(branch): reuse query from shared utilsin `branch:create`

* refactor(channel): reuse query from shared utilsin `channel:create`

Co-authored-by: Will Schurman <[email protected]>
2022-10-31 18:47:56 +01:00
Cedric van PuttenandWill Schurman cb09906b2e [eas-cli] Add flag sanitization in updates command (#1473)
* [eas-cli] Add flag sanitization in updates command

* refactor(update): remove default non-interactive value to rely on falsy values

Co-authored-by: Will Schurman <[email protected]>

* refactor(updates): make non interactive optional to rely on falsy values

* Revert "refactor(update): remove default non-interactive value to rely on falsy values"

This reverts commit 23ac36d36e35b7a87724229234d41dcac8cf02db.

* Revert "refactor(updates): make non interactive optional to rely on falsy values"

This reverts commit 94ae7cc039c94f28b74a5cfd9857f79e5a11e82c.

Co-authored-by: Will Schurman <[email protected]>
2022-10-31 18:18:44 +01:00
Bartosz Kaszubowski fa704c43b3 [eas-cli] Update: improve DX on first run, correct error message (#1427) 2022-10-27 17:01:27 +02:00
Dominik Sokal eede0c3dd7 fix CHANGELOG.md 2022-10-27 12:07:44 +02:00
Expo CI 1758a4e5e5 update CHANGELOG.md 2022-10-27 08:06:51 +00:00
Dominik Sokal 20ebf24a20 v2.6.0 2022-10-27 10:00:11 +02:00
Juwan WheatleyandWill Schurman 427ececb0b [eas-cli] Warn about outdated build deployment when configuring EAS Update (#1467)
* [eas-cli] Warn about outdated build deployment when configuring EAS Update

* update changelog with PR number

* print warning no matter what

* Update packages/eas-cli/src/commands/update/configure.ts

Co-authored-by: Will Schurman <[email protected]>

Co-authored-by: Will Schurman <[email protected]>
2022-10-26 17:18:50 -04:00
Cedric van Putten f65670e18f [eas-cli] Drop remaining cli tables (#1481)
* refactor: Replace `branch:view` and `update:list` table with formatted fields

* refactor: Replace `channel:view` and `channel:list` table with formatted fields

* refactor: Replace `update:list --all` table with formatted fields

* refactor: drop cli-table3 dependency

* update CHANGELOG.md
2022-10-26 11:55:51 -07:00
Dominik Sokal 41a0a4554d upgrade dependencies (#1480)
* upgrade dependencies

* regenerate graphql code

* fix types

* revert axios upgrade

* fix tests

* upgrade oclif deps

* fix more tests

* update CHANGELOG.md
2022-10-26 14:09:33 +02:00
Will Schurman c1da053f8c [eas-cli] Add feature gate support (#1475) 2022-10-25 08:55:14 -07:00
Szymon Dziedzic 57da02af9d [ENG-6238][eas-cli] Add eas build:run command and implement running iOS simulator builds directly from EAS CLI (#1447)
* create eas run command

* improve run command

* downaload and install iOS sim app

* add logic to select best avaliable simulator

* add not implemented android command

* use expo cli implementation of detecting if simulator is running

* support both archive and app paths in path argument

* better validate path argument

* refactor iOS code

* assert that Xcode and xcrun are installed

* rename ios and android run files

* assert simulator app is installed

* apply suggestions from code review

* improve select sim build prompt

* hide command

* minor fixes

* apply improvements suggested in code review

* update CHANGELOG.md
2022-10-25 12:02:38 +02:00
Brent Vatne b9c505bbdc Update CODEOWNERS 2022-10-24 11:28:02 -07:00
Brent Vatne 633e6a6721 Update CODEOWNERS 2022-10-24 11:19:59 -07:00
Cedric van PuttenandDominik Sokal f7ddb4ba8f [eas-cli] Add metadata validation command with complex rules (#1416)
* refactor: update ajv, add human errors, and prepare lint command

* feature: add custom validation rules

* refactor: allow metadata context to be created without auth state

* refactor: update validation errors with new custom issues

* refactor: update existing code to use new features

* feature: add custom apple validation rules

* feature: implement validation command with json option

* chore: clean up outdated files

* refactor: update config schema type to improve human error message

* [eas-cli] Fix typo in ajv version

* [eas-cli] Use built-in context API to get project dir

* [eas-cli] Remove commented code

* [eas-cli] Load project without enforcing project id in metadata:lint

* [eas-cli] Guard against faulty config objects

* update CHANGELOG.md

* docs(cli): fix keyword length typo

* refactor(cli): use optional chaining over ternary

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

* refactor(cli): drop non-null assertion in config resolver

* refactor(cli): drop implicit coercion in expressions

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

* refactor(cli): only initialize ajv when used

* refactor(cli): simplify error logging

* refactor(cli): move coascle to expodash as filter

* fix(cli): use proper empty check on readable errors

* refactor(cli): support non interactive in metadata lint

* refactor(cli): remove metadata context from lint command

* refactor(cli): split up metadata context into different methods

* refactor(cli): use profile instead of metadata path in config resolve methods

* refactor(cli): rename context to auth and simplify usage

* refactor(cli): move json schema files to top-level imports

* refactor(cli): use predicates in expodash filter instead of filter functions

* test(cli): update config tests to use mocked profile isntead

* test(cli): fix issue in filter predicate test

* [eas-cli] Reimplement the analytics manager

Co-authored-by: Dominik Sokal <[email protected]>
2022-10-24 18:09:11 +02:00
Cedric van Putten 91eb2c467b [eas-cli] Replace branch:list table with formatted fields (#1466)
* [eas-cli] Replace channel:view table with formatted fields

* update CHANGELOG.md

* [eas-cli] Add getBranchDescription method like update group
2022-10-24 15:57:41 +02:00
Cedric van Putten f237a7e074 [eas-cli] Replace device:create input summary table with format fields (#1465)
* [eas-cli] Replace device:create input summary table with format fields

* update CHANGELOG.md

* [eas-cli] Add device class to format device
2022-10-24 15:17:57 +02:00
Cedric van PuttenandDominik Sokal 242c3cffb6 [eas-cli] Replace secret:list table with format fields (#1464)
* [eas-cli] Replace secret:list table with format fields

* update CHANGELOG.md

* [eas-cli] Replace created at with update in secrets list

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

* Revert updated at

Co-authored-by: Dominik Sokal <[email protected]>
2022-10-24 14:59:55 +02:00
Dominik Sokal eee54af6ee Update CHANGELOG.md 2022-10-24 12:28:44 +02:00
Expo CI 86f86dbe86 update CHANGELOG.md 2022-10-24 10:26:53 +00:00
Dominik Sokal cc99bb3326 v2.5.1 2022-10-24 12:18:07 +02:00
Dominik Sokal 08dc52c631 Revert "feat: use versioned cli for export (#1460)"
This reverts commit 2aade4c0c8.
2022-10-24 12:17:47 +02:00
Dominik Sokal 51db45a861 revert #1460 (#1472)
* Revert "feat: use versioned cli for export (#1460)"

This reverts commit 2aade4c0c8.

* revert CHANGELOG.md change

* update CHANGELOG.md
2022-10-24 12:08:06 +02:00
Dominik Sokal 790853b3b2 Update CHANGELOG.md 2022-10-24 09:52:15 +02:00
Expo CI 23479acabc update CHANGELOG.md 2022-10-24 07:49:28 +00:00
Dominik Sokal de965e3693 v2.5.0 2022-10-24 09:41:08 +02:00
Dominik Sokal 5c3afc5141 upgrade eas-cli-local-build-plugin to 0.0.115 2022-10-24 09:40:50 +02:00
Dominik Sokal 50814723c5 [ENG-6183] add secret:push command (#1457)
* add secret:sync command

* address PR feedback

* address PR feedback
2022-10-21 09:52:09 +02:00
Will Schurman c6919e9fd6 [eas-cli] Add changelog entry for #1452 (#1468) 2022-10-20 10:29:08 -07:00
Evan Bacon 6b220f12ae chore: remove table logging in update:view (#1463)
* chore: remove table logging in update:view

* update CHANGELOG.md
2022-10-20 09:18:34 -07:00
Evan Bacon 2aade4c0c8 feat: use versioned cli for export (#1460)
* feat: use versioned cli

* update CHANGELOG.md

* allow EXPO_USE_LOCAL_CLI to disable local CLI
2022-10-19 13:52:37 -07:00
Evan Bacon f16c40a496 chore(updates): fix up install prompt language (#1462)
* chore: fixup install prompt

* update CHANGELOG.md
2022-10-19 13:44:39 -07:00
Brent Vatne aed6725d17 [eas-cli] Handle case where git is installed but not working properly (#1454) 2022-10-19 10:54:16 -07:00
Will Schurman ac6c240217 [eas-cli] Update init command to handle slug and owner (#1452) 2022-10-19 09:34:22 -07:00
Will Schurman 34ae7f5e0b [eas-cli] Clean up withAnalyticsAsync (#1449) 2022-10-18 09:25:14 -07:00
Dominik Sokal 40aa1c7d35 [ENG-5269] add changelog linter (#1451)
* [scripts] add simple CHANGELOG.md linter

* fix CHANGELOG.md

* run lint-changelog in CI
2022-10-18 12:56:03 +02:00
Will Schurman b128af464c [eas-cli] Initialize analytics consistently and move analytics into context (#1444) 2022-10-17 09:43:45 -07:00
Expo CI 1236152120 update CHANGELOG.md 2022-10-17 09:11:35 +00:00
Dominik Sokal 9201af566d v2.4.1 2022-10-17 10:57:02 +02:00
Dominik Sokal cf4eb7a4e4 [ENG-6721][submit] move error messages from CLI (#1443)
* remove submit errors from CLI

* update CHANGELOG.md
2022-10-17 09:45:53 +02:00
Dominik Sokal 3ca02ab740 update CHANGELOG.md 2022-10-14 14:36:43 +02:00
Dominik Sokal c05ef9ccca upgrade eas-cli-local-build-plugin to 0.0.114 2022-10-14 14:33:52 +02:00
Expo CI dccd249cb0 update CHANGELOG.md 2022-10-14 08:03:48 +00:00
Dominik Sokal 9e3840a987 v2.4.0 2022-10-14 09:55:46 +02:00
Dominik Sokal a32e98a72a handle new EAS Submit errors (#1440)
* handle new EAS Submit errors

* update CHANGELOG.md
2022-10-14 09:47:02 +02:00
Will Schurman 3496c30a2c Revert "[eas-cli] Fix command statistic analytics (#1439)" (#1442)
This reverts commit 38e4b65a8a.
2022-10-13 18:11:50 -07:00
Will Schurman 38e4b65a8a [eas-cli] Fix command statistic analytics (#1439) 2022-10-13 17:17:36 -07:00
Dominik Sokal 5e5273e6a4 [ENG-6652] automatically choose next version when releasing (#1433)
* automatically choose next version when releasing

* Update CONTRIBUTING.md
2022-10-13 09:23:27 +02:00
Will Schurman e1be834d62 [eas-cli] Update changelog (#1438) 2022-10-12 10:16:03 -07:00
Will Schurman 55e5ab465a [25/n][eas-cli] Move graphql and session into context (#1436) 2022-10-12 10:12:36 -07:00
Will Schurman 5be66f3963 [24/n][eas-cli] Start to move graphql into a context (#1435) 2022-10-12 10:11:48 -07:00
Expo CI 024c699637 update CHANGELOG.md 2022-10-10 11:42:55 +00:00
Szymon Dziedzic 52a4e9b78d v2.3.0 2022-10-10 13:32:41 +02:00
Szymon Dziedzic b3b0350556 [ENG-6571][eas-cli] use resource class flag to select M1 resource class (#1425)
* [eas-cli] use env variable to select M1 resource class

* [eas-cli] create `m1-experimental` option for `--resource-class` flag

* improvements after code review

* throw an error instead of using oclif/errors in resolveResourceClass function

* update CHANGELOG.md
2022-10-10 11:54:20 +02:00
Expo CI afd445721c update CHANGELOG.md 2022-10-05 09:25:36 +00:00
Dominik Sokal b36ac56851 v2.2.1 2022-10-05 11:14:27 +02:00
Dominik Sokal 2ff5dbe5fb actually read file when creating secret file (#1423)
* actually read file when creating secret file

* update CHANGELOG.md
2022-10-05 11:12:59 +02:00
Dominik Sokal d5fe7890cd use double quotes for app config fields 2022-10-05 09:26:03 +02:00
Quinlan JungandDominik Sokal 0c183b447c [cli] more robust platform check (#1417)
* [cli] more robust platform check

* update CHANGELOG.md

* pr feedback

* extra docs

* csv or number

* Update packages/eas-cli/src/project/ios/__tests__/target-test.ts

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

* update changelog, fix tsc

Co-authored-by: Dominik Sokal <[email protected]>
2022-10-05 00:11:41 +02:00
Expo CI b3c2ebb9e5 update CHANGELOG.md 2022-10-04 18:10:49 +00:00
Brent Vatne 5bc7bcbf87 v2.2.0 2022-10-04 10:37:18 -07:00
Brent Vatne a1d361db1d upgrade eas-cli-local-build-plugin to 0.0.110 2022-10-04 10:37:07 -07:00
Brent VatneandDominik Sokal 4a36f98abe [eas-cli] Add learn more link to project ID mismatch errors (#1420)
Co-authored-by: Dominik Sokal <[email protected]>
2022-10-04 10:36:07 -07:00
Quinlan Jung a518728bf0 [cli] surface context error with invalid eas.json (#1418) 2022-10-03 16:33:37 -07:00
Szymon Dziedzic 482b7f8f64 [ENG-4932][eas-cli] handle errors form new platform-specific kill switches (#1401)
* [eas-cli] handle errors form new platform-specific kill switches

* add learn more to error messages

* improve error message

* make error sentence correct

* update CHANGELOG.md
2022-10-03 11:34:06 +02:00
Bartosz Kaszubowski 639d5881f5 [eas-json] improve errors and error message formatting (#1414) 2022-09-30 13:39:42 +02:00
Wojciech Kozyra 0fc6cf8658 [eas-cli] Fix automatic configuration if eas.json does not exist. (#1415) 2022-09-29 13:26:14 +02:00
Bartosz Kaszubowski c7e13c25ab Revert "[eas-json] improve errors and error message formatting"
This reverts commit 77b5d63282.
2022-09-29 11:42:56 +02:00
Bartosz Kaszubowski 77b5d63282 [eas-json] improve errors and error message formatting 2022-09-29 11:35:32 +02:00
Jakov Glavina daded8d7d2 [eas-cli] Fix typo when failing to publish updates (#1408) 2022-09-28 13:14:59 -07:00
Will Schurman 5a21417c8d [23/21][eas-cli] Move context into class to alleviate the need for imports (#1413) 2022-09-28 10:31:36 -07:00
Will Schurman 9c3aa4c96d [22/21][eas-cli] Move some files around to avoid private functions (#1411) 2022-09-28 09:28:46 -07:00
Bartosz Kaszubowski 14481716d1 [eas-cli] DeviceUtils: fix typo in message (#1407) 2022-09-28 14:39:03 +02:00
kirb 5e8b109746 [eas-cli] Add pagination to device:* commands (#1381) 2022-09-27 16:11:48 -07:00
Will Schurman 996ef329eb [eas-cli] Update Changlog for recent stack land (#1409) 2022-09-27 14:09:07 -07:00
Will Schurman 0e93eb884f [21/21][eas-cli] Validate that owner and projectId and slug all are in alignment (#1405) 2022-09-27 14:04:53 -07:00
Will Schurman 958fce59e6 [20/n][eas-cli] Simplify submit context creation (#1403) 2022-09-27 14:04:00 -07:00
Will Schurman 595a841f21 [19/n][eas-cli] Reduce code duplication in project config context commands (#1402) 2022-09-27 14:02:57 -07:00
Will Schurman fd01b9393b [17/n] Move context-specific methods into context fields code to discourage misuse (#1394) 2022-09-27 14:01:57 -07:00
Will Schurman 61c16674d5 [16/n][eas-cli] Replace command option canRunOutsideProject with projectDir context (#1391) 2022-09-27 13:57:17 -07:00
Will Schurman 838701f0d1 [15/n][eas-cli] Remove allowUnauthenticated command configuration (#1390) 2022-09-27 13:56:31 -07:00
Will Schurman 3ca26392b7 [13/n][eas-cli] Use projectId context in remaining commands (#1388) 2022-09-27 13:55:03 -07:00
Will Schurman 40c0a0a5a1 [eas-cli] Add changelog entry for command context (#1404) 2022-09-26 12:14:53 -07:00
Will Schurman f412451aaf [12/n][eas-cli] Use new context for project ID (#1387) 2022-09-26 09:35:47 -07:00
Will Schurman e2d747aadb [11/n][eas-cli] Use new context for logged-in actor (#1384) 2022-09-26 09:27:09 -07:00
Will Schurman 5df5bd0aa5 [10/n][eas-cli] Add new on-demand context concept for commands (#1383) 2022-09-26 09:25:37 -07:00
Wojciech Kozyra 583fd24e33 [eas-cli] Increase max versionCode for Android (#1400) 2022-09-26 14:36:33 +02:00
Kim Brandwijk 12c67e525c [eas-cli] warn about channel configuration before build (#1397) 2022-09-26 11:25:20 +02:00
Will Schurman fd4a64cc30 [9/n][eas-cli] Account -> AccountFragment (#1380) 2022-09-22 13:30:15 -07:00
Will Schurman a77cb45692 [8/n][eas-cli] Remove findAccountByName (#1379) 2022-09-22 13:27:58 -07:00
Will Schurman 863ef1eeec [7/n][eas-cli] Improve eas init command (#1376) 2022-09-22 13:27:09 -07:00
kirb bc04241e0f [eas-cli] extract app lookup params (#1382) 2022-09-22 07:45:04 -07:00
Szymon Dziedzic 9f94ddb606 [eas-json] add Xcode 14 image (#1365)
* [eas-json] add XCode 14 image

* bump eas-build-job version

* fix eas.schema.json

* update default and latest images

* update eas-build-job package

* update CHANGELOG.md
2022-09-21 13:43:02 +02:00
Will Schurman 6b101614ba [eas-cli] Update changelog for recent series of stacked commits (#1377) 2022-09-19 15:54:15 -07:00
Will Schurman 811568737b [6/n][eas-cli] Clean up some remaining project utils methods (#1373) 2022-09-19 15:31:07 -07:00
Will Schurman 36b5e0cee8 [5/n][eas-cli] Remove getProjectFullNameAsync (#1372) 2022-09-19 15:28:34 -07:00
Will Schurman 8681437081 [4/n][eas-cli] Remove getProjectAccountName (#1371) 2022-09-19 15:26:41 -07:00
Will Schurman 3e79916462 [2/n][eas-cli] Remove rest of getProjectAccountNameAsync callsites (#1369) 2022-09-19 15:24:13 -07:00
Will Schurman 0613b6c2cd [eas-cli][1/n] Remove some of getProjectAccountNameAsync (#1368) 2022-09-19 15:22:42 -07:00
kirb 63ed6f2b4e [eas-cli] Use confirmation when canceling builds (#1366) 2022-09-19 09:37:47 -07:00
Will Schurman 9985d618ef [eas-cli] Make optional nonInteractive arguments non-optional in utility functions (#1367) 2022-09-19 08:59:35 -07:00
Szymon Dziedzic 46c0ee6423 [eas-cli][ENG-6325] provide suggestions for developers when archive size is large (#1363)
* [eas-cli] provide suggestions for developers when archive size is large

* use learn more function

* apply suggestion from review

* apply suggestions from code review

* update CHANGELOG.md
2022-09-19 10:23:26 +02:00
Will Schurman 5585874c21 [eas-cli] Prompt before configuring EAS project (#1356) 2022-09-16 13:00:26 -07:00
kirb b288c31216 [eas-cli] add pagination for build queries (#1353) 2022-09-16 11:43:57 -07:00
kirb 738c8a21c8 [eas-cli] Add pagination to channel commands (#1352) 2022-09-16 11:32:57 -07:00
Dominik SokalandJuwan Wheatley ff5669ff30 [ENG-4730] support uploading files to EAS Secret (#1354)
* regenerate graphql code

* add support for uploading secret files to EAS Secret

* address PR feedback

* address PR feedback

* regenerate graphql code

* Update packages/eas-cli/src/commands/secret/create.ts

Co-authored-by: Juwan Wheatley <[email protected]>

* address PR feedback

* address PR feedback

* update CHANGELOG.md

Co-authored-by: Juwan Wheatley <[email protected]>
2022-09-16 15:47:22 +02:00
kirb fdf64a8673 [eas-cli] Standardize GQL Update type schema using Fragment (#1348) 2022-09-14 17:00:06 -07:00
kirb 2c7b442f06 [eas-cli] use paginated query API for update commands (#1323) 2022-09-14 16:49:46 -07:00
Dominik Sokal 9c09cb9034 Update CHANGELOG.md 2022-09-12 09:28:21 +02:00
Dominik Sokal e52d14586f [ENG-6129] support json5 in eas.json (#1350)
* support json5 in eas.json

* address PR feedback

* address more PR feedback

* fix test
2022-09-12 09:27:01 +02:00
Szymon Dziedzic 0f7646868f [eas-cli][ENG-6229] add -e as a shortcut for --profile flag (#1342)
* [eas-cli] add `-e` flag as a shortcut for `--profile` flag

* revert readme changes

* add description for profile flag on config command

* update CHANGELOG.md
2022-09-07 19:36:23 +02:00
Justin McBride 6fe4511280 Small updates to the language of the Device Create command (#1346) 2022-09-07 09:28:53 +02:00
Expo CI efc1551d62 update CHANGELOG.md 2022-09-05 12:11:53 +00:00
Dominik Sokal 0cc5b74081 v2.1.0 2022-09-05 14:02:46 +02:00
Wojciech Kozyra 6c6f9b079d [eas-cli] resolve paths from root when checking for gitignored values (#1336) 2022-09-05 14:00:26 +02:00
Dominik Sokal 7da0db0237 Update CHANGELOG.md 2022-09-05 13:56:02 +02:00
Dominik SokalandKim Brandwijk 66f916ac02 [ENG-3865] add support for uploading build artifacts (#1321)
* add support for uploading build artifacts

* handle auto-renaming artifactPath -> applicationArchivePath

* update eas.schema.json

* upgrade @expo/eas-build-job

* rename buildArtifactsPaths -> buildArtifactPaths

* Apply suggestions from code review

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

* fix ts error

* address PR feedback

Co-authored-by: Kim Brandwijk <[email protected]>
2022-09-05 13:55:07 +02:00
Bartosz Kaszubowski 0e500ec827 [eas-cli] bump oclif help plugin (#1341)
* [eas-cli] bump oclif help plugin

* update CHANGELOG.md

* cleanup changelog
2022-09-05 13:54:26 +02:00
Dominik Sokal 00f1d24aa5 [ENG-6186] add "eas open" command (#1337)
* add "eas open" command

* update CHANGELOG.md
2022-09-05 11:53:56 +02:00
Dominik Sokal b909409c54 Update CHANGELOG.md 2022-09-05 10:28:46 +02:00
HBiedeandKim Brandwijk d18d7a6e21 Fix typos (#1340)
* Fix typos

* Revert “re-authentication”

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

Co-authored-by: Kim Brandwijk <[email protected]>
2022-09-05 10:25:14 +02:00
Quinlan Jung 0131cdbf4b [ENG-6070][cli] generate tvOS credentials for adhoc and prod (#1325)
* [cli] generate tvOS credentials for adhoc and prod

* update CHANGELOG.md

* pr feedback: dsokal

* pr feedback: wojtek

* fix tsc

* tricky mocks
2022-09-02 00:45:09 +02:00
Will Schurman 114190c52e [eas-cli] Add noImplicitOverride to tsconfigs (#1327) 2022-09-01 09:23:08 -07:00
HBiede a06352d150 [eas-cli] Fix viewing branch list when a branch has no associated platforms (#1326) 2022-09-01 09:12:22 -07:00
Expo CI 1969eea8a7 update CHANGELOG.md 2022-09-01 11:40:30 +00:00
Dominik Sokal 8c758fa413 v2.0.0 2022-09-01 13:30:03 +02:00
Dominik Sokal 4b2fb24d1d Update CHANGELOG.md 2022-09-01 13:24:46 +02:00
Dominik Sokal 1637573d72 fix typo in promptToConfigurePushNotifications (#1332)
* fix typo in promptToConfigurePushNotifications

* address PR feedback
2022-09-01 13:20:18 +02:00
Will Schurman 367477f75b [eas-cli] Fix dynamic config update warning for update:configure (#1322) 2022-08-31 14:24:40 -07:00
Expo CI f5d44a08ec update CHANGELOG.md 2022-08-29 09:58:55 +00:00
Dominik Sokal d4ffb07402 v1.2.0 2022-08-29 11:39:02 +02:00
Evan Bacon 0b2b6ddc14 feat(eas-cli): add 2022 Apple capabilities (#1307)
* feat(eas-cli): add 2022 Apple capabilities

* updated tests

* updated tests and third-party

* update CHANGELOG.md

* updated names

* Update CHANGELOG.md
2022-08-29 11:37:00 +02:00
Szymon Dziedzic 0cff5ef83f [eas-cli] warn about EAS outages (#1312)
* [eas-cli] warn users about EAS Build outages

* [eas-cli] warn users about EAS Submit outages

* [eas-cli] show warnings about EAS outages as a first thing when running build and submit commands

* [eas-cli] don't return null as a result of StatuspageServiceQuery

* [eas-cli] remove unneccessary if check

* [eas-cli] rename warnAboutEasOutagesAsync function to maybeWarnAboutEasOutagesAsync and remove please word from output

* [eas-cli] warn about outages when running `eas update` command

* [eas-cli] improve outage warning communicate

* [eas-cli] move maybeWarnAboutEasOutagesAsync call after flags and project dir validation

* [eas-cli] minor refactor after code review

* [eas-cli] change warning message on EAS outage

* [eas-cli] put platform selection and validation logic into ensurePlatformSelectedAsync function

* update CHANGELOG.md

* [eas-cli] use ensurePlatformSelectedAsync in submit command
2022-08-29 11:29:51 +02:00
Dominik Sokal 396c45269e add eas.json option to skip push notifications setup (#1315)
* add eas.json option to skip push notifications setup

* address PR feedback

* update CHANGELOG.md
2022-08-29 10:19:43 +02:00
Brent Vatne 98ad9798c5 Copy changes to EAS Submit output (#1318)
* Change e-mail -> email

* Update link to build to point to TestFlight

* update CHANGELOG.md
2022-08-29 09:30:20 +02:00
Dominik Sokal b4741293ba improve instructions on setting eas.projectId in app.json (#1316)
* improve instructions on setting eas.projectId in app.json

* address PR feedback

* update CHANGELOG.md
2022-08-26 09:49:41 +02:00
Dominik SokalandBrent Vatne 43a6aa826b [ENG-6125] print warning when eas-cli is installed as dependency (#1310)
* print warning when eas-cli is installed as dependency

* address PR feedback

* Apply suggestions from code review

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

* PR feedback

* Apply suggestions from code review

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

* update CHANGELOG.md

Co-authored-by: Brent Vatne <[email protected]>
2022-08-26 09:24:47 +02:00
Wojciech Kozyra e0a9384e56 update CHANGELOG 2022-08-25 10:21:57 +02:00
Dominik Sokal b5ca8c63d9 add new common submit error 2022-08-25 10:18:10 +02:00
Will Schurman c98401131a [eas-cli] Fix EnvironmentSecretsQuery param name (#1305) 2022-08-24 08:18:16 -07:00
Will Schurman c1cd0f584f [eas-cli] Use AppQuery instead of ProjectQuery (#1309) 2022-08-24 08:16:04 -07:00
Will Schurman f82aae7fc9 [eas-cli] Replace promptToCreateProjectIfNotExistsAsync with getProjectIdAsync (#1303) 2022-08-24 08:14:06 -07:00
Dominik Sokal c349ad7a40 Update CHANGELOG.md 2022-08-24 09:07:03 +02:00
Expo CI 3500a4d5ec update CHANGELOG.md 2022-08-23 19:43:43 +00:00
Quinlan Jung 149d07b28e v1.1.1 2022-08-23 12:33:01 -07:00
Quinlan Jung e248bf3fc3 [ENG-6057][cli] api key creation: exponential backoff for slow apple propagation (#1302)
* [cli] exponential backoff for slow apple propagation

* sdf

* update CHANGELOG.md

* pr feedback
2022-08-23 21:25:48 +02:00
Wojciech Kozyra f07b692ead [eas-cli] Handle trailing backslash in gitignore (#1306)
* [eas-cli] Handle trailing backslash in gitignore

* update CHANGELOG.md
2022-08-23 14:18:06 +02:00
Szymon Dziedzic ebfe69535f [eas-json] add support for autoIncrement option at root of build profile (#1298)
* [eas-json] support `autoIncrement` at root of build profile

* [eas-json] Start `autoIncrement` markdown enum description with capital letter

* update CHANGELOG.md

* [eas-json] use interfaces instead of types

* [eas-json] fix issues with types and improve schema descriptions

* [eas-json] remove unneccessary comment

* [eas-json] make EasJsonBuildProfile interface again

* [eas-json] remove unneccessary semicolon

* [eas-json] use only boolean values for autoIncrement flag

* [eas-json] improve schema description for autoIncrement option

* [eas-json] remove change from changelog

* update CHANGELOG.md

* improve changelog entry
2022-08-23 11:36:06 +02:00
Expo CI 58dbee8dee update CHANGELOG.md 2022-08-23 08:10:02 +00:00
Dominik Sokal feca0dc721 v1.1.0 2022-08-23 10:02:07 +02:00
Will Schurman 81831d156d [eas-cli] Prevent throwing dynamic app config write error when configuring project id (#1301) 2022-08-22 17:08:23 -07:00
Jon Samp 55c859cea7 [eas-cli] Removes "please" from output (#1250)
* Removes "please" from output

* updates changelog

* incorporate feedback from kim
2022-08-22 13:18:57 -04:00
Bartosz Kaszubowski 613748b2f0 [cli] remove unused dependencies (#1297) 2022-08-22 11:37:24 +02:00
Bartosz Kaszubowski bbd4bca83d [cli] correct Changelog (#1295) 2022-08-22 10:11:38 +02:00
Bartosz KaszubowskiandDominik Sokal 007866f278 [cli] Credentials: support platform flag (#1277)
Co-authored-by: Dominik Sokal <[email protected]>
2022-08-22 10:03:53 +02:00
Expo CI 1d2e9818ed update CHANGELOG.md 2022-08-22 08:02:35 +00:00
Dominik Sokal 34deafe440 v1.0.0 2022-08-22 09:55:27 +02:00
Dominik Sokal 55d8b650d3 remove timeout when waiting for build / submit (#1289)
* remove timeout when waiting for build / submit

* update CHANGELOG.md
2022-08-22 09:43:57 +02:00
Will Schurman 3ef6802d55 [eas-cli] Limit the number of SignedAssetUploadSpecifications fetched at a time (#1287) 2022-08-19 10:11:26 -07:00
Dominik Sokal e9484ffa5a update CODEOWNERS 2022-08-19 13:51:38 +02:00
Will Schurman c2b50a3a08 [eas-cli] Remove git merge artifact from CHANGELOG (#1285) 2022-08-17 11:31:02 -07:00
Will Schurman 06bb202bdf [eas-cli] Improve reliability of update asset presigned upload requests (#1278) 2022-08-17 10:59:18 -07:00
Will Schurman 64b67e9ea7 [eas-cli] Configure EAS project ID in app config in all cases (#1282) 2022-08-17 10:12:14 -07:00
faf1e90bed [eas-cli] Add js metadata config file support (#1270)
* [eas-cli] Add js metadata config file support

* [eas-cli] Add js metadata config file download support

* update CHANGELOG.md

* [eas-cli] Only save static files when pulling metadata

* [eas-cli] Support config functions and async functions in metadata

* [eas-cli] Tweak the metadata logs

* [eas-cli] Improve the dynamic config return validation

* [eas-cli] Apply improved language suggestions from the pros

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

* [eas-cli] Apply code changes from review

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

* [eas-cli] Split out load config logging and move validation to single method

* [eas-cli] Fix missed renamed function

Co-authored-by: Dominik Sokal <[email protected]>
Co-authored-by: Wojciech Kozyra <[email protected]>
2022-08-17 16:16:52 +02:00
c8935e199b [workspace] improve DX while working on Windows (#1272)
Co-authored-by: Dominik Sokal <[email protected]>
Co-authored-by: Wojciech Kozyra <[email protected]>
2022-08-16 15:10:55 +02:00
Wojciech Kozyra af76a9ce48 [eas-cli] remove shell check (#1281) 2022-08-16 13:54:22 +02:00
Wojciech Kozyra 3986e2fbb7 [eas-cli] remove install script (#1280)
* [eas-cli] remove installer

* update CHANGELOG.md
2022-08-16 13:27:13 +02:00
Dominik Sokal 1f97e8cc1c [eas-cli] init git repo in monorepo root (#1279)
* [eas-cli] init git repo in monorepo root

* update CHANGELOG.md
2022-08-16 11:02:05 +02:00
dependabot[bot] be1e2d7189 Bump simple-plist from 1.1.1 to 1.3.1 (#1139)
Bumps [simple-plist](https://github.com/wollardj/simple-plist) from 1.1.1 to 1.3.1.
- [Release notes](https://github.com/wollardj/simple-plist/releases)
- [Commits](https://github.com/wollardj/simple-plist/compare/v1.1.1...v1.3.1)

---
updated-dependencies:
- dependency-name: simple-plist
  dependency-type: indirect
...

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-16 10:13:25 +02:00
Evan Bacon 8c4d7a6d0e fix: log binding was incorrect after ora instance (#1113)
* fix: log binding was incorrect after ora instance

* update CHANGELOG.md
2022-08-15 12:50:17 +02:00
Bartosz Kaszubowski 0849bf80b0 [workspace] bump Node used by oclif (#1273) 2022-08-12 15:20:40 +02:00
Expo CI 57ea4cd8c2 update CHANGELOG.md 2022-08-12 11:43:54 +00:00
Wojciech Kozyra 4c95900299 v0.60.0 2022-08-12 13:33:40 +02:00
Wojciech KozyraandDominik Sokal 94238f3274 [eas-cli] Add warning about unsupported policy. (#1271)
* [eas-cli] Add warning about unsupported policy.

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

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

* update CHANGELOG.md

Co-authored-by: Dominik Sokal <[email protected]>
2022-08-12 13:20:06 +02:00
Wojciech Kozyra 361230ffd2 [eas-cli] add appVersion runtime policy (#1267)
* Update packages

* update CHANGELOG.md
2022-08-12 11:33:49 +02:00
Cedric van Putten 88065c2837 [eas-json] Add metadata path to ios submit profile (#1269)
* [eas-json] Add metadata path to ios submit profile

* update CHANGELOG.md
2022-08-12 10:19:33 +02:00
dependabot[bot] 7f7b59ccfc Bump ejs from 3.1.6 to 3.1.8 (#1125)
Bumps [ejs](https://github.com/mde/ejs) from 3.1.6 to 3.1.8.
- [Release notes](https://github.com/mde/ejs/releases)
- [Changelog](https://github.com/mde/ejs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mde/ejs/compare/v3.1.6...v3.1.8)

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

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-11 12:14:08 +02:00
Expo CI 2a9f9d4695 update CHANGELOG.md 2022-08-10 10:15:59 +00:00
Dominik Sokal 8281a68e2e v0.59.0 2022-08-10 12:03:38 +02:00
Dominik Sokal cf47843712 upgrade dependencies (#1262)
* upgrade dependencies

* regenerate graphql code

* install ts-node

* update CHANGELOG.md
2022-08-10 11:54:17 +02:00
Wojciech Kozyra 7901fad195 yarn lint 2022-08-10 10:45:57 +02:00
Wojciech Kozyra df346764c8 [eas-cli] Validate only if using remote version source (#1261)
* [eas-cli] Validate only if using remote version source

* update CHANGELOG.md
2022-08-10 10:36:42 +02:00
Wojciech Kozyra 8024d33b25 [eas-cli] Improve wording for version bumps (#1260)
* [eas-cli] Improve wording for version bumps

* update CHANGELOG.md
2022-08-10 10:31:40 +02:00
Dominik Sokal e41d9f6430 Update CHANGELOG.md 2022-08-09 15:59:36 +02:00
Dominik Sokal 822313e90e upgrade eas-cli-local-build-plugin to 0.0.99 2022-08-09 15:56:40 +02:00
Expo CI 6c4f4c0049 update CHANGELOG.md 2022-08-09 11:44:42 +00:00
Dominik Sokal bb249345a7 v0.58.0 2022-08-09 13:31:08 +02:00
Dominik Sokal c1716e3b5a Update CHANGELOG.md 2022-08-09 12:11:05 +02:00
Dominik Sokal 40a6f3dda3 [ENG-3865] pass build metadata to local plugin (#1256)
* pass build metadata to local plugin

* upgrade eas-cli-local-build-plugin to 0.0.98
2022-08-09 12:05:04 +02:00
Wojciech Kozyra 8cadda2099 [eas-cli] add submit info to json only output (#1246)
* add submit info to json only output

* review feedback

* update CHANGELOG.md
2022-08-09 11:09:44 +02:00
Brent Vatne 8837214db2 [eas-cli] Swallow error when unable to track file in no commit workflow (#1109)
* [eas-cli] Swallow error when unable to track file in no commit workflow

* Include a warning when failing to track a file with GitNoCommitClient

* update CHANGELOG.md
2022-08-06 16:34:22 -07:00
kirb 1812b85e39 [eas-cli] add asset limit warning on publish flow (#1243) 2022-08-05 11:39:00 -07:00
Cedric van Putten 5024164408 [eas-cli] Add version selector to store config for metadata (#1194)
* [eas-cli] Add version selector to store config for metadata

* [eas-cli] Add version snippet to emphasize on app version

* [eas-cli] Add the missing phased release nock endpoints
2022-08-04 07:53:24 -07:00
Wojciech Kozyra b7983f4998 [eas-json] stop including sources in the npm package (#1248)
* [eas-json] Stop including sources in npm package

* update CHANGELOG.md
2022-08-04 15:43:00 +02:00
Wojciech Kozyra 7eea81ef83 [eas-json] move eas.json schema to eas-cli repo (#1247) 2022-08-04 15:05:05 +02:00
Cedric van Putten 5b9b4f9a2b [eas-cli] Finalize the store config release properties (#1191)
* [eas-cli] Clean up the store config release options

* [eas-cli] Add basic implementation of phased release

* [eas-cli] Fix outdated config writer tests
2022-08-04 03:04:09 -07:00
Cedric van Putten f862b7744b [eas-cli] Remove eas metadata preview until implementation is done (#1195) 2022-08-04 02:39:55 -07:00
Dominik Sokal e0b38f79dc [ENG-5893] add runWithNoWaitFlag and runFromCI to build metadata (#1245)
* generate graphql code

* add runWithNoWaitFlag and runFromCI to metadata

* send analytics events

* address PR feedback

* address more PR feedback

* upgrade @expo/eas-build-job
2022-08-04 09:52:00 +02:00
Wojciech Kozyra a709b9fd23 [eas-cli] managed versions support (#1232) 2022-08-03 12:52:26 +02:00
Expo CI ad2109103a update CHANGELOG.md 2022-08-03 07:48:35 +00:00
Dominik Sokal f797ae1c0a v0.57.0 2022-08-03 09:38:31 +02:00
Dominik Sokal f0287fab8a Update CHANGELOG.md 2022-08-03 09:37:14 +02:00
kirb 68a2b14e8b [eas-cli] disallow .only from being commited (#1242) 2022-08-02 09:38:47 -07:00
Dominik Sokal 04e55c33c1 [ENG-5836] add git commit message to metadata (#1241)
* generate graphql code

* upgrade @expo/eas-build-job

* add git commit message to build metadata
2022-08-01 13:35:23 +02:00
kirb 95a7137caa [eas-cli] Add unified pagination API for branch commands (#1213) 2022-07-29 18:51:52 -07:00
Dominik Sokal f9978baba2 [ENG-5836] add --message param to eas build (#1237)
* generate graphql code

* [eas-cli] allow specifying build message

* upgrade @expo/eas-build-job

* update CHANGELOG.md
2022-07-29 10:41:55 +02:00
Dominik Sokal 2023f6161d [ENG-5622] allow importing devices from Apple Developer Portal (#1236)
* allow importing devices from Apple Developer Portal

* update CHANGELOG.md
2022-07-29 10:30:18 +02:00
Expo CI a4f35466c3 update CHANGELOG.md 2022-07-28 09:53:15 +00:00
Dominik Sokal 5f73b0a2d7 v0.56.0 2022-07-28 11:43:00 +02:00
James Ide d194f18b8e Set Kirby and Will as codeowners for EAS Update CLI (#1233) 2022-07-26 09:22:45 -07:00
Wojciech Kozyra 17e1a145e8 [eas-cli] Fix newlines in cli deprecation message (#1231)
* [eas-cli] Fix newlines in cli deprecation message

* Update CHANGELOG
2022-07-26 10:46:09 +02:00
Kim Brandwijk 7ebb547fd9 [eas-cli] Invoke export with sourcemaps on update (#1228) 2022-07-22 12:47:37 +02:00
Wojciech Kozyra 6eca9ad564 [eas-cli] Add eas build:version:sync command (#1219) 2022-07-21 16:04:15 +02:00
Dominik Sokal 229f00a8ed Update CHANGELOG.md 2022-07-21 10:25:32 +02:00
Jérémy Barbet 2bbf7f2b6d chore: add better message on how to self-upgrade eas-cli (#1222)
* fix: make sure we add a line when needed while runing commands

* chore: add instruction on how to upgrade + remove ending new line

* chore: don't make the * bold
2022-07-21 10:24:10 +02:00
Dominik SokalandBrent Vatne d578abc69a [ENG-5017] display queue type when waiting (#1217)
* display queue type when waiting

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

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

* update CHANGELOG.md

Co-authored-by: Brent Vatne <[email protected]>
2022-07-20 09:36:22 +02:00
Wojciech Kozyra 89253f5767 [eas-cli] Add eas build:version:set command (#1209) 2022-07-18 13:26:46 +02:00
Kim Brandwijk dd5afd354a [cli] Change wording on build warning for bundle id/package name (#1211) 2022-07-16 20:43:36 +02:00
Dominik Sokal bdcc768354 rename uploadAsync -> uploadFileAtPathToS3Async (#1216) 2022-07-15 12:13:18 +02:00
Dominik Sokal 9cd2bafac5 fix CHANGELOG bot (#1215)
* upgrade node to 18.6.0 and yarn to 1.22.19

* upgrade dependencies
2022-07-15 12:13:09 +02:00
Dominik Sokal 69515ab47b fix S3 upload URLs (#1207) 2022-07-13 13:00:30 +02:00
Will Schurman ac5f7ab186 [eas-cli] Show spinner instead of silently timing out during asset upload (#1206) 2022-07-12 16:41:51 -07:00
Wojciech Kozyra 265c7eacc5 [eas-json] Increase max number of chained "extends" in build profiles (#1208) 2022-07-12 13:46:40 +02:00
Expo CI 98229581ca update CHANGELOG.md 2022-07-12 00:36:36 +00:00
Will Schurman a718fd10d3 v0.55.1 2022-07-11 17:25:37 -07:00
Will Schurman 78a63b8ca1 [eas-cli] Fix asset upload crash (#1205) 2022-07-11 16:50:09 -07:00
Cedric van Putten 7487f413de [eas-cli] Temporarily increase coverage memory to stabilize CI (#1196) 2022-07-11 19:40:32 +02:00
Expo CI da5f4e644a update CHANGELOG.md 2022-07-11 10:28:38 +00:00
Dominik Sokal be38117012 v0.55.0 2022-07-11 11:49:04 +02:00
Wojciech Kozyra 8f3314f188 [eas-cli] Keep using AndroidManifest.xml as a workflow marker (#1204) 2022-07-11 11:05:36 +02:00
Wojciech KozyraandDominik Sokal 31b6cc9ab6 [eas-cli] allow applicationId override (#1203)
* allow applicationId override

* Apply suggestions from code review

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

* update CHANGELOG.md

* update CHANGELOG.md

Co-authored-by: Dominik Sokal <[email protected]>
2022-07-11 10:19:06 +02:00
Dominik Sokal 04a8b08f59 [ENG-4579][scripts] convert to ESM (#1192)
* upgrade typescript to 4.7.4

* [scripts] add extensions to imports

* [scripts] configure as ESM

* [scripts] fix after converting to ESM

* [scripts] fix lint

* [scripts] require node >= 14.16

* [scripts] add node: protocol

* add scripts/build to .gitignore

* [scripts] add typecheck script

* add workflow for testing scripts

* address PR feedback
2022-07-04 11:50:57 +02:00
Cedric van Putten 8bce9b19b2 [eas-cli] Remove deprecated idfa and price tier from store config (#1193)
* [eas-cli] Drop deprecated idfa from store config

* [eas-cli] Drop deprecated price tier from store config

* update CHANGELOG.md
2022-07-02 02:52:35 +02:00
Cedric van Putten d3d6d41f9f [eas-cli] Improve store config categories configuration (#1187)
* [eas-cli] Improve EAS metadata category configuration

* [eas-cli] Update category types for apple store configuration

* [eas-cli] Add tests for the behavior of the categories from store configuration

* [eas-cli] Update apple store config with new categories

* [eas-cli] Simplify parent vs normal categories in schema

* [eas-cli] Fix serialization issue with empty secondary sub category

* [eas-cli] Relax the apple category types to allow strings

* [eas-cli] Fix an issue in reader related to the category types

* [eas-cli] Add comments about string vs category enum

* update CHANGELOG.md
2022-07-02 02:02:37 +02:00
Brent Vatne 0534328dfc [github] Add issue triage workflows 2022-06-30 17:45:44 -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
587 changed files with 37987 additions and 15220 deletions

No files matched your search

+4
View File
@@ -1 +1,5 @@
* text eol=lf
*.png binary
*.jpg binary
**/yarn.lock linguist-generated
+1 -1
View File
@@ -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
@@ -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:
-20
View File
@@ -1,20 +0,0 @@
name: Installer tests
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize]
jobs:
test:
name: Run the installer
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v2
- run: bash bin/install.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: eas --version
+146
View File
@@ -0,0 +1,146 @@
name: Issue Triage
on:
issues:
types: [labeled]
jobs:
needs-repro:
runs-on: ubuntu-20.04
if: "${{ contains(github.event.label.name, 'incomplete issue: missing or invalid repro') }}"
steps:
- uses: actions/github-script@v3
with:
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Hi there! It looks like your issue requires a minimal reproducible example, but it is invalid or absent. Please prepare such an example and share it in a new issue.
**The best way to get attention to your issue is to provide a clean and easy way for a developer to reproduce the issue on their own machine.** Please do not provide your entire project, or a project with more code than is necessary to reproduce the issue.
A side benefit of going through the process of narrowing down the minimal amount of code needed to reproduce the issue is that you may get lucky and discover that the bug is due to a mistake in your application code that you can quickly fix on your own.
### Resources
- ["How to create a Minimal, Reproducible Example"](https://stackoverflow.com/help/minimal-reproducible-example)
- ["How to narrow down the source of an error"](https://expo.fyi/manual-debugging)
### Common concerns
#### "I've only been able to reproduce it in private, proprietary code"
You may not have spent enough time narrowing down the root cause of the issue. Try out the techniques discussed in this [manual debugging guide](https://expo.fyi/manual-debugging) to learn how to isolate the problem from the rest of your codebase.
#### "I didn't have time to create one"
That's understandable, it can take some time to prepare. We ask that you hold off on filing an issue until you are able to fully complete the required fields in the issue template.
#### "You can reproduce it by yourself by creating a project and following these steps"
This is useful knowledge, but it's still valuable to have the resulting project that is produced from running the steps, where you have verified you can reproduce the issue.
`})
github.issues.update({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed'
})
needs-info:
runs-on: ubuntu-20.04
if: "${{ contains(github.event.label.name, 'incomplete issue: missing info') }}"
steps:
- uses: actions/github-script@v3
with:
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Hello! There isn't enough information provided in your issue description for us to be able to help you. The best way to get help is to provide information to enable other developers to understand the issue and reproduce it on their own machines.
Please create a new issue and fill out the entire issue template to the best of your ability. Refer to the following resources for more information on how to create a good issue report.
### Resources
- ["How do I ask a good question?"](https://stackoverflow.com/help/how-to-ask)
- ["How to create a Minimal, Reproducible Example"](https://stackoverflow.com/help/minimal-reproducible-example)
- ["How to narrow down the source of an error"](https://expo.fyi/manual-debugging)
`})
github.issues.update({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed'
})
issue-accepted:
runs-on: ubuntu-20.04
if: github.event.label.name == 'issue accepted'
steps:
- uses: actions/github-script@v3
with:
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Thank you for filing this issue!
This comment acknowledges we believe this may be a bug and there’s enough information to investigate it.
However, we can’t promise any sort of timeline for resolution. We prioritize issues based on severity, breadth of impact, and alignment with our roadmap. If you’d like to help move it more quickly, you can continue to investigate it more deeply and/or you can open a pull request that fixes the cause.
`})
question:
runs-on: ubuntu-20.04
if: "${{ contains(github.event.label.name, 'invalid issue: question') }}"
steps:
- uses: actions/github-script@v3
with:
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Hello! Our GitHub issues are reserved for bug reports.
If you have a question about Expo or related tools, please post on our forums at https://forums.expo.dev/ or join our Discord at https://chat.expo.dev/.
### Resources
- ["How do I ask a good question?"](https://stackoverflow.com/help/how-to-ask)
- ["Join the community"](https://docs.expo.dev/next-steps/community/)
`})
github.issues.update({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed'
})
feature-request:
runs-on: ubuntu-20.04
if: "${{ contains(github.event.label.name, 'invalid issue: feature request') }}"
steps:
- uses: actions/github-script@v3
with:
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Hello! Our GitHub issues are reserved for bug reports.
If you would like to request a feature, please post to https://expo.canny.io/feature-requests.
`})
github.issues.update({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed'
})
+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'
+26
View File
@@ -0,0 +1,26 @@
name: Test scripts
defaults:
run:
working-directory: scripts
on:
push:
branches: [main]
paths:
- 'scripts/**'
pull_request:
paths:
- 'scripts/**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 18
- run: yarn install --frozen-lockfile --check-files
- run: yarn typecheck
+9 -3
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:
@@ -27,8 +27,14 @@ jobs:
if: ${{ !matrix.coverage }}
- run: yarn test --coverage
if: ${{ matrix.coverage }}
env:
# Oclif seems to cause OOM errors when testing with coverage enabled.
# Remove this whenever we solved those OOM errors.
NODE_OPTIONS: --max-old-space-size=4096
- run: yarn lint --max-warnings=0
- run: shellcheck bin/*.sh
- run: yarn install --frozen-lockfile --check-files
working-directory: ./scripts
- run: yarn lint-changelog
- uses: codecov/codecov-action@v1
if: ${{ matrix.coverage }}
with:
+2
View File
@@ -2,10 +2,12 @@
packages/*/build
packages/eas-cli/dist
packages/eas-cli/tmp
scripts/build
# Code editors
.idea
.history
.vscode
# macOS
.DS_Store
+2 -1
View File
@@ -4,5 +4,6 @@
"singleQuote": true,
"bracketSameLine": true,
"trailingComma": "es5",
"arrowParens": "avoid"
"arrowParens": "avoid",
"endOfLine": "auto"
}
+420 -23
View File
@@ -8,10 +8,407 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Add `eas build:run` support for Android. ([#1485](https://github.com/expo/eas-cli/pull/1485) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Only create a new channel when update branch is new. ([#1507](https://github.com/expo/eas-cli/pull/1507) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
- Better wording in eas submit. ([#1527](https://github.com/expo/eas-cli/pull/1527) by [@dsokal](https://github.com/dsokal))
- Upload project sources for EAS Build and archives for EAS Submit to GCS. ([#1524](https://github.com/expo/eas-cli/pull/1524) by [@wkozyra95](https://github.com/wkozyra95))
- Add a better error message for when an invalid `image` is set in `eas-json`. ([#1531](https://github.com/expo/eas-cli/pull/1531) by [@szdziedzic](https://github.com/szdziedzic))
- Remove internal expo id from device formatter. ([#1541](https://github.com/expo/eas-cli/pull/1541) by [@dsokal](https://github.com/dsokal))
## [2.7.1](https://github.com/expo/eas-cli/releases/tag/v2.7.1) - 2022-11-16
### 🐛 Bug fixes
- Use envs from the build profile to resolve credentials in `eas credentials`. ([#1520](https://github.com/expo/eas-cli/pull/1520) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Print prompt message when failing because of the non-interactive shell. ([#1523](https://github.com/expo/eas-cli/pull/1523) by [@wkozyra95](https://github.com/wkozyra95))
- Add SDK version to the analytics build events. ([#1529](https://github.com/expo/eas-cli/pull/1529) by [@wkozyra95](https://github.com/wkozyra95))
- Add Xcode 14.1 image to `eas-json`. ([#1511](https://github.com/expo/eas-cli/pull/1511) by [@szdziedzic](https://github.com/szdziedzic))
## [2.7.0](https://github.com/expo/eas-cli/releases/tag/v2.7.0) - 2022-11-11
### 🎉 New features
- Support uploading different platform combinations in `eas update`. ([#1461](https://github.com/expo/eas-cli/pull/1461) by [@EvanBacon](https://github.com/EvanBacon))
- Add Git commit to EAS Update record. ([#1499](https://github.com/expo/eas-cli/pull/1499) by [@fiberjw](https://github.com/fiberjw))
- Use local CLI for `expo export` in SDK +46. ([#1474](https://github.com/expo/eas-cli/pull/1474) by [@EvanBacon](https://github.com/EvanBacon))
- Validate icon PNGs before running Android build. ([#1477](https://github.com/expo/eas-cli/pull/1477) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fixed the values of the distribution enum on the build profile schema receiving the incorrect descriptions. ([#1504](https://github.com/expo/eas-cli/pull/1504) by [@macksal](https://github.com/macksal))
- Remove hard dependency on expo package being installed to init a project. ([#1517](https://github.com/expo/eas-cli/pull/1517) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
- Remove friction from the initial EAS update setup. ([#1479](https://github.com/expo/eas-cli/pull/1479) by [@byCedric](https://github.com/byCedric))
- Improve choice formatting when selecting a build to submit in the `eas submit` command. ([#1502](https://github.com/expo/eas-cli/pull/1502) by [@szdziedzic](https://github.com/szdziedzic))
## [2.6.0](https://github.com/expo/eas-cli/releases/tag/v2.6.0) - 2022-10-27
### 🎉 New features
- Add custom metadata validation command with more complex rules. ([#1416](https://github.com/expo/eas-cli/pull/1416) by [@byCedric](https://github.com/byCedric))
- Add `eas build:run` command which runs iOS simulator builds from the CLI. ([#1447](https://github.com/expo/eas-cli/pull/1447) by [@szdziedzic](https://github.com/szdziedzic))
- Add feature gate support. ([#1475](https://github.com/expo/eas-cli/pull/1475) by [@wschurman](https://github.com/wschurman))
- Warn about outdated build deployment when configuring EAS Update. ([#1467](https://github.com/expo/eas-cli/pull/1467) by [@fiberjw](https://github.com/fiberjw))
### 🧹 Chores
- Replace `secret:list` table with formatted fields. ([#1464](https://github.com/expo/eas-cli/pull/1464) by [@byCedric](https://github.com/byCedric))
- Replace `device:create` input table with formatted fields. ([#1465](https://github.com/expo/eas-cli/pull/1465) by [@byCedric](https://github.com/byCedric))
- Replace `channel:view` table with formatted fields. ([#1466](https://github.com/expo/eas-cli/pull/1466) by [@byCedric](https://github.com/byCedric))
- Upgrade dependencies. ([#1480](https://github.com/expo/eas-cli/pull/1480) by [@dsokal](https://github.com/dsokal))
- Replace all remaining tables with formatted fields. ([#1481](https://github.com/expo/eas-cli/pull/1481) by [@byCedric](https://github.com/byCedric))
## [2.5.1](https://github.com/expo/eas-cli/releases/tag/v2.5.1) - 2022-10-24
### 🐛 Bug fixes
- Revert using local CLI for `expo export` ([#1460](https://github.com/expo/eas-cli/pull/1460)). ([#1472](https://github.com/expo/eas-cli/pull/1472) by [@dsokal](https://github.com/dsokal))
## [2.5.0](https://github.com/expo/eas-cli/releases/tag/v2.5.0) - 2022-10-24
### 🎉 New features
- Update init command to handle slug and owner. ([#1452](https://github.com/expo/eas-cli/pull/1452) by [@wschurman](https://github.com/wschurman))
- Add `eas secret:push`. ([#1457](https://github.com/expo/eas-cli/pull/1457) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Initialize analytics consistently and move analytics into context. ([#1444](https://github.com/expo/eas-cli/pull/1444) by [@wschurman](https://github.com/wschurman))
- - Skip using `--non-interactive` and `--experimental-bundle` flags when local Expo CLI is installed. ([#1460](https://github.com/expo/eas-cli/pull/1460) by [@EvanBacon](https://github.com/EvanBacon))
### 🧹 Chores
- Handle case where git is installed but not working properly. ([#1454](https://github.com/expo/eas-cli/pull/1454) by [@brentvatne](https://github.com/brentvatne))
- Remove table logging in `update:view`. ([#1463](https://github.com/expo/eas-cli/pull/1463) by [@EvanBacon](https://github.com/EvanBacon))
- Fix install prompt language. ([#1462](https://github.com/expo/eas-cli/pull/1462) by [@EvanBacon](https://github.com/EvanBacon))
## [2.4.1](https://github.com/expo/eas-cli/releases/tag/v2.4.1) - 2022-10-17
### 🐛 Bug fixes
- Bump `eas-cli-local-build-plugin` to 0.0.144 to not require `yarn` for local builds. ([c05ef](https://github.com/expo/eas-cli/commit/c05ef9cccaa819d8e5fb84858cb09cadaae90134) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Remove EAS Submit errors from codebase. The errors have been moved to the server side. ([#1443](https://github.com/expo/eas-cli/pull/1443) by [@dsokal](https://github.com/dsokal))
## [2.4.0](https://github.com/expo/eas-cli/releases/tag/v2.4.0) - 2022-10-14
### 🎉 New features
- Handle more common errors for EAS Submit. ([#1440](https://github.com/expo/eas-cli/pull/1440) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Move graphql and session into command context. ([#1435](https://github.com/expo/eas-cli/pull/1435), [#1436](https://github.com/expo/eas-cli/pull/1436) by [@wschurman](https://github.com/wschurman))
## [2.3.0](https://github.com/expo/eas-cli/releases/tag/v2.3.0) - 2022-10-10
### 🎉 New features
- Add new experimental resource class for M1 Macs. ([#1425](https://github.com/expo/eas-cli/pull/1425) by [@szdziedzic](https://github.com/szdziedzic))
## [2.2.1](https://github.com/expo/eas-cli/releases/tag/v2.2.1) - 2022-10-05
### 🐛 Bug fixes
- Make Apple Platform detection more robust. ([#1417](https://github.com/expo/eas-cli/pull/1417) by [@quinlanj](https://github.com/quinlanj))
- Fix creating EAS secret from file. ([#1423](https://github.com/expo/eas-cli/pull/1423) by [@dsokal](https://github.com/dsokal))
## [2.2.0](https://github.com/expo/eas-cli/releases/tag/v2.2.0) - 2022-10-04
### 🛠 Breaking changes
- Prompt before configuring EAS project. ([#1356](https://github.com/expo/eas-cli/pull/1356) by [@wschurman](https://github.com/wschurman))
- Adds paginated support to device commands. Removes the ability to delete multiple devices at once. ([#1381](https://github.com/expo/eas-cli/pull/1381) by [@kgc00](https://github.com/kgc00))
### 🎉 New features
- Add `-e` as a shortcut for `--profile` flag. ([#1342](https://github.com/expo/eas-cli/pull/1342) by [@szdziedzic](https://github.com/szdziedzic))
- Support JSON5 in **eas.json**. ([#1350](https://github.com/expo/eas-cli/pull/1350) by [@dsokal](https://github.com/dsokal))
- Add pagination and interactivity to update commands. ([#1323](https://github.com/expo/eas-cli/pull/1323) by [@kgc00](https://github.com/kgc00))
- Add support for uploading files to EAS Secret. ([#1354](https://github.com/expo/eas-cli/pull/1354) by [@dsokal](https://github.com/dsokal))
- Add pagination to channel commands. ([#1352](https://github.com/expo/eas-cli/pull/1352) by [@kgc00](https://github.com/kgc00))
- Add pagination to build commands. ([#1353](https://github.com/expo/eas-cli/pull/1353) by [@kgc00](https://github.com/kgc00))
- Provide suggestions for developers when archive size is large. ([#1363](https://github.com/expo/eas-cli/pull/1363) by [@szdziedzic](https://github.com/szdziedzic))
- Improve `eas init` command. ([#1376](https://github.com/expo/eas-cli/pull/1376) by [@wschurman](https://github.com/wschurman))
- Warn about outdated channel configuration before build when using eas update. ([#1397](https://github.com/expo/eas-cli/pull/1397) by [@kbrandwijk](https://github.com/kbrandwijk))
- Validate that owner and projectId and slug all are in alignment. ([#1405](https://github.com/expo/eas-cli/pull/1405) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
- Fix automatic eas.json generation when running `eas build`. ([#1415](https://github.com/expo/eas-cli/pull/1415) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Standardize the GQL 'Update' schema using UpdateFragmentNode. ([#1348](https://github.com/expo/eas-cli/pull/1348) by [@kgc00](https://github.com/kgc00))
- Remove 'owner' app config field dependency. ([#1368](https://github.com/expo/eas-cli/pull/1368), [#1369](https://github.com/expo/eas-cli/pull/1369), [#1371](https://github.com/expo/eas-cli/pull/1371), [#1372](https://github.com/expo/eas-cli/pull/1372), [#1373](https://github.com/expo/eas-cli/pull/1373) by [@wschurman](https://github.com/wschurman))
- Add Xcode 14 image. ([#1365](https://github.com/expo/eas-cli/pull/1365) by [@szdziedzic](https://github.com/szdziedzic))
- Update max `versionCode` for Android. ([#1400](https://github.com/expo/eas-cli/pull/1400) by [@wkozyra95](https://github.com/wkozyra95))
- Add and use command context to declare command dependencies. ([#1383](https://github.com/expo/eas-cli/pull/1383), [#1384](https://github.com/expo/eas-cli/pull/1384), [#1387](https://github.com/expo/eas-cli/pull/1387), [#1388](https://github.com/expo/eas-cli/pull/1388), [#1390](https://github.com/expo/eas-cli/pull/1390), [#1391](https://github.com/expo/eas-cli/pull/1391), [#1394](https://github.com/expo/eas-cli/pull/1394), [#1402](https://github.com/expo/eas-cli/pull/1402), [#1403](https://github.com/expo/eas-cli/pull/1403) by [@wschurman](https://github.com/wschurman))
- Fix typo in the ad hoc build message. ([#1407](https://github.com/expo/eas-cli/pull/1407) by [@Simek](https://github.com/Simek))
- Improve errors and error messages formatting related to **eas.json**. ([#1414](https://github.com/expo/eas-cli/pull/1414) by [@Simek](https://github.com/Simek))
- Handle errors from platform-specific kill switches to disable free tier builds. ([#1401](https://github.com/expo/eas-cli/pull/1401) by [@szdziedzic](https://github.com/szdziedzic))
- Surface invalid eas.json errors in an optional project context. ([#1418](https://github.com/expo/eas-cli/pull/1418) by [@quinlanj](https://github.com/quinlanj))
- Improve message when project ID doesn't match either slug or owner. ([#1420](https://github.com/expo/eas-cli/pull/1420) by [@brentvatne](https://github.com/brentvatne))
## [2.1.0](https://github.com/expo/eas-cli/releases/tag/v2.1.0) - 2022-09-05
### 🎉 New features
- Add tvOS credentials compatibility for Adhoc and App store builds. ([#1325](https://github.com/expo/eas-cli/pull/1325) by [@quinlanj](https://github.com/quinlanj))
- Add `eas open` command for opening project page in web browser. ([#1337](https://github.com/expo/eas-cli/pull/1337) by [@dsokal](https://github.com/dsokal))
- Add support for buildArtifactPaths. Rename artifactPath to applicationArchivePath. ([#1321](https://github.com/expo/eas-cli/pull/1321) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix viewing branch list when a branch has no associated platforms. ([#1326](https://github.com/expo/eas-cli/pull/1326) by [@hbiede](https://github.com/hbiede))
- Fix description of `help` command in help prompt. ([#1341](https://github.com/expo/eas-cli/pull/1341) by [@Simek](https://github.com/Simek))
- Resolve paths from the root when checking for gitignored values. ([#1336](https://github.com/expo/eas-cli/pull/1336) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Add noImplicitOverride to tsconfigs. ([#1327](https://github.com/expo/eas-cli/pull/1327) by [@wschurman](https://github.com/wschurman))
- Fix typos. ([#1340](https://github.com/expo/eas-cli/pull/1340) by [@hbiede](https://github.com/hbiede))
## [2.0.0](https://github.com/expo/eas-cli/releases/tag/v2.0.0) - 2022-09-01
### 🛠 Breaking changes
- Fix typo in `cli.promptToConfigurePushNotifications` field in **eas.json**. EAS CLI will throw an error if the old field name is in **eas.json**. ([#1332](https://github.com/expo/eas-cli/pull/1332) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix dynamic config update warning. ([#1322](https://github.com/expo/eas-cli/pull/1322) by [@wschurman](https://github.com/wschurman))
## [1.2.0](https://github.com/expo/eas-cli/releases/tag/v1.2.0) - 2022-08-29
### 🎉 New features
- Add 2022 Apple capabilities. ([#1307](https://github.com/expo/eas-cli/pull/1307) by [@EvanBacon](https://github.com/EvanBacon))
- Print warning when eas-cli is installed as project dependency. ([#1310](https://github.com/expo/eas-cli/pull/1310) by [@dsokal](https://github.com/dsokal))
- Add eas.json to skip push notifications credentials setup. ([#1315](https://github.com/expo/eas-cli/pull/1315) by [@dsokal](https://github.com/dsokal))
- Warn about EAS outages when running `build`, `submit`, and `update` commands. ([#1312](https://github.com/expo/eas-cli/pull/1312) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Replace promptToCreateProjectIfNotExistsAsync with getProjectIdAsync. ([#1303](https://github.com/expo/eas-cli/pull/1303) by [@wschurman](https://github.com/wschurman))
- Use AppQuery instead of ProjectQuery. ([#1304](https://github.com/expo/eas-cli/pull/1304) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Improve instructions on setting `extra.eas.projectId` in app configuration. ([#1316](https://github.com/expo/eas-cli/pull/1316) by [@dsokal](https://github.com/dsokal))
- Improve copy in EAS Submit - "e-mail" -> email, and make the link to app on App Store go directly to the TestFlight tab. ([#1318](https://github.com/expo/eas-cli/pull/1318) by [@brentvatne](https://github.com/brentvatne))
## [1.1.1](https://github.com/expo/eas-cli/releases/tag/v1.1.1) - 2022-08-23
### 🎉 New features
- Add support for `autoIncrement` option at the root of the build profile. ([#1298](https://github.com/expo/eas-cli/pull/1298) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Handle trailing backslash in `.gitignore`. ([#1306](https://github.com/expo/eas-cli/pull/1306) by [@wkozyra95](https://github.com/wkozyra95))
- Retry ASC Api Key downloads if it has not fully propagated on Apple's infrastructure. ([#1302](https://github.com/expo/eas-cli/pull/1302) by [@quinlanj](https://github.com/quinlanj))
## [1.1.0](https://github.com/expo/eas-cli/releases/tag/v1.1.0) - 2022-08-23
### 🎉 New features
- Add support for passing platform flag to `credentials` command. ([#1277](https://github.com/expo/eas-cli/pull/1277) by [@Simek](https://github.com/Simek))
### 🐛 Bug fixes
- Prevent throwing dynamic app config write error when configuring project ID. ([#1301](https://github.com/expo/eas-cli/pull/1301) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Remove unused dependency and devDependency from the CLI. ([#1297](https://github.com/expo/eas-cli/pull/1297) by [@Simek](https://github.com/Simek))
## [1.0.0](https://github.com/expo/eas-cli/releases/tag/v1.0.0) - 2022-08-22
### 🛠 Breaking changes
- Remove timeout when waiting for build / submission. ([#1289](https://github.com/expo/eas-cli/pull/1289) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Add metadata support for dynamic store.config.js files. ([#1270](https://github.com/expo/eas-cli/pull/1270) by [@byCedric](https://github.com/byCedric))
- Improve reliability of update asset presigned upload requests. ([#1278](https://github.com/expo/eas-cli/pull/1278) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
- Rebind `console.info` correctly after `ora` instance stops. ([#1113](https://github.com/expo/eas-cli/pull/1113) by [@EvanBacon](https://github.com/EvanBacon))
- Fix initializing git repository in monorepo. ([#1279](https://github.com/expo/eas-cli/pull/1279) by [@dsokal](https://github.com/dsokal))
- Limit the number of SignedAssetUploadSpecifications fetched at a time. ([#1287](https://github.com/expo/eas-cli/pull/1287) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Remove unused install script. ([#1280](https://github.com/expo/eas-cli/pull/1280), [#1281](https://github.com/expo/eas-cli/pull/1281) by [@wkozyra95](https://github.com/wkozyra95))
- Remove "please" from output ([#1250](https://github.com/expo/eas-cli/pull/1250) by [@jonsamp](https://github.com/jonsamp))
## [0.60.0](https://github.com/expo/eas-cli/releases/tag/v0.60.0) - 2022-08-12
### 🎉 New features
- Add `appVersion` runtime policy. ([#1267](https://github.com/expo/eas-cli/pull/1267) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Add JSON Schema `metadataPath` to iOS submission profile. ([#1269](https://github.com/expo/eas-cli/pull/1269) by [@byCedric](https://github.com/byCedric))
- Add warning when `appVersion` runtime policy is not supported. ([#1271](https://github.com/expo/eas-cli/pull/1271) by [@wkozyra95](https://github.com/wkozyra95))
## [0.59.0](https://github.com/expo/eas-cli/releases/tag/v0.59.0) - 2022-08-10
### 🐛 Bug fixes
- Fix building Android projects locally that don't have execute permissions set for `gradlew`. ([82231](https://github.com/expo/eas-cli/commit/822313e90e94bd6ddd3872061a0c25a8aa4db7cd) by [@dsokal](https://github.com/dsokal))
- Disable `nativeVersion` policy only for remote version source. ([#1261](https://github.com/expo/eas-cli/pull/1261) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Improve wording on version auto-increment. ([#1260](https://github.com/expo/eas-cli/pull/1260) by [@wkozyra95](https://github.com/wkozyra95))
- Upgrade dependencies. ([#1262](https://github.com/expo/eas-cli/pull/1262) by [@dsokal](https://github.com/dsokal))
## [0.58.0](https://github.com/expo/eas-cli/releases/tag/v0.58.0) - 2022-08-09
### 🎉 New features
- Managed app versions support. ([#1209](https://github.com/expo/eas-cli/pull/1209), [#1219](https://github.com/expo/eas-cli/pull/1219), [#1232](https://github.com/expo/eas-cli/pull/1232) by [@wkozyra95](https://github.com/wkozyra95))
- Add a warning when publishing an update with too many assets. ([#1243](https://github.com/expo/eas-cli/pull/1243) by [@kgc00](https://github.com/kgc00))
- Add submission info when building with `--json` option. ([#1246](https://github.com/expo/eas-cli/pull/1246) by [@wkozyra95](https://github.com/wkozyra95))
- Set all environment variables (documented in https://docs.expo.dev/build-reference/variables/) when running local build. ([#1256](https://github.com/expo/eas-cli/pull/1256) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Do not include sources in `eas-json` npm package. ([#1248](https://github.com/expo/eas-cli/pull/1248) by [@wkozyra95](https://github.com/wkozyra95))
- Swallow error when unable to track file in no commit workflow. ([#1109](https://github.com/expo/eas-cli/pull/1109) by [@brentvatne](https://github.com/brentvatne))
## [0.57.0](https://github.com/expo/eas-cli/releases/tag/v0.57.0) - 2022-08-03
### 🎉 New features
- Add new option to `eas device:create` - allow importing devices from Apple Developer Portal. ([#1236](https://github.com/expo/eas-cli/pull/1236) by [@dsokal](https://github.com/dsokal))
- Add support for attaching messages to builds. ([#1237](https://github.com/expo/eas-cli/pull/1237) by [@dsokal](https://github.com/dsokal))
- Introduce interactive pagination for `branch` commands. ([#1213](https://github.com/expo/eas-cli/pull/1213) by [@kgc00](https://github.com/kgc00))
## [0.56.0](https://github.com/expo/eas-cli/releases/tag/v0.56.0) - 2022-07-28
### 🎉 New features
- Display build queue type when waiting. ([#1217](https://github.com/expo/eas-cli/pull/1217) by [@dsokal](https://github.com/dsokal))
- Add better message on how to upgrade EAS CLI. ([#1222](https://github.com/expo/eas-cli/pull/1222) by [@jeremybarbet](https://github.com/jeremybarbet), [#1231](https://github.com/expo/eas-cli/pull/1231) by [@wkozyra95](https://github.com/wkozyra95))
- Invoke export with sourcemaps on update. ([#1228](https://github.com/expo/eas-cli/pull/1228) by [@kbrandwijk](https://github.com/kbrandwijk))
### 🐛 Bug fixes
- Show spinner instead of silently timing out during asset upload. ([#1206](https://github.com/expo/eas-cli/pull/1206) by [@wschurman](https://github.com/wschurman))
- Fix build archive S3 URLs. ([#1207](https://github.com/expo/eas-cli/pull/1207) by [@dsokal](https://github.com/dsokal))
- `autoIncrement` option on iOS will update versions of all targets that depend on an application target. ([#1219](https://github.com/expo/eas-cli/pull/1219) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Increase max number of chained "extends" in build profiles. ([#1208](https://github.com/expo/eas-cli/pull/1208) by [@wkozyra95](https://github.com/wkozyra95))
- Change wording on build warning for bundle id/package name. ([#1211](https://github.com/expo/eas-cli/pull/1211) by [@kbrandwijk](https://github.com/kbrandwijk))
## [0.55.1](https://github.com/expo/eas-cli/releases/tag/v0.55.1) - 2022-07-12
### 🐛 Bug fixes
- Fix asset upload crash. ([#1205](https://github.com/expo/eas-cli/pull/1205) by [@wschurman](https://github.com/wschurman))
## [0.55.0](https://github.com/expo/eas-cli/releases/tag/v0.55.0) - 2022-07-11
### 🎉 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))
- Override applicationId via env. ([#1203](https://github.com/expo/eas-cli/pull/1203) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 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))
- Improve store config categories configuration. ([#1187](https://github.com/expo/eas-cli/pull/1187) by [@byCedric](https://github.com/byCedric))
- Remove deprecated idfa and price tier from store config. ([#1193](https://github.com/expo/eas-cli/pull/1193) 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
@@ -159,7 +556,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores
- Upgrade `node-forge` to 1.0.0. ([#902](https://github.com/expo/eas-cli/pull/902) by [@dependabot](https://github.com/apps/dependabot) + [@dsokal](https://github.com/dsokal))
- Upgrade `node-forge` to 1.0.0. ([#902](https://github.com/expo/eas-cli/pull/902) by [@dependabot](https://github.com/apps/dependabot), [@dsokal](https://github.com/dsokal))
- Replace `@expo/plugin-autocomplete` with `@oclif/plugin-autocomplete`. Upgrade oclif deps. ([#903](https://github.com/expo/eas-cli/pull/903) by [@dsokal](https://github.com/dsokal))
## [0.44.0](https://github.com/expo/eas-cli/releases/tag/v0.44.0) - 2022-01-11
@@ -211,7 +608,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Adds commands for EAS Update, which is now in preview for subscribers. EAS Update makes fixing small bugs and pushing quick fixes a snap in between app store submissions. It accomplishes this by allowing an end-user's app to swap out the non-native parts of their app (for example, JS, styling, and image changes) with a new update that contains bug fixes and other updates.
- Adds commands for EAS Update, which is now in preview for subscribers. EAS Update makes fixing small bugs and pushing quick fixes a snap in between app store submissions. It accomplishes this by allowing an end-user's app to swap out the non-native parts of their app (for example, JS, styling, and image changes) with a new update that contains bug fixes and other updates. ([#854](https://github.com/expo/eas-cli/pull/854) by [@jonsamp](https://github.com/jonsamp))
- Adds `eas update`, which can bundle and publish updates on a branch.
- Adds `eas branch`, which manages branches. Branches contain a list of updates and are linked to channels.
- Adds `eas channel`, which manages channels. Channels are specified inside builds and are linked to branches, allowing us to link specific updates with specific builds.
@@ -257,7 +654,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- Fix submit for multi-target projects where ascAppId is not specified. ([#809](https://github.com/expo/eas-cli/pull/809) by [@wkozyra95](https://github.com/wkozyra95))
- Fix building iOS projects with Apple Watch companion app. ([#812](https://github.com/expo/eas-cli/pull/812), [#817](https://github.com/expo/eas-cli/pull/817), [#821](https://github.com/expo/eas-cli/pull/821) by [@jkhales](https://github.com/jkhales) + [@dsokal](https://github.com/dsokal))
- Fix building iOS projects with Apple Watch companion app. ([#812](https://github.com/expo/eas-cli/pull/812), [#817](https://github.com/expo/eas-cli/pull/817), [#821](https://github.com/expo/eas-cli/pull/821) by [@jkhales](https://github.com/jkhales), [@dsokal](https://github.com/dsokal))
### 🧹 Chores
@@ -293,7 +690,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Improve unknown capability syncing error message. ([#775](https://github.com/expo/eas-cli/pulls/775) by [@EvanBacon](https://github.com/EvanBacon))
- Improve unknown capability syncing error message. ([#775](https://github.com/expo/eas-cli/pull/775) by [@EvanBacon](https://github.com/EvanBacon))
- Add submit webhooks. ([#777](https://github.com/expo/eas-cli/pull/777) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
@@ -391,7 +788,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
- Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701) and [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal))
- Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701), [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal))
- Don't print logs to `stderr`. ([#708](https://github.com/expo/eas-cli/pull/708) by [@dsokal](https://github.com/dsokal))
- Remove automatic migration for legacy `eas.json` format introduced in [v0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2). ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
- Implement no-commit build workflow. Add required `cli` field in root of `eas.json`. ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
@@ -423,7 +820,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Make "production" the default profile for building and submitting. ([#677](https://github.com/expo/eas-cli/pull/677)) by [@jonsamp](https://github.com/jonsamp)
- Make "production" the default profile for building and submitting. ([#677](https://github.com/expo/eas-cli/pull/677) by [@jonsamp](https://github.com/jonsamp))
- Previously, the default profile when running `eas build` or `eas submit` was "release". We're changing it to a more recognizable name that is consistent with our docs, which is now "production". You can always specify a profile manually with the `--profile` flag. For this major version, if you do not have a profile named "production", and still have a profile named "release", we will fall back to the "release" profile as the default, however you'll see a warning as we're going to remove that behavior in the next major release of EAS CLI.
- To upgrade, update **eas.json** to have a "production" profile in both the `build` and `submit` objects. If you already have a project set up, this will replace the existing "release" profile. After the change, eas.json should have the following profiles:
```json
@@ -469,7 +866,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
- Don't resolve the iOS builder image on the client side. EAS Build will use the appropriate iOS image for a given Expo SDK version unless the `image` is defined. This only applies to managed projects. ([#675](https://github.com/expo/eas-cli/pull/675) by [@wkozyra95](https://github.com/wkozyra95) + [@dsokal](https://github.com/dsokal))
- Don't resolve the iOS builder image on the client side. EAS Build will use the appropriate iOS image for a given Expo SDK version unless the `image` is defined. This only applies to managed projects. ([#675](https://github.com/expo/eas-cli/pull/675) by [@wkozyra95](https://github.com/wkozyra95), [@dsokal](https://github.com/dsokal))
### 🎉 New features
@@ -481,7 +878,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- Fix `--json` flag when running EAS CLI on GitHub actions. ([#669](https://github.com/expo/eas-cli/pull/669) by [@dsokal](https://github.com/dsokal))
- Fix `"ios: mods.ios.infoPlist: Failed to find Info.plist linked to Xcode project."` warning when running `eas build` in a managed project.([#670](https://github.com/expo/eas-cli/pull/670) by [@brentvatne](https://github.com/brentvatne))
- Fix `"ios: mods.ios.infoPlist: Failed to find Info.plist linked to Xcode project."` warning when running `eas build` in a managed project. ([#670](https://github.com/expo/eas-cli/pull/670) by [@brentvatne](https://github.com/brentvatne))
- Fix building monorepo projects on Windows. ([#671](https://github.com/expo/eas-cli/pull/671) by [@dsokal](https://github.com/dsokal))
## [0.30.0](https://github.com/expo/eas-cli/releases/tag/v0.30.0) - 2021-10-05
@@ -615,7 +1012,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- Fix wrong warning for `metro.config.js` check on Windows. ([#587](https://github.com/expo/eas-cli/pull/588) by [@louisgv](https://github.com/louisgv))
- Fix wrong warning for `metro.config.js` check on Windows. ([#588](https://github.com/expo/eas-cli/pull/588) by [@louisgv](https://github.com/louisgv))
- Fix detecting `googleServicesFile` with `EAS_NO_VCS=1`. ([#583](https://github.com/expo/eas-cli/pull/583) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
@@ -695,7 +1092,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Include the file extension for update's assets in the manifest fragment. ([#532](https://github.com/expo/eas-cli/pull/535) by [@jkhales](https://github.com/jkhales))
- Include the file extension for update's assets in the manifest fragment. ([#535](https://github.com/expo/eas-cli/pull/535) by [@jkhales](https://github.com/jkhales))
- Use env variables from `eas.json` when evaluating `app.config.js`. ([#532](https://github.com/expo/eas-cli/pull/532) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
@@ -715,7 +1112,7 @@ This is the log of notable changes to EAS CLI and related packages.
- Add `secret:create --force` command to overwrite existing secrets. ([#513](https://github.com/expo/eas-cli/pull/513) by [@bycedric](https://github.com/bycedric))
- Fall back to APK when building for internal distribution. ([#527](https://github.com/expo/eas-cli/pull/527) by [@dsokal](https://github.com/dsokal))
- Add `iosEnterpriseProvisioning` to build metadata. ([#522](https://github.com/expo/eas-cli/pull/522) by [@dsokal](https://github.com/dsokal))
- Autodetect Google Services JSON key path in `eas submit -p android`. ([#520](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
- Autodetect Google Services JSON key path in `eas submit -p android`. ([#520](https://github.com/expo/eas-cli/pull/520) by [@barthap](https://github.com/barthap))
### 🐛 Bug fixes
@@ -729,7 +1126,7 @@ This is the log of notable changes to EAS CLI and related packages.
- Add `project:*` commands. ([#500](https://github.com/expo/eas-cli/pull/500) by [@jkhales](https://github.com/jkhales))
- Added support for iOS 15 capabilities: Communication Notifications, Time Sensitive Notifications, Group Activities, and Family Controls. ([#499](https://github.com/expo/eas-cli/pull/499) by [@EvanBacon](https://github.com/EvanBacon))
- Show more build metadata in `build:view` and `build:list`. ([#504](https://github.com/expo/eas-cli/pull/504) and [#508](https://github.com/expo/eas-cli/pull/508) by [@dsokal](https://github.com/dsokal))
- Show more build metadata in `build:view` and `build:list`. ([#504](https://github.com/expo/eas-cli/pull/504), [#508](https://github.com/expo/eas-cli/pull/508) by [@dsokal](https://github.com/dsokal))
- Add runtime version to build metadata. ([#493](https://github.com/expo/eas-cli/pull/493) by [@dsokal](https://github.com/dsokal))
## [0.20.0](https://github.com/expo/eas-cli/releases/tag/v0.20.0) - 2021-07-09
@@ -809,15 +1206,15 @@ This is the log of notable changes to EAS CLI and related packages.
- New version of Android credentials manager. ([#460](https://github.com/expo/eas-cli/pull/460) by [@quinlanj](https://github.com/quinlanj))
- Add an internal distribution with dev client to `build:configure` defaults. ([#465](https://github.com/expo/eas-cli/pull/465) by [@fson](https://github.com/fson))
- Add `updates.channel` to build metadata. ([#461](https://github.com/expo/eas-cli/pull/461) by [@jkhales](https://github.com/jkhales))
- iOS push key setup and management now available in `eas-cli credentials`. ([#469](https://github.com/expo/eas-cli/pull/469) [#470](https://github.com/expo/eas-cli/pull/470) by [@quinlanj](https://github.com/quinlanj))
- iOS push key setup and management now available in `eas-cli credentials`. ([#469](https://github.com/expo/eas-cli/pull/469), [#470](https://github.com/expo/eas-cli/pull/470) by [@quinlanj](https://github.com/quinlanj))
- Support new build status: `new`. ([#475](https://github.com/expo/eas-cli/pull/475) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Deprecate `--skip-credentials-check` flag because it doesn't do anything and is no longer needed. ([#442](https://github.com/expo/eas-cli/pull/442) by [@brentvatne](https://github.com/brentvatne))
- Move android credentials code to new Graphql API. ([#439](https://github.com/expo/eas-cli/pull/439) [#440](https://github.com/expo/eas-cli/pull/440) [#438](https://github.com/expo/eas-cli/pull/438) [#443](https://github.com/expo/eas-cli/pull/443) [#447](https://github.com/expo/eas-cli/pull/447) [#451](https://github.com/expo/eas-cli/pull/451) [#455](https://github.com/expo/eas-cli/pull/455) by [@quinlanj](https://github.com/quinlanj))
- Move android credentials code to new Graphql API. ([#439](https://github.com/expo/eas-cli/pull/439), [#440](https://github.com/expo/eas-cli/pull/440), [#438](https://github.com/expo/eas-cli/pull/438), [#443](https://github.com/expo/eas-cli/pull/443), [#447](https://github.com/expo/eas-cli/pull/447), [#451](https://github.com/expo/eas-cli/pull/451), [#455](https://github.com/expo/eas-cli/pull/455) by [@quinlanj](https://github.com/quinlanj))
- Prepare Graphql infra to support iOS push keys. ([#456](https://github.com/expo/eas-cli/pull/456) by [@quinlanj](https://github.com/quinlanj))
- Improve credentials DX ([#448](https://github.com/expo/eas-cli/pull/448) [#449](https://github.com/expo/eas-cli/pull/449) by [@quinlanj](https://github.com/quinlanj))
- Improve credentials DX ([#448](https://github.com/expo/eas-cli/pull/448), [#449](https://github.com/expo/eas-cli/pull/449) by [@quinlanj](https://github.com/quinlanj))
- Add analytics on dev client builds. ([#454](https://github.com/expo/eas-cli/pull/454) by [@fson](https://github.com/fson))
- Support non-git projects. ([#462](https://github.com/expo/eas-cli/pull/462) by [@wkozyra95](https://github.com/wkozyra95))
@@ -831,7 +1228,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores
- Android credentials setup now on Graphql API. ([#434](https://github.com/expo/eas-cli/pull/427) by [@quinlanj](https://github.com/quinlanj))
- Android credentials setup now on Graphql API. ([#427](https://github.com/expo/eas-cli/pull/427) by [@quinlanj](https://github.com/quinlanj))
## [0.16.0](https://github.com/expo/eas-cli/releases/tag/v0.16.0) - 2021-05-26
@@ -863,7 +1260,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Auto sync associated domains capability before building. ([#384](https://github.com/expo/eas-cli/pull/384) by [@EvanBacon](https://github.com/EvanBacon))
- Add eas init command. [#402](https://github.com/expo/eas-cli/pull/402) by [@jkhales](https://github.com/jkhales))
- Add eas init command. ([#402](https://github.com/expo/eas-cli/pull/402) by [@jkhales](https://github.com/jkhales))
- Allow for arbitrary string values in `schemeBuildConfiguration`. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
- Allow for installing custom `expo-cli` version on EAS Build. ([#409](https://github.com/expo/eas-cli/pull/409) by [@randomhajile](https://github.com/randomhajile))
- Support PKCS kesytores for Android. ([#398](https://github.com/expo/eas-cli/pull/398) by [@wkozyra95](https://github.com/wkozyra95))
@@ -874,7 +1271,7 @@ This is the log of notable changes to EAS CLI and related packages.
- Fix failure when publishing without the platform flag. ([#415](https://github.com/expo/eas-cli/pull/415) by [@jkhales](https://github.com/jkhales))
- Pin versions in package.json. ([#399](https://github.com/expo/eas-cli/pull/399) by [@dsokal](https://github.com/dsokal))
- Revert [0ac2f](https://github.com/expo/eas-cli/commit/0ac2fb77a118df609381c4e350aa68609340c3cd) as the root cause of the issue has been fixed in [#399](https://github.com/expo/eas-cli/pull/399).
- Revert [0ac2f](https://github.com/expo/eas-cli/commit/0ac2fb77a118df609381c4e350aa68609340c3cd) as the root cause of the issue has been fixed in [#399](https://github.com/expo/eas-cli/pull/399).
- Include development-client in valid buildType for internal distribution. ([#410](https://github.com/expo/eas-cli/pull/410) by [@brentvatne](https://github.com/brentvatne))
## [0.14.1](https://github.com/expo/eas-cli/releases/tag/v0.14.1) - 2021-05-11
@@ -901,7 +1298,7 @@ This is the log of notable changes to EAS CLI and related packages.
- Cleanup unused code. ([#364](https://github.com/expo/eas-cli/pull/364), [#365](https://github.com/expo/eas-cli/pull/365) by [@quinlanj](https://github.com/quinlanj))
- Redesign UX in beta credentials manager. ([#360](https://github.com/expo/eas-cli/pull/360) by [@quinlanj](https://github.com/quinlanj))
- Port more options to the beta credentials manager. ([#352](https://github.com/expo/eas-cli/pull/352), [357](https://github.com/expo/eas-cli/pull/357), [361](https://github.com/expo/eas-cli/pull/361) by [@quinlanj](https://github.com/quinlanj))
- Port more options to the beta credentials manager. ([#352](https://github.com/expo/eas-cli/pull/352), [#357](https://github.com/expo/eas-cli/pull/357), [#361](https://github.com/expo/eas-cli/pull/361) by [@quinlanj](https://github.com/quinlanj))
- Add getBuildProfileNamesAsync helper to EasJsonReader. ([#351](https://github.com/expo/eas-cli/pull/351) by [@quinlanj](https://github.com/quinlanj))
- Increase build timeout to 1 hour. ([#370](https://github.com/expo/eas-cli/pull/370) by [@wkozyra95](https://github.com/wkozyra95))
- Remove check for pending builds. ([#373](https://github.com/expo/eas-cli/pull/373) by [@wkozyra95](https://github.com/wkozyra95))
@@ -962,7 +1359,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Use special Expo SDK runtime version for managed projects ([#336](https://github.com/expo/eas-cli/pull/336)) by [@wschurman](https://github.com/wschurman))
- Use special Expo SDK runtime version for managed projects ([#336](https://github.com/expo/eas-cli/pull/336) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
@@ -987,7 +1384,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Retry graphQL call on network or transient server errors. ([#320](https://github.com/expo/eas-cli/pull/320) by [@jkhales](https://github.com/jkhales))
- Display more friendly error messages when `eas submit` fails. ([#311](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
- Display more friendly error messages when `eas submit` fails. ([#311](https://github.com/expo/eas-cli/pull/311) by [@barthap](https://github.com/barthap))
- Add support for managing webhooks (new commands: `webhook:create`, `webhook:view`, `webhook:list`, `webhook:update`, and `webhook:delete`). ([#314](https://github.com/expo/eas-cli/pull/314) by [@dsokal](https://github.com/dsokal))
- Support for local builds [experimental]. ([#305](https://github.com/expo/eas-cli/pull/305) by [@wkozyra95](https://github.com/wkozyra95))
@@ -1071,7 +1468,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- Make sure all files are committed before build. ([#251](https://github.com/expo/eas-cli/pull/251) by [@wkozyra95](https://github.com/wkozyra95))
- Fix `eas submit` support for tar.gz files. ([#257](https://github.com/expo/eas-cli/pull/257)) by [@wkozyra95](https://github.com/wkozyra95))
- Fix `eas submit` support for tar.gz files. ([#257](https://github.com/expo/eas-cli/pull/257) by [@wkozyra95](https://github.com/wkozyra95))
- Show untracked files when checking `git status`. ([#259](https://github.com/expo/eas-cli/pull/259) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
@@ -1098,7 +1495,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- Fix `"buildType" is not allowed` error. ([#595bf](https://github.com/expo/eas-cli/commit/595bfecf1cbff0f76e7fd2049fe16f6f38bbe150) by [@wkozyra95](https://github.com/wkozyra95))
- Fix `"buildType" is not allowed` error. ([595bf](https://github.com/expo/eas-cli/commit/595bfecf1cbff0f76e7fd2049fe16f6f38bbe150) by [@wkozyra95](https://github.com/wkozyra95))
## [0.4.0](https://github.com/expo/eas-cli/releases/tag/v0.4.0) - 2021-02-16
+11 -2
View File
@@ -1,7 +1,16 @@
* @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/src/submit @dsokal
packages/eas-cli/src/commands/submit.ts @dsokal
packages/eas-cli/src/update @EvanBacon @byCedric @wschurman
packages/eas-cli/src/commands/update @EvanBacon @byCedric @wschurman
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,4 +1,4 @@
## Releasing a new version
1. Run `yarn release` in the repository root folder.
1. Run `yarn release` in the repository root folder. The next version is chosen automatically based on the changelog entries. If you want to use different version, pass the version string as a positional argument to the command, e.g. `yarn release 1.2.3`.
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
-130
View File
@@ -1,130 +0,0 @@
#!/bin/bash
# This script downloads the latest release version of EAS CLI from GitHub and
# installs it in /usr/local/bin
{
# All code is inside a block to ensure the script executes only when
# downloaded completely.
set -euo pipefail
prefix="${EAS_PREFIX:-/usr/local}"
temp_dir="$(mktemp -d)"
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]
then
# Use colors when stdout is a terminal and NO_COLOR isn't set.
text_bold=$'\033[1m'
text_normal=$'\033[22m'
text_red=$'\033[31m'
text_default_color=$'\033[39m'
else
# Disable colors
text_bold=""
text_normal=""
text_red=""
text_default_color=""
fi
abort() {
echo -e "$text_bold$text_red$1$text_normal$text_default_color"
exit 1
}
install() {
needs_sudo=""
# Make sure path exists and can be written to by the current user.
if ! mkdir -p "$prefix/lib" || [[ ! -w "$prefix/lib" ]]
then
needs_sudo=true
fi
if ! mkdir -p "$prefix/bin" || [[ ! -w "$prefix/bin" ]]
then
needs_sudo=true
fi
if ! rm -rf "$prefix/lib/eas"
then
needs_sudo=true
fi
# Run commands with sudo if necessary.
if [ -n "$needs_sudo" ]
then
if [ "$EUID" -ne 0 ]
then
echo "Installing requires superuser access."
echo "The sudo command will prompt for your password."
fi
sudo rm -rf "$prefix/lib/eas"
sudo mv "$temp_dir/eas" "$prefix/lib/eas"
sudo ln -fs "$prefix/lib/eas/bin/eas" "$prefix/bin/eas"
else
mv "$temp_dir/eas" "$prefix/lib/eas"
ln -fs "$prefix/lib/eas/bin/eas" "$prefix/bin/eas"
fi
}
if [[ ! ":$PATH:" == *":$prefix/bin:"* ]]; then
abort "Your path is missing $prefix/bin, you need to add this to \$PATH use this installer."
fi
hardware="$(uname -m)"
case "$hardware" in
x86_64)
arch="x64"
;;
arm*)
arch="arm"
;;
*)
abort "Unsupported CPU architecture $hardware."
;;
esac
case "$(uname -s)" in
Darwin*)
platform="darwin"
if [[ "$arch" != "x64" ]]; then
abort "Unsupported CPU architecture $hardware on macOS."
fi
;;
Linux*)
platform="linux"
;;
*)
abort "This installer is supported only on macOS, Linux, and Windows Subsystem for Linux."
;;
esac
url=$(hub release -L 1 | xargs hub release show --show-downloads | grep "${platform}-${arch}.tar.gz")
echo "Installing EAS CLI from $url"
mkdir -p "$temp_dir"
cd "$temp_dir"
echo
echo "Downloading..."
if [ "$(command -v curl)" ]
then
curl --location --progress-bar "$url" | tar xz
else
wget -O- "$url" | tar xz
fi
# Move the installation over and link the executable (may require sudo)
install
# Test the bundled node binary and if it doesn't work remove it (fall back to
# node installed in path).
"$prefix/lib/eas/bin/node" -v > /dev/null || {
rm "$prefix/lib/eas/bin/node"
}
# Test the CLI
executable=$(command -v eas) || abort "Could not run eas after installation."
echo
echo -e "EAS CLI is installed to ${executable}"
eas --version
echo
echo -e "Type ${text_bold}eas --help${text_normal} to get started."
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.52.0"
"version": "2.8.0"
}
+17 -16
View File
@@ -8,31 +8,32 @@
"build:tarballs:linux": "yarn build-for-size-check",
"build-for-size-check": "yarn build && yarn workspace eas-cli pretarball-ci && CLI_SIZE_CHECK=1 yarn workspace eas-cli oclif pack:tarballs --no-xz --targets linux-x64",
"typecheck": "lerna run typecheck",
"start": "lerna run watch --parallel",
"start": "lerna run watch --stream --parallel",
"watch": "yarn start",
"eas": "packages/eas-cli/bin/run",
"lint": "eslint .",
"release": "./scripts/bin/run update-local-plugin && lerna version --exact",
"lint-changelog": "./scripts/bin/run lint-changelog",
"release": "./scripts/bin/run release",
"test": "jest",
"clean": "lerna run clean && rm -rf node_modules coverage"
},
"devDependencies": {
"@types/jest": "27.4.1",
"@types/node": "16.7.8",
"eslint": "8.12.0",
"eslint-config-universe": "10.0.0",
"eslint-plugin-async-protect": "1.1.0",
"@types/jest": "29.2.0",
"@types/node": "18.11.5",
"eslint": "8.26.0",
"eslint-config-universe": "11.1.0",
"eslint-plugin-async-protect": "2.1.0",
"eslint-plugin-node": "11.1.0",
"jest": "27.5.1",
"jest-watch-typeahead": "1.0.0",
"lerna": "4.0.0",
"oclif": "2.6.3",
"prettier": "2.6.1",
"ts-jest": "27.1.4",
"typescript": "4.6.3"
"jest": "29.2.2",
"jest-watch-typeahead": "2.2.0",
"lerna": "6.0.1",
"oclif": "3.2.22",
"prettier": "2.7.1",
"ts-jest": "29.0.3",
"typescript": "4.8.4"
},
"volta": {
"node": "16.10.0",
"yarn": "1.22.15"
"node": "18.6.0",
"yarn": "1.22.19"
}
}
+18
View File
@@ -2,6 +2,24 @@ const rootEslintrc = require('../../.eslintrc.js');
module.exports = {
rules: {
'no-restricted-properties': [
'warn',
{
object: 'it',
property: 'only',
message: 'it.only should not be committed to main.',
},
{
object: 'test',
property: 'only',
message: 'test.only should not be committed to main.',
},
{
object: 'describe',
property: 'only',
message: 'describe.only should not be committed to main.',
},
],
'no-restricted-imports': [
'error',
{
+334 -122
View File
File diff suppressed because it is too large. Load diff
@@ -1,7 +0,0 @@
{
"extends": "@tsconfig/node16/tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"strict": true
}
}
+4 -2
View File
@@ -1,10 +1,12 @@
overwrite: true
schema: 'http://localhost:3000/graphql'
schema: 'https://staging-api.expo.dev/graphql'
documents:
- 'src/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/commands/**/*.ts'
- 'src/branch/**/*.ts'
- 'src/channel/**/*.ts'
generates:
src/graphql/generated.ts:
plugins:
@@ -14,7 +16,7 @@ generates:
dedupeOperationSuffix: true
hooks:
afterOneFileWrite:
- ./annotate-graphql-codegen.sh
- 'node ./scripts/annotate-graphql-codegen.js'
./graphql.schema.json:
plugins:
- 'introspection'
+7588 -2019
View File
File diff suppressed because it is too large. Load diff
+82 -72
View File
@@ -1,126 +1,129 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.52.0",
"version": "2.8.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.31",
"@expo/code-signing-certificates": "0.0.2",
"@expo/config": "6.0.21",
"@expo/config-plugins": "4.1.2",
"@expo/config-types": "44.0.0",
"@expo/eas-build-job": "0.2.71",
"@expo/eas-json": "0.52.0",
"@expo/json-file": "8.2.34",
"@expo/apple-utils": "0.0.0-alpha.34",
"@expo/code-signing-certificates": "0.0.3",
"@expo/config": "7.0.1",
"@expo/config-plugins": "5.0.1",
"@expo/config-types": "46.0.2",
"@expo/eas-build-job": "0.2.97",
"@expo/eas-json": "2.8.0",
"@expo/json-file": "8.2.36",
"@expo/multipart-body-parser": "1.1.0",
"@expo/package-manager": "0.0.50",
"@expo/pkcs12": "0.0.7",
"@expo/plist": "0.0.17",
"@expo/plugin-warn-if-update-available": "2.1.0",
"@expo/prebuild-config": "3.1.2",
"@expo/osascript": "2.0.33",
"@expo/package-manager": "0.0.56",
"@expo/pkcs12": "0.0.8",
"@expo/plist": "0.0.18",
"@expo/plugin-help": "5.1.16",
"@expo/plugin-warn-if-update-available": "2.4.0",
"@expo/prebuild-config": "5.0.3",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.6.0",
"@expo/spawn-async": "1.7.0",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "1.6.4",
"@oclif/plugin-autocomplete": "1.2.0",
"@oclif/plugin-help": "5.1.12",
"@urql/core": "2.4.3",
"@urql/exchange-retry": "0.3.2",
"adm-zip": "0.5.9",
"@oclif/core": "1.19.2",
"@oclif/plugin-autocomplete": "1.3.4",
"@segment/ajv-human-errors": "^2.1.2",
"@urql/core": "3.0.5",
"@urql/exchange-retry": "1.0.0",
"ajv": "^8.11.0",
"ajv-formats": "2.1.1",
"better-opn": "3.0.2",
"chalk": "4.1.2",
"cli-progress": "3.10.0",
"cli-table3": "0.6.1",
"cli-progress": "3.11.2",
"dateformat": "4.6.3",
"dotenv": "16.0.3",
"env-paths": "2.2.0",
"envinfo": "7.8.1",
"fast-deep-equal": "3.1.3",
"fast-glob": "3.2.11",
"fast-glob": "3.2.12",
"figures": "3.2.0",
"form-data": "4.0.0",
"fs-extra": "10.0.1",
"fs-extra": "10.1.0",
"getenv": "1.0.0",
"got": "^11.1.4",
"gradle-to-js": "2.0.0",
"graphql": "16.3.0",
"gradle-to-js": "2.0.1",
"graphql": "16.6.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",
"js-yaml": "^3.13.1",
"joi": "17.6.0",
"jks-js": "1.1.0",
"joi": "17.6.4",
"keychain": "1.3.0",
"log-symbols": "4.1.0",
"mime": "3.0.0",
"minimatch": "3.0.5",
"nanoid": "3.3.2",
"minimatch": "5.1.0",
"nanoid": "3.3.4",
"node-fetch": "2.6.7",
"node-forge": "1.3.1",
"nullthrows": "1.1.1",
"ora": "5.1.0",
"pkg-dir": "4.2.0",
"pngjs": "6.0.0",
"promise-limit": "2.7.0",
"promise-retry": "2.0.1",
"prompts": "2.4.2",
"qrcode-terminal": "0.12.0",
"pnp-webpack-plugin": "1.7.0",
"react-dev-utils": "12.0.0",
"resolve-from": "5.0.0",
"semver": "7.3.5",
"semver": "7.3.8",
"slash": "3.0.0",
"strip-ansi": "6.0.0",
"tar": "6.1.11",
"terminal-link": "2.1.1",
"ts-loader": "^9.2.8",
"tslib": "2.3.1",
"tslib": "2.4.0",
"turndown": "7.1.1",
"untildify": "4.0.0",
"uuid": "8.3.2",
"webpack": "5.72.0",
"uuid": "9.0.0",
"wrap-ansi": "7.0.0"
},
"devDependencies": {
"@graphql-codegen/cli": "2.6.2",
"@graphql-codegen/introspection": "2.1.1",
"@graphql-codegen/typescript": "2.4.8",
"@graphql-codegen/typescript-operations": "2.3.5",
"@types/cli-progress": "3.9.2",
"@types/adm-zip": "0.4.34",
"@graphql-codegen/cli": "2.13.7",
"@graphql-codegen/introspection": "2.2.1",
"@graphql-codegen/typescript": "2.8.0",
"@graphql-codegen/typescript-operations": "2.5.5",
"@types/cli-progress": "3.11.0",
"@types/dateformat": "3.0.1",
"@types/envinfo": "7.8.1",
"@types/express": "4.17.14",
"@types/fs-extra": "9.0.13",
"@types/getenv": "^1.0.0",
"@types/js-yaml": "^3.12.2",
"@types/mime": "2.0.3",
"@types/node-fetch": "^2.5.7",
"@types/node-forge": "1.0.1",
"@types/prompts": "2.0.14",
"@types/semver": "7.3.9",
"@types/tar": "6.1.1",
"@types/tough-cookie": "4.0.1",
"@types/mime": "3.0.1",
"@types/node-fetch": "2.6.2",
"@types/node-forge": "1.3.0",
"@types/pngjs": "6.0.1",
"@types/promise-retry": "1.1.3",
"@types/prompts": "2.4.1",
"@types/semver": "7.3.12",
"@types/tar": "6.1.3",
"@types/tough-cookie": "4.0.2",
"@types/uuid": "8.3.4",
"@types/webpack": "5.28.0",
"@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",
"express": "4.18.2",
"memfs": "3.4.7",
"mockdate": "3.0.5",
"nock": "13.2.4",
"tempy": "0.7.0",
"ts-deepmerge": "2.0.1",
"typescript": "4.6.3"
"nock": "13.2.9",
"rimraf": "3.0.2",
"ts-deepmerge": "4.0.0",
"ts-mockito": "2.6.1",
"ts-node": "10.9.1",
"typescript": "4.8.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": [
@@ -134,7 +137,7 @@
"commands": "./build/commands",
"plugins": [
"@oclif/plugin-autocomplete",
"@oclif/plugin-help",
"@expo/plugin-help",
"@expo/plugin-warn-if-update-available"
],
"topics": {
@@ -153,6 +156,9 @@
"device": {
"description": "manage Apple devices for Internal Distribution"
},
"metadata": {
"description": "manage store configuration"
},
"project": {
"description": "manage project"
},
@@ -171,11 +177,11 @@
},
"warn-if-update-available": {
"timeoutInDays": 0.5,
"message": "<%= chalk.bold('★') %> <%= chalk.bold(config.name + '@' + latest) %> <%= chalk.bold('is now available, please upgrade.') %>\n<%= chalk.dim('Proceeding with outdated version') %>\n"
"message": "<%= chalk('★') %> <%= chalk.bold(config.name + '@' + latest) %> <%= chalk.bold('is now available.') %>\n<%= chalk.dim('To upgrade, run ') %><%= chalk.dim(chalk.bold('npm install -g eas-cli')) %><%= chalk.dim('.') %>\n<%= chalk.dim('Proceeding with outdated version.') %>\n"
},
"update": {
"node": {
"version": "12.13.0"
"version": "18.6.0"
},
"s3": {
"templates": {
@@ -195,8 +201,8 @@
},
"repository": "expo/eas-cli",
"scripts": {
"postpack": "rm -f oclif.manifest.json",
"prepack": "./scripts/prepack.sh",
"postpack": "rimraf oclif.manifest.json",
"prepack": "rimraf build && yarn build && node ./scripts/prepack.js",
"pretarball-ci": "./scripts/pretarball-ci.sh",
"build": "tsc --project tsconfig.build.json",
"watch": "yarn build --watch --preserveWatchOutput",
@@ -204,6 +210,10 @@
"test": "jest",
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
"clean": "rm -rf dist build tmp node_modules yarn-error.log"
"clean": "rimraf dist build tmp node_modules yarn-error.log"
},
"volta": {
"node": "18.6.0",
"yarn": "1.22.19"
}
}
+793
View File
@@ -0,0 +1,793 @@
{
"$schema": "http://json-schema.org/draft-06/schema",
"type": "object",
"definitions": {
"apple": {
"AppleKidsAge": {
"enum": [
"FIVE_AND_UNDER",
"SIX_TO_EIGHT",
"NINE_TO_ELEVEN"
]
},
"AppleRating": {
"enum": [
"NONE",
"INFREQUENT_OR_MILD",
"FREQUENT_OR_INTENSE"
]
},
"AppleCategory": {
"type": "array",
"description": "App Store categories for the app, can add up to two categories. The first item in this list is the primary category, the second is the secondary and optional category. `STICKERS` and `GAMES` categories can add up to two additional subcategories, written as nested array.",
"markdownDescription": "App Store categories for the app, can add up to two categories. The first item in this list is the primary category, the second is the secondary and optional category. `STICKERS` and `GAMES` categories can add up to two additional subcategories, written as nested array. E.g.\n\n- `[\"SPORTS\"]`\n- `[\"SPORTS\", \"LIFESTYLE\"]`\n- `[[\"GAMES\", \"GAMES_CARD\"], \"ENTERTAINMENT\"]`\n- `[\"GAMES\", \"WEATHER\"]`",
"minItems": 1,
"maxItems": 2,
"uniqueItems": true,
"defaultSnippets": [
{
"label": "Basic categories",
"description": "Add two simple categories",
"body": [
"$1",
"$2"
]
},
{
"label": "Category with subcategories",
"description": "Add a category containing subcategories",
"body": [
[
"${1|GAMES,STICKERS|}",
"$3",
"$4"
]
]
}
],
"items": {
"oneOf": [
{
"$ref": "#/definitions/apple/AppleCategory/categories/root"
},
{
"$ref": "#/definitions/apple/AppleCategory/categories/parent"
},
{
"type": "array",
"description": "The GAMES category can have additional subcategories",
"minItems": 1,
"maxItems": 3,
"uniqueItems": true,
"items": [
{
"const": "GAMES"
},
{
"$ref": "#/definitions/apple/AppleCategory/categories/games"
},
{
"$ref": "#/definitions/apple/AppleCategory/categories/games"
}
]
},
{
"type": "array",
"description": "The STICKERS category can have additional subcategories",
"minItems": 1,
"maxItems": 3,
"uniqueItems": true,
"items": [
{
"const": "STICKERS"
},
{
"$ref": "#/definitions/apple/AppleCategory/categories/stickers"
},
{
"$ref": "#/definitions/apple/AppleCategory/categories/stickers"
}
]
}
]
},
"categories": {
"root": {
"enum": [
"BUSINESS",
"SOCIAL_NETWORKING",
"MEDICAL",
"FOOD_AND_DRINK",
"EDUCATION",
"BOOKS",
"SPORTS",
"FINANCE",
"REFERENCE",
"GRAPHICS_AND_DESIGN",
"DEVELOPER_TOOLS",
"HEALTH_AND_FITNESS",
"MUSIC",
"WEATHER",
"TRAVEL",
"ENTERTAINMENT",
"LIFESTYLE",
"MAGAZINES_AND_NEWSPAPERS",
"UTILITIES",
"SHOPPING",
"PRODUCTIVITY",
"NEWS",
"PHOTO_AND_VIDEO",
"NAVIGATION"
]
},
"parent": {
"enum": [
"GAMES",
"STICKERS"
]
},
"games": {
"enum": [
"GAMES_SPORTS",
"GAMES_WORD",
"GAMES_MUSIC",
"GAMES_ADVENTURE",
"GAMES_ACTION",
"GAMES_ROLE_PLAYING",
"GAMES_CASUAL",
"GAMES_BOARD",
"GAMES_TRIVIA",
"GAMES_CARD",
"GAMES_PUZZLE",
"GAMES_CASINO",
"GAMES_STRATEGY",
"GAMES_SIMULATION",
"GAMES_RACING",
"GAMES_FAMILY"
]
},
"stickers": {
"enum": [
"STICKERS_PLACES_AND_OBJECTS",
"STICKERS_EMOJI_AND_EXPRESSIONS",
"STICKERS_CELEBRATIONS",
"STICKERS_CELEBRITIES",
"STICKERS_MOVIES_AND_TV",
"STICKERS_SPORTS_AND_ACTIVITIES",
"STICKERS_EATING_AND_DRINKING",
"STICKERS_CHARACTERS",
"STICKERS_ANIMALS",
"STICKERS_FASHION",
"STICKERS_ART",
"STICKERS_GAMING",
"STICKERS_KIDS_AND_FAMILY",
"STICKERS_PEOPLE",
"STICKERS_MUSIC"
]
}
}
},
"AppleInfo": {
"type": "object",
"additionalProperties": false,
"required": [
"title",
"privacyPolicyUrl"
],
"defaultSnippets": [
{
"label": "Basic",
"description": "Create the minimum required locale properties",
"body": {
"title": "${1:App title}",
"privacyPolicyUrl": "https://${3:www.}"
}
},
{
"label": "Full",
"description": "Create all available locale properties",
"body": {
"title": "${1:App title}",
"subtitle": "${2:Subtitle for your app}",
"description": "${3:A longer description of what your app does}",
"keywords": [
"${4:keyword}"
],
"releaseNotes": "${5:Bug fixes and improved stability}",
"promoText": "${6:Short tagline for your app}",
"marketingUrl": "https://${7:www.}",
"supportUrl": "https://${8:www.}",
"privacyPolicyUrl": "https://${9:www.}",
"privacyChoicesUrl": "https://${10:www.}"
}
}
],
"properties": {
"title": {
"type": "string",
"description": "Name of the app in the store. This should be similar to the installed app name.",
"minLength": 2,
"maxLength": 30,
"meta": {
"storeInfo": "The name will be reviewed before it is made available on the App Store."
}
},
"subtitle": {
"type": "string",
"description": "Subtext for the app. Example: 'A Fun Game For Friends'",
"maxLength": 30,
"meta": {
"storeInfo": "The subtitle will be reviewed before it is made available on the App Store."
}
},
"description": {
"type": "string",
"description": "Main description of what the app does",
"minLength": 10,
"maxLength": 4000,
"meta": {
"versioned": true,
"liveEdits": true
}
},
"keywords": {
"type": "array",
"description": "List of keywords to help users find the app in the App Store",
"uniqueItems": true,
"items": {
"maxLength": 100,
"type": "string"
},
"meta": {
"versioned": true
}
},
"releaseNotes": {
"type": "string",
"description": "Changes since the last public version",
"maxLength": 4000,
"meta": {
"versioned": true,
"liveEdits": true
}
},
"promoText": {
"type": "string",
"description": "Short tagline for the app",
"maxLength": 170,
"meta": {
"versioned": true,
"liveEdits": true,
"aso": false
}
},
"marketingUrl": {
"type": "string",
"description": "URL to the app marketing page",
"maxLength": 255,
"meta": {
"versioned": true,
"liveEdits": true
}
},
"supportUrl": {
"type": "string",
"description": "URL to the app support page",
"maxLength": 255,
"meta": {
"versioned": true,
"liveEdits": true
}
},
"privacyPolicyText": {
"type": "string",
"description": "Privacy policy for Apple TV"
},
"privacyPolicyUrl": {
"type": "string",
"description": "A URL that links to your privacy policy. A privacy policy is required for all apps.",
"maxLength": 255,
"meta": {
"versioned": true,
"liveEdits": true
}
},
"privacyChoicesUrl": {
"type": "string",
"description": "A URL where users can modify and delete the data collected from the app, or decide how their data is used and shared.",
"maxLength": 255,
"meta": {
"versioned": true,
"liveEdits": true,
"optional": true
}
}
}
},
"AppleAdvisory": {
"type": "object",
"additionalProperties": false,
"required": [
"alcoholTobaccoOrDrugUseOrReferences",
"contests",
"gamblingSimulated",
"horrorOrFearThemes",
"matureOrSuggestiveThemes",
"medicalOrTreatmentInformation",
"profanityOrCrudeHumor",
"sexualContentGraphicAndNudity",
"sexualContentOrNudity",
"violenceCartoonOrFantasy",
"violenceRealistic",
"violenceRealisticProlongedGraphicOrSadistic",
"gambling",
"unrestrictedWebAccess",
"kidsAgeBand",
"seventeenPlus"
],
"defaultSnippets": [
{
"label": "Basic",
"description": "Create an advisory that disables all age-rated properties, for when your app does nothing",
"body": {
"alcoholTobaccoOrDrugUseOrReferences": "NONE",
"contests": "NONE",
"gamblingSimulated": "NONE",
"horrorOrFearThemes": "NONE",
"matureOrSuggestiveThemes": "NONE",
"medicalOrTreatmentInformation": "NONE",
"profanityOrCrudeHumor": "NONE",
"sexualContentGraphicAndNudity": "NONE",
"sexualContentOrNudity": "NONE",
"violenceCartoonOrFantasy": "NONE",
"violenceRealistic": "NONE",
"violenceRealisticProlongedGraphicOrSadistic": "NONE",
"gambling": false,
"unrestrictedWebAccess": false,
"kidsAgeBand": null,
"seventeenPlus": false
}
},
{
"label": "Full",
"description": "Create an advisory that enables some of the age-rated properties",
"body": {
"alcoholTobaccoOrDrugUseOrReferences": "${1|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"contests": "${2|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"gamblingSimulated": "${3|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"horrorOrFearThemes": "${4|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"matureOrSuggestiveThemes": "${5|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"medicalOrTreatmentInformation": "${6|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"profanityOrCrudeHumor": "${7|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"sexualContentGraphicAndNudity": "${8|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"sexualContentOrNudity": "${9|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"violenceCartoonOrFantasy": "${10|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"violenceRealistic": "${11|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"violenceRealisticProlongedGraphicOrSadistic": "${12|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"unrestrictedWebAccess": false,
"gambling": false,
"kidsAgeBand": null,
"seventeenPlus": false
}
}
],
"properties": {
"alcoholTobaccoOrDrugUseOrReferences": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain alcohol, tobacco, or drug use or references?"
},
"contests": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain contests?"
},
"gamblingSimulated": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain simulated gambling?"
},
"medicalOrTreatmentInformation": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain medical or treatment information?"
},
"profanityOrCrudeHumor": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain profanity or crude humor?"
},
"sexualContentGraphicAndNudity": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain graphic sexual content and nudity?"
},
"sexualContentOrNudity": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain sexual content or nudity?"
},
"horrorOrFearThemes": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain horror or fear themes?"
},
"matureOrSuggestiveThemes": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain mature or suggestive themes?"
},
"violenceCartoonOrFantasy": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain cartoon or fantasy violence?"
},
"violenceRealisticProlongedGraphicOrSadistic": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain prolonged graphic or sadistic realistic violence?"
},
"violenceRealistic": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain realistic violence?"
},
"unrestrictedWebAccess": {
"type": "boolean",
"description": "Does your app contain unrestricted web access, such as with an embedded browser?"
},
"gambling": {
"type": "boolean",
"description": "Does your app contain gambling?"
},
"kidsAgeBand": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/definitions/apple/AppleKidsAge"
}
],
"description": "When parents visit the Kids category on the App Store, they expect the apps they find will protect their children’s data, provide only age-appropriate content, and require a parental gate in order to link out of the app, request permissions, or present purchasing opportunities. It’s critical that no personally identifiable information or device information be transmitted to third parties, and that advertisements are human-reviewed for age appropriateness in order to be displayed.\n@see https://developer.apple.com/news/?id=091202019a",
"markdownDescription": "When parents visit the Kids category on the App Store, they expect the apps they find will protect their children’s data, provide only age-appropriate content, and require a parental gate in order to link out of the app, request permissions, or present purchasing opportunities. It’s critical that no personally identifiable information or device information be transmitted to third parties, and that advertisements are human-reviewed for age appropriateness in order to be displayed.\n\n[Learn more](https://developer.apple.com/news/?id=091202019a)"
},
"seventeenPlus": {
"type": "boolean",
"description": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n@see https://help.apple.com/app-store-connect/#/dev599d50efb",
"markdownDescription": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n\n[Learn more](https://help.apple.com/app-store-connect/#/dev599d50efb)"
}
}
},
"AppleRelease": {
"type": "object",
"description": "Configure how the app is released in the app store.",
"additionalProperties": false,
"properties": {
"automaticRelease": {
"description": "If and how the app should automatically be released when approved by App Store review.",
"markdownDescription": "If and how the app should automatically be released when approved by App Store review.\n- `false` - Manually release the app after store approval. (default behavior)\n- `true` - Automatically release after store approval.\n- `Date` - Automatically schedule release on this date, after store approval.\n\nApple does not guarantee that your app is available at the chosen scheduled release date.",
"oneOf": [
{
"type": "string",
"format": "date-time"
},
{
"type": "boolean"
}
],
"defaultSnippets": [
{
"label": "Manual release",
"description": "Manually release the app after store approval",
"body": false
},
{
"label": "Automatic release",
"description": "Automatically release the app after store approval",
"body": true
},
{
"label": "Scheduled release",
"description": "Automatically release the app on this date, and store approval",
"body": "$CURRENT_YEAR-${1:$CURRENT_MONTH}-${2:$CURRENT_DATE}T${3:$CURRENT_HOUR}:00:00+00:00"
}
]
},
"phasedRelease": {
"type": "boolean",
"description": "Phased release for automatic updates lets you gradually release this update over a 7-day period to users who have turned on automatic updates. Keep in mind that this version will still be available to all users as a manual update from the App Store. You can pause the phased release for up to 30 days or release this update to all users at any time.\n@see https://help.apple.com/app-store-connect/#/dev3d65fcee1",
"markdownDescription": "Phased release for automatic updates lets you gradually release this update over a 7-day period to users who have turned on automatic updates.\n\nKeep in mind that this version will still be available to all users as a manual update from the App Store.\n\nYou can pause the phased release for up to 30 days or release this update to all users at any time.\n\n[Learn More](https://help.apple.com/app-store-connect/#/dev3d65fcee1)"
}
}
},
"AppleReview": {
"type": "object",
"description": "Info provided to the App Store review team to help them understand how to use your app.",
"required": [
"firstName",
"lastName",
"email",
"phone"
],
"defaultSnippets": [
{
"label": "Basic",
"description": "Only define the contact details in case communcation is needed with the App Store review team.",
"body": {
"firstName": "$1",
"lastName": "$2",
"email": "$3",
"phone": "+$4"
}
},
{
"label": "Full",
"description": "Define the demo account credentials, additional info, and contact details for the App Store review team.",
"body": {
"firstName": "$1",
"lastName": "$2",
"email": "$3",
"phone": "+$4",
"demoUsername": "$5",
"demoPassword": "$6",
"demoRequired": true,
"notes": "$7"
}
}
],
"properties": {
"firstName": {
"type": "string",
"description": "First name of contact in case communication is needed with the App Store review team.",
"minLength": 1
},
"lastName": {
"type": "string",
"description": "Last name of contact in case communication is needed with the App Store review team.",
"minLength": 1
},
"email": {
"type": "string",
"description": "Email address of contact in case communication is needed with the App Store review team.",
"format": "email"
},
"phone": {
"type": "string",
"description": "Phone number of contact in case communication is needed with the App Store review team. Preface the phone number with ‘+’ followed by the country code (for example, +44 844 209 0611)"
},
"demoUsername": {
"type": "string",
"description": "The user name to sign in to your app to review its features."
},
"demoPassword": {
"type": "string",
"description": "The password to sign in to your app to review its features."
},
"demoRequired": {
"type": "boolean",
"description": "A Boolean value that indicates if sign-in information is required to review all the features of your app. If users sign in using social media, provide information for an account for review. Credentials must be valid and active for duration of review.",
"markdownDescription": "A Boolean value that indicates if sign-in information is required to review all the features of your app. If users sign in using social media, provide information for an account for review.\n\n**Credentials must be valid and active for duration of review.**"
},
"notes": {
"type": "string",
"description": "Additional information about your app that can help during the review process. Do not include demo account details.",
"markdownDescription": "Additional information about your app that can help during the review process.\n\n**Do not include demo account details.**",
"minLength": 2,
"maxLength": 4000
}
}
}
},
"AppleConfig": {
"type": "object",
"description": "Configuration that is specific to the Apple App Store.",
"additionalProperties": false,
"properties": {
"version": {
"type": "string",
"description": "The version string to edit, or create, if it doesn't exist. All the metadata will be stored on this app version.",
"minLength": 1,
"defaultSnippets": [
{
"body": "${1:1}.${2:0}"
}
]
},
"copyright": {
"type": "string",
"description": "The updated company copyright. Example: 2021 Evan Bacon",
"defaultSnippets": [
{
"body": "$CURRENT_YEAR ${1:name}"
}
],
"meta": {
"versioned": true,
"liveEdits": true,
"storeInfo": "The name of the person or entity that owns the exclusive rights to your app, preceded by the year the rights were obtained (for example, \"2008 Acme Inc.\"). Do not provide a URL."
}
},
"advisory": {
"$ref": "#/definitions/apple/AppleAdvisory"
},
"categories": {
"$ref": "#/definitions/apple/AppleCategory"
},
"info": {
"description": "Localized core app info",
"type": "object",
"additionalProperties": false,
"properties": {
"ar-SA": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Arabic"
},
"ca": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Catalan"
},
"zh-Hans": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Chinese (Simplified)"
},
"zh-Hant": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Chinese (Traditional)"
},
"hr": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Croatian"
},
"cs": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Czech"
},
"da": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Danish"
},
"nl-NL": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Dutch"
},
"en-AU": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "English (Australia)"
},
"en-CA": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "English (Canada)"
},
"en-GB": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "English (U.K.)"
},
"en-US": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "English (U.S.)"
},
"fi": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Finnish"
},
"fr-FR": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "French"
},
"fr-CA": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "French (Canada)"
},
"de-DE": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "German"
},
"el": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Greek"
},
"he": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Hebrew"
},
"hi": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Hindi"
},
"hu": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Hungarian"
},
"id": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Indonesian"
},
"it": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Italian"
},
"ja": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Japanese"
},
"ko": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Korean"
},
"ms": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Malay"
},
"no": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Norwegian"
},
"pl": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Polish"
},
"pt-BR": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Portuguese (Brazil)"
},
"pt-PT": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Portuguese (Portugal)"
},
"ro": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Romanian"
},
"ru": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Russian"
},
"sk": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Slovak"
},
"es-MX": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Spanish (Mexico)"
},
"es-ES": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Spanish (Spain)"
},
"sv": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Swedish"
},
"th": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Thai"
},
"tr": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Turkish"
},
"uk": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Ukrainian"
},
"vi": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Vietnamese"
}
}
},
"release": {
"$ref": "#/definitions/apple/AppleRelease",
"description": "Release strategy"
},
"review": {
"$ref": "#/definitions/apple/AppleReview",
"description": "Info for the App Store reviewer"
}
}
}
},
"properties": {
"configVersion": {
"enum": [0],
"description": "The EAS metadata store configuration version"
},
"apple": {
"$ref": "#/definitions/AppleConfig"
}
},
"additionalProperties": false,
"required": [
"configVersion",
"apple"
]
}
@@ -1,13 +1,15 @@
#!/usr/bin/env sh
const fs = require('fs/promises');
OUTFILE="src/graphql/generated.ts"
const path = './src/graphql/generated.ts';
content="/**
(async () => {
const generatedCode = await fs.readFile(path, 'utf8');
const comment = `/**
* This file was generated using GraphQL Codegen
* Command: yarn generate-graphql-code
* Run this during development for automatic type generation when editing GraphQL documents
* For more info and docs, visit https://graphql-code-generator.com/
*/
"
*/\n\n`;
printf '%s\n%s\n' "$content" "$(cat $OUTFILE)" > $OUTFILE
await fs.writeFile(path, comment + generatedCode);
})();
+1 -1
View File
@@ -1,4 +1,4 @@
const { promises: fs } = require('fs');
const fs = require('fs/promises');
(async () => {
// Patch `oclif readme` path and link generation
+7
View File
@@ -0,0 +1,7 @@
const spawn = require('@expo/spawn-async');
(async () => {
if (!process.env.CLI_SIZE_CHECK) {
await spawn('yarn', ['oclif', 'manifest'], { stdio: 'inherit' });
}
})();
-10
View File
@@ -1,10 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
rm -rf build
yarn build
if [ -z "$CLI_SIZE_CHECK" ]; then
yarn oclif manifest
fi
+23
View File
@@ -0,0 +1,23 @@
import { JSONValue } from '@expo/json-file';
export class ApiV2Error extends Error {
override readonly name = 'ApiV2Error';
readonly expoApiV2ErrorCode: string;
readonly expoApiV2ErrorDetails?: JSONValue;
readonly expoApiV2ErrorServerStack?: string;
readonly expoApiV2ErrorMetadata?: object;
constructor(response: {
message: string;
code: string;
stack?: string;
details?: JSONValue;
metadata?: object;
}) {
super(response.message);
this.expoApiV2ErrorCode = response.code;
this.expoApiV2ErrorDetails = response.details;
this.expoApiV2ErrorServerStack = response.stack;
this.expoApiV2ErrorMetadata = response.metadata;
}
}
+9 -4
View File
@@ -1,9 +1,10 @@
import assert from 'assert';
import nock from 'nock';
import { ApiV2Error, api, getExpoApiBaseUrl } from '../api';
import { ApiV2Error } from '../ApiV2Error';
import { ApiV2Client, getExpoApiBaseUrl } from '../api';
describe('apiClient', () => {
describe(ApiV2Client, () => {
it('converts Expo APIv2 error to ApiV2Error', async () => {
nock(getExpoApiBaseUrl())
.post('/v2/test')
@@ -21,7 +22,9 @@ describe('apiClient', () => {
let error: Error | null = null;
try {
await api.postAsync('test', { body: {} });
await new ApiV2Client({ accessToken: null, sessionSecret: null }).postAsync('test', {
body: {},
});
} catch (e: any) {
error = e;
}
@@ -41,7 +44,9 @@ describe('apiClient', () => {
let error: Error | null = null;
try {
await api.postAsync('test', { body: {} });
await new ApiV2Client({ accessToken: null, sessionSecret: null }).postAsync('test', {
body: {},
});
} catch (e: any) {
error = e;
}
@@ -1,60 +0,0 @@
import { Actor } from '../user/User';
interface MockProject {
projectRoot: string;
projectTree: Record<string, string>;
appJSON: AppJSON;
packageJSON: PackageJSON;
}
interface PackageJSON {
name: string;
version: string;
description: string;
main: string;
}
interface AppJSON {
expo: {
name: string;
version: string;
slug: string;
sdkVersion: string;
owner?: string;
[key: string]: any;
};
}
export function createTestProject(
user: Actor,
appJSONExtraData?: Record<string, object | string>
): MockProject {
const projectRoot = '/test-project';
const packageJSON: PackageJSON = {
name: 'testing123',
version: '0.1.0',
description: 'fake description',
main: 'index.js',
};
const owner = appJSONExtraData?.owner as string | undefined;
const appJSON: AppJSON = {
expo: {
name: 'testing 123',
version: '0.1.0',
slug: 'testing-123',
sdkVersion: '33.0.0',
owner: owner ?? (user.__typename === 'User' ? user.username : undefined),
...appJSONExtraData,
},
};
return {
appJSON,
packageJSON,
projectRoot,
projectTree: {
[projectRoot + '/package.json']: JSON.stringify(packageJSON, null, 2),
[projectRoot + '/app.json']: JSON.stringify(appJSON, null, 2),
},
};
}
@@ -0,0 +1,232 @@
import RudderAnalytics from '@expo/rudder-sdk-node';
import os from 'os';
import { URL } from 'url';
import { v4 as uuidv4 } from 'uuid';
import { Actor, getActorDisplayName } from '../user/User';
import UserSettings from '../user/UserSettings';
import { easCliVersion } from '../utils/easCli';
const PLATFORM_TO_ANALYTICS_PLATFORM: Partial<Record<NodeJS.Platform, string>> = {
darwin: 'Mac',
win32: 'Windows',
linux: 'Linux',
};
export type AnalyticsEvent = CommandEvent | BuildEvent | SubmissionEvent | MetadataEvent;
export enum CommandEvent {
ACTION = 'action', // generic event type which is used to determine the 'daily active user' stat, include an `action: eas ${subcommand}` property inside of the event properties object
}
export enum SubmissionEvent {
SUBMIT_COMMAND = 'submit cli submit command',
SUBMIT_COMMAND_ATTEMPT = 'submit cli attempt',
SUBMIT_COMMAND_SUCCESS = 'submit cli success',
SUBMIT_COMMAND_FAIL = 'submit cli fail',
GATHER_CREDENTIALS_ATTEMPT = 'submit cli gather credentials attempt',
GATHER_CREDENTIALS_SUCCESS = 'submit cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'submit cli gather credentials fail',
GATHER_ARCHIVE_ATTEMPT = 'submit cli gather archive attempt',
GATHER_ARCHIVE_SUCCESS = 'submit cli gather archive success',
GATHER_ARCHIVE_FAIL = 'submit cli gather archive fail',
SUBMIT_REQUEST_ATTEMPT = 'submit cli request attempt',
SUBMIT_REQUEST_SUCCESS = 'submit cli request success',
SUBMIT_REQUEST_FAIL = 'submit cli request fail',
API_KEY_DOWNLOAD_FAIL = 'submit cli credentials api key download fail',
API_KEY_DOWNLOAD_RETRY = 'submit cli credentials api key download fail temporary',
API_KEY_DOWNLOAD_SUCCESS = 'submit cli credentials api key download succeed',
}
export enum BuildEvent {
BUILD_COMMAND = 'build cli build command',
PROJECT_UPLOAD_ATTEMPT = 'build cli project upload attempt',
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
GATHER_CREDENTIALS_ATTEMPT = 'build cli gather credentials attempt',
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
CONFIGURE_PROJECT_ATTEMPT = 'build cli configure project attempt',
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
BUILD_REQUEST_ATTEMPT = 'build cli build request attempt',
BUILD_REQUEST_SUCCESS = 'build cli build request success',
BUILD_REQUEST_FAIL = 'build cli build request fail',
BUILD_STATUS_COMMAND = 'build cli build status',
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
CREDENTIALS_SYNC_UPDATE_LOCAL_ATTEMPT = 'build cli credentials sync update local attempt',
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
CREDENTIALS_SYNC_UPDATE_REMOTE_ATTEMPT = 'build cli credentials sync update remote attempt',
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
}
export enum MetadataEvent {
APPLE_METADATA_DOWNLOAD = 'metadata cli download apple response',
APPLE_METADATA_UPLOAD = 'metadata cli upload apple response',
}
export type AnalyticsEventProperties = Record<string, string | number | boolean>;
/**
* The interface for commands to use to log events to analytics.
*/
export interface Analytics {
logEvent(name: AnalyticsEvent, properties: AnalyticsEventProperties): void;
}
/**
* The interface for commands to use to orchestrate the analytics system. Should only be necessary to use
* this within EASCommand.
*/
export interface AnalyticsWithOrchestration extends Analytics {
setActor(actor: Actor): void;
flushAsync(): Promise<void>;
}
const USER_SETTINGS_KEY_AMPLITUDE_ENABLED = 'amplitudeEnabled';
const USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID = 'amplitudeDeviceId';
const USER_SETTINGS_KEY_ANALYTICS_ENABLED = 'analyticsEnabled';
const USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID = 'analyticsDeviceId';
/**
* Sets the user's analytics enabled preference. Note that this will only take effect
* upon the next run of the CLI.
*/
export async function setAnalyticsEnabledAsync(enabled: boolean): Promise<void> {
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, enabled);
}
/**
* Returns the user's analytics enabled preference.
*/
export async function getAnalyticsEnabledAsync(): Promise<boolean> {
const analyticsEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, null);
return !!analyticsEnabled;
}
/**
* Create an instance of Analytics based on the user's analytics enabled preferences.
*/
export async function createAnalyticsAsync(): Promise<AnalyticsWithOrchestration> {
// TODO: remove after some time
const amplitudeEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_AMPLITUDE_ENABLED, null);
if (amplitudeEnabled !== null) {
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, amplitudeEnabled);
await UserSettings.deleteKeyAsync(USER_SETTINGS_KEY_AMPLITUDE_ENABLED);
}
const amplitudeDeviceId = await UserSettings.getAsync(
USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID,
null
);
if (amplitudeDeviceId !== null) {
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID, amplitudeDeviceId);
await UserSettings.deleteKeyAsync(USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID);
}
if (process.env.DISABLE_EAS_ANALYTICS) {
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, false);
}
const analyticsEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, true);
if (!analyticsEnabled) {
return new NoOpAnalytics();
}
const persistedDeviceId = await UserSettings.getAsync(
USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID,
null
);
const deviceId = persistedDeviceId ?? uuidv4();
if (!persistedDeviceId) {
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID, deviceId);
}
return new RudderstackAnalytics(deviceId);
}
class NoOpAnalytics implements AnalyticsWithOrchestration {
logEvent(): void {}
setActor(): void {}
async flushAsync(): Promise<void> {}
}
const RudderstackAnalyticsConfig =
process.env.EXPO_STAGING || process.env.EXPO_LOCAL
? {
// staging environment
rudderstackWriteKey: '1wpX20Da4ltFGSXbPFYUL00Chb7',
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
}
: {
// prod environment
rudderstackWriteKey: '1wpXLFxmujq86etH6G6cc90hPcC',
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
};
class RudderstackAnalytics implements AnalyticsWithOrchestration {
private readonly rudderstackClient = new RudderAnalytics(
RudderstackAnalyticsConfig.rudderstackWriteKey,
new URL('/v1/batch', RudderstackAnalyticsConfig.rudderstackDataPlaneURL).toString(),
{
flushInterval: 300,
}
);
private identifiedActor: Actor | null = null;
constructor(private readonly persistentDeviceId: string) {
// identify once with just anonymous ID. Once the actor is fetched, re-indentify with
// both so that they can be associated
this.rudderstackClient.identify({
anonymousId: persistentDeviceId,
});
}
public setActor(actor: Actor): void {
if (this.identifiedActor) {
return;
}
this.rudderstackClient.identify({
userId: actor.id,
anonymousId: this.persistentDeviceId,
traits: {
username: getActorDisplayName(actor),
user_id: actor.id,
user_type: actor.__typename,
},
});
this.identifiedActor = actor;
}
public logEvent(name: AnalyticsEvent, properties: AnalyticsEventProperties): void {
const userId = this.identifiedActor?.id;
const deviceId = this.persistentDeviceId;
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
this.rudderstackClient.track({
event: name,
properties: { ...properties, ...commonEventProperties },
...identity,
context: this.getRudderStackContext(),
});
}
public async flushAsync(): Promise<void> {
await this.rudderstackClient.flush();
}
private getRudderStackContext(): Record<string, any> {
const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform();
return {
os: { name: platform, version: os.release() },
device: { type: platform, model: platform },
app: { name: 'eas cli', version: easCliVersion ?? undefined },
};
}
}
+16 -11
View File
@@ -1,23 +1,28 @@
import { Analytics, Event } from './events';
export type TrackingContext = Record<string, string | number | boolean>;
import { Analytics, AnalyticsEvent, AnalyticsEventProperties } from './AnalyticsManager';
export async function withAnalyticsAsync<Result>(
analytics: Analytics,
fn: () => Promise<Result>,
analytics: {
attemptEvent: Event;
successEvent: Event;
failureEvent: Event;
trackingCtx: TrackingContext;
{
attemptEvent,
successEvent,
failureEvent,
properties,
}: {
attemptEvent: AnalyticsEvent;
successEvent: AnalyticsEvent;
failureEvent: AnalyticsEvent;
properties: AnalyticsEventProperties;
}
): Promise<Result> {
try {
analytics.logEvent(attemptEvent, properties);
const result = await fn();
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
analytics.logEvent(successEvent, properties);
return result;
} catch (error: any) {
Analytics.logEvent(analytics.failureEvent, {
...analytics.trackingCtx,
analytics.logEvent(failureEvent, {
...properties,
reason: error.message,
});
throw error;
-52
View File
@@ -1,52 +0,0 @@
import { logEvent } from './rudderstackClient';
export type Event = BuildEvent | SubmissionEvent;
export enum SubmissionEvent {
SUBMIT_COMMAND = 'submit cli submit command',
SUBMIT_COMMAND_ATTEMPT = 'submit cli attempt',
SUBMIT_COMMAND_SUCCESS = 'submit cli success',
SUBMIT_COMMAND_FAIL = 'submit cli fail',
GATHER_CREDENTIALS_ATTEMPT = 'submit cli gather credentials attempt',
GATHER_CREDENTIALS_SUCCESS = 'submit cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'submit cli gather credentials fail',
GATHER_ARCHIVE_ATTEMPT = 'submit cli gather archive attempt',
GATHER_ARCHIVE_SUCCESS = 'submit cli gather archive success',
GATHER_ARCHIVE_FAIL = 'submit cli gather archive fail',
SUBMIT_REQUEST_ATTEMPT = 'submit cli request attempt',
SUBMIT_REQUEST_SUCCESS = 'submit cli request success',
SUBMIT_REQUEST_FAIL = 'submit cli request fail',
}
export enum BuildEvent {
BUILD_COMMAND = 'build cli build command',
PROJECT_UPLOAD_ATTEMPT = 'build cli project upload attempt',
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
GATHER_CREDENTIALS_ATTEMPT = 'build cli gather credentials attempt',
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
CONFIGURE_PROJECT_ATTEMPT = 'build cli configure project attempt',
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
BUILD_REQUEST_ATTEMPT = 'build cli build request attempt',
BUILD_REQUEST_SUCCESS = 'build cli build request success',
BUILD_REQUEST_FAIL = 'build cli build request fail',
BUILD_STATUS_COMMAND = 'build cli build status',
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
CREDENTIALS_SYNC_UPDATE_LOCAL_ATTEMPT = 'build cli credentials sync update local attempt',
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
CREDENTIALS_SYNC_UPDATE_REMOTE_ATTEMPT = 'build cli credentials sync update remote attempt',
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
}
export class Analytics {
static logEvent(name: Event, properties: Record<string, any>): void {
logEvent(name, properties);
}
}
@@ -1,129 +0,0 @@
import RudderAnalytics from '@expo/rudder-sdk-node';
import os from 'os';
import { URL } from 'url';
import { v4 as uuidv4 } from 'uuid';
import UserSettings from '../user/UserSettings';
import { easCliVersion } from '../utils/easCli';
const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
darwin: 'Mac',
win32: 'Windows',
linux: 'Linux',
};
let rudderstackClient: RudderAnalytics | null = null;
let userIdentified = false;
let identifyData: {
userId: string;
deviceId: string;
traits: Record<string, any>;
} | null = null;
export async function initAsync(): Promise<void> {
// TODO: remove after some time
const amplitudeEnabled = await UserSettings.getAsync('amplitudeEnabled', null);
if (amplitudeEnabled !== null) {
await UserSettings.setAsync('analyticsEnabled', amplitudeEnabled);
await UserSettings.deleteKeyAsync('amplitudeEnabled');
}
const amplitudeDeviceId = await UserSettings.getAsync('amplitudeDeviceId', null);
if (amplitudeDeviceId !== null) {
await UserSettings.setAsync('analyticsDeviceId', amplitudeDeviceId);
await UserSettings.deleteKeyAsync('amplitudeDeviceId');
}
// TODO: cut here
if (process.env.DISABLE_EAS_ANALYTICS) {
await UserSettings.setAsync('analyticsEnabled', false);
}
const analyticsEnabled = await UserSettings.getAsync('analyticsEnabled', true);
if (analyticsEnabled) {
const config =
process.env.EXPO_STAGING || process.env.EXPO_LOCAL
? {
// staging environment
rudderstackWriteKey: '1wpX20Da4ltFGSXbPFYUL00Chb7',
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
}
: {
// prod environment
rudderstackWriteKey: '1wpXLFxmujq86etH6G6cc90hPcC',
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
};
rudderstackClient = new RudderAnalytics(
config.rudderstackWriteKey,
new URL('/v1/batch', config.rudderstackDataPlaneURL).toString(),
{
flushInterval: 300,
}
);
}
}
export async function setUserDataAsync(userId: string, traits: Record<string, any>): Promise<void> {
const savedDeviceId = await UserSettings.getAsync('analyticsDeviceId', null);
const deviceId = savedDeviceId ?? uuidv4();
if (!savedDeviceId) {
await UserSettings.setAsync('analyticsDeviceId', deviceId);
}
identifyData = {
userId,
deviceId,
traits,
};
ensureUserIdentified();
}
export async function flushAsync(): Promise<void> {
if (rudderstackClient) {
await rudderstackClient.flush();
}
}
export function logEvent(name: string, properties: Record<string, any> = {}): void {
if (!rudderstackClient) {
return;
}
ensureUserIdentified();
const { userId, deviceId } = identifyData ?? {};
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
rudderstackClient.track({
event: name,
properties: { ...properties, ...commonEventProperties },
...identity,
context: getRudderStackContext(),
});
}
function ensureUserIdentified(): void {
if (!rudderstackClient || userIdentified || !identifyData) {
return;
}
rudderstackClient.identify({
userId: identifyData.userId,
anonymousId: identifyData.deviceId,
traits: identifyData.traits,
});
userIdentified = true;
}
function getRudderStackContext(): Record<string, any> {
const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform();
return {
os: { name: platform, version: os.release() },
device: { type: platform, model: platform },
app: { name: 'eas cli', version: easCliVersion ?? undefined },
};
}
export enum AnalyticsEvent {
ACTION = 'action', // generic event type which is used to determine the 'daily active user' stat, include an `action: eas ${subcommand}` property inside of the event properties object
}
+59 -74
View File
@@ -1,93 +1,78 @@
import { JSONValue } from '@expo/json-file';
import { ApiV2Error } from './ApiV2Error';
import fetch, { RequestError, RequestInit } from './fetch';
import { getAccessToken, getSessionSecret } from './user/sessionStorage';
export class ApiV2Error extends Error {
readonly name = 'ApiV2Error';
readonly expoApiV2ErrorCode: string;
readonly expoApiV2ErrorDetails?: JSONValue;
readonly expoApiV2ErrorServerStack?: string;
readonly expoApiV2ErrorMetadata?: object;
constructor(response: {
message: string;
code: string;
stack?: string;
details?: JSONValue;
metadata?: object;
}) {
super(response.message);
this.expoApiV2ErrorCode = response.code;
this.expoApiV2ErrorDetails = response.details;
this.expoApiV2ErrorServerStack = response.stack;
this.expoApiV2ErrorMetadata = response.metadata;
}
}
interface RequestOptions {
body: JSONValue;
}
export const api = {
async putAsync(path: string, options: RequestOptions): Promise<any> {
return await requestAsync(path, { method: 'PUT', body: JSON.stringify(options.body) });
},
export class ApiV2Client {
constructor(
private readonly authInfo: {
accessToken: string | null;
sessionSecret: string | null;
}
) {}
async postAsync(path: string, options: RequestOptions): Promise<any> {
return await requestAsync(path, { method: 'POST', body: JSON.stringify(options.body) });
},
async deleteAsync(path: string): Promise<any> {
return await requestAsync(path, { method: 'DELETE' });
},
async getAsync(path: string): Promise<any> {
return await requestAsync(path, { method: 'GET' });
},
};
async function requestAsync(path: string, options: RequestInit): Promise<any> {
try {
const response = await fetch(`${getExpoApiBaseUrl()}/v2/${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...getAuthHeaders(),
},
});
return await response.json();
} catch (err) {
await handleApiErrorAsync(err);
public async putAsync(path: string, options: RequestOptions): Promise<any> {
return await this.requestAsync(path, { method: 'PUT', body: JSON.stringify(options.body) });
}
}
async function handleApiErrorAsync(err: any): Promise<void> {
if (err instanceof RequestError) {
let result: { [key: string]: any };
public async postAsync(path: string, options: RequestOptions): Promise<any> {
return await this.requestAsync(path, { method: 'POST', body: JSON.stringify(options.body) });
}
public async deleteAsync(path: string): Promise<any> {
return await this.requestAsync(path, { method: 'DELETE' });
}
public async getAsync(path: string): Promise<any> {
return await this.requestAsync(path, { method: 'GET' });
}
private getAuthHeaders(): Record<string, string> {
const token = this.authInfo.accessToken;
if (token) {
return { authorization: `Bearer ${token}` };
}
const sessionSecret = this.authInfo.sessionSecret;
if (sessionSecret) {
return { 'expo-session': sessionSecret };
}
return {};
}
private async requestAsync(path: string, options: RequestInit): Promise<any> {
try {
result = (await err.response.json()) as { [key: string]: any };
} catch {
throw new Error(`Malformed api response: ${await err.response.text()}`);
const response = await fetch(`${getExpoApiBaseUrl()}/v2/${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...this.getAuthHeaders(),
},
});
return await response.json();
} catch (err) {
await ApiV2Client.handleApiErrorAsync(err);
}
if (result.errors?.length) {
throw new ApiV2Error(result.errors[0]);
}
} else {
throw err;
}
}
function getAuthHeaders(): Record<string, string> {
const token = getAccessToken();
if (token) {
return { authorization: `Bearer ${token}` };
private static async handleApiErrorAsync(err: any): Promise<void> {
if (err instanceof RequestError) {
let result: { [key: string]: any };
try {
result = (await err.response.json()) as { [key: string]: any };
} catch {
throw new Error(`Malformed api response: ${await err.response.text()}`);
}
if (result.errors?.length) {
throw new ApiV2Error(result.errors[0]);
}
} else {
throw err;
}
}
const sessionSecret = getSessionSecret();
if (sessionSecret) {
return { 'expo-session': sessionSecret };
}
return {};
}
export function getExpoApiBaseUrl(): string {
+183
View File
@@ -0,0 +1,183 @@
import chalk from 'chalk';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { withErrorHandlingAsync } from '../graphql/client';
import {
CreateUpdateBranchForAppMutation,
CreateUpdateBranchForAppMutationVariables,
UpdateBranch,
UpdateBranchFragment,
} from '../graphql/generated';
import { BranchQuery } from '../graphql/queries/BranchQuery';
import Log from '../log';
import { formatBranch, getBranchDescription } from '../update/utils';
import { printJsonOnlyOutput } from '../utils/json';
import {
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
import { BranchNotFoundError } from './utils';
export const BRANCHES_LIMIT = 50;
export async function selectBranchOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
promptTitle,
displayTextForListItem,
paginatedQueryOptions,
}: {
projectId: string;
displayTextForListItem: (queryItem: UpdateBranchFragment) => string;
promptTitle: string;
paginatedQueryOptions: PaginatedQueryOptions;
}
): Promise<UpdateBranchFragment> {
if (paginatedQueryOptions.nonInteractive) {
throw new Error('Unable to select a branch in non-interactive mode.');
}
const selectedBranch = await paginatedQueryWithSelectPromptAsync({
limit: paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
queryBranchesOnProjectAsync(graphqlClient, limit, offset, projectId),
promptOptions: {
title: promptTitle,
getIdentifierForQueryItem: updateBranchFragment => updateBranchFragment.id,
createDisplayTextForSelectionPromptListItem: displayTextForListItem,
},
});
if (!selectedBranch) {
throw new Error(`Could not find any branches for project "${projectId}"`);
}
return selectedBranch;
}
export async function listAndRenderBranchesOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
paginatedQueryOptions,
}: {
projectId: string;
paginatedQueryOptions: PaginatedQueryOptions;
}
): Promise<void> {
if (paginatedQueryOptions.nonInteractive) {
const branches = await queryBranchesOnProjectAsync(
graphqlClient,
paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
paginatedQueryOptions.offset,
projectId
);
renderPageOfBranches(branches, paginatedQueryOptions);
} else {
await paginatedQueryWithConfirmPromptAsync({
limit: paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
queryBranchesOnProjectAsync(graphqlClient, limit, offset, projectId),
promptOptions: {
title: 'Load more branches?',
renderListItems: branches => renderPageOfBranches(branches, paginatedQueryOptions),
},
});
}
}
async function queryBranchesOnProjectAsync(
graphqlClient: ExpoGraphqlClient,
limit: number,
offset: number,
projectId: string
): Promise<UpdateBranchFragment[]> {
return await BranchQuery.listBranchesOnAppAsync(graphqlClient, {
appId: projectId,
limit,
offset,
});
}
function renderPageOfBranches(
currentPage: UpdateBranchFragment[],
{ json }: PaginatedQueryOptions
): void {
if (json) {
printJsonOnlyOutput(currentPage);
} else {
Log.addNewLineIfNone();
Log.log(chalk.bold('Branches:'));
Log.addNewLineIfNone();
Log.log(
currentPage
.map(branch => formatBranch(getBranchDescription(branch)))
.join(`\n\n${chalk.dim('———')}\n\n`)
);
}
}
export async function createUpdateBranchOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, name }: CreateUpdateBranchForAppMutationVariables
): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
const result = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
gql`
mutation CreateUpdateBranchForApp($appId: ID!, $name: String!) {
updateBranch {
createUpdateBranchForApp(appId: $appId, name: $name) {
id
name
}
}
}
`,
{
appId,
name,
}
)
.toPromise()
);
const newBranch = result.updateBranch.createUpdateBranchForApp;
if (!newBranch) {
throw new Error(`Could not create branch ${name}.`);
}
return newBranch;
}
export async function ensureBranchExistsAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
branchName,
}: {
appId: string;
branchName: string;
}
): Promise<{ branchId: string; createdBranch: boolean }> {
try {
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId,
name: branchName,
});
const { id } = updateBranch;
return { branchId: id, createdBranch: false };
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
appId,
name: branchName,
});
return { branchId: newUpdateBranch.id, createdBranch: true };
} else {
throw error;
}
}
}
+14
View File
@@ -0,0 +1,14 @@
import { getVcsClient } from '../vcs';
export async function getDefaultBranchNameAsync(): Promise<string> {
return (
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substring(2, 4)}`
);
}
export class BranchNotFoundError extends Error {
constructor(message?: string) {
super(message ?? 'Branch not found.');
}
}
@@ -0,0 +1,89 @@
import { instance, mock } from 'ts-mockito';
import { v4 as uuid } from 'uuid';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { selectBuildToCancelAsync } from '../../commands/build/cancel';
import { AppPlatform, BuildFragment, BuildPriority, BuildStatus } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { confirmAsync, selectAsync } from '../../prompts';
jest.mock('../../ora', () => ({
ora: jest.fn().mockImplementation(() => ({
start: jest.fn().mockImplementation(() => ({
stop: jest.fn(),
fail: jest.fn(),
})),
})),
}));
jest.mock('../../prompts', () => {
return {
selectAsync: jest.fn(),
confirmAsync: jest.fn(),
};
});
jest.mock('../../graphql/queries/BuildQuery', () => {
const actual = jest.requireActual('../../graphql/queries/BuildQuery');
return {
BuildQuery: {
...actual.BuildQuery,
viewBuildsOnAppAsync: jest.fn(),
},
};
});
describe(selectBuildToCancelAsync.name, () => {
const selectedBuildId = uuid();
const projectId = uuid();
beforeEach(() => {
jest
.mocked(BuildQuery.viewBuildsOnAppAsync)
.mockImplementation(async () => [
createMockBuildFragment({ projectId, buildId: selectedBuildId }),
]);
});
it('does not return build id when confirmation is rejected', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(null);
});
it('returns build id when confirmation is confirmed', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(selectAsync).mockResolvedValueOnce(selectedBuildId);
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(
selectedBuildId
);
});
});
function createMockBuildFragment({
projectId,
buildId,
}: {
projectId: string;
buildId?: string;
}): BuildFragment {
return {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
platform: AppPlatform.Android,
id: buildId ?? uuid(),
priority: BuildPriority.Normal,
project: {
__typename: 'App',
slug: 'test-project',
id: projectId,
name: 'test-project',
ownerAccount: {
__typename: 'Account',
id: uuid(),
name: 'test-account',
},
},
status: BuildStatus.InQueue,
};
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,14 @@
import { truncateGitCommitMessage } from '../metadata';
describe(truncateGitCommitMessage, () => {
it('returns undefined if no message was passed', () => {
expect(truncateGitCommitMessage(undefined)).toBeUndefined();
});
it('returns commit message', () => {
expect(truncateGitCommitMessage('a'.repeat(10))).toBe('a'.repeat(10));
});
it('truncates long commit messages', () => {
expect(truncateGitCommitMessage('a'.repeat(5000))).toBe(`${'a'.repeat(4093)}...`);
});
});
@@ -0,0 +1,142 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { ExitError } from '@oclif/core/lib/errors';
import path from 'path';
import { instance, mock, when } from 'ts-mockito';
import { CommonContext } from '../context';
import { validatePNGsForManagedProjectAsync } from '../validate';
const fixturesPath = path.join(__dirname, 'fixtures');
describe(validatePNGsForManagedProjectAsync, () => {
it('does not validate PNGs for generic projects', async () => {
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
when(ctxMock.workflow).thenReturn(Workflow.GENERIC);
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
});
it('does not validate PNGs for projects with SDK >= 47', async () => {
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
when(ctxMock.workflow).thenReturn(Workflow.GENERIC);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
android: { adaptiveIcon: { foregroundImage: 'assets/icon.jpg' } },
sdkVersion: '47.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
});
describe(Platform.ANDROID, () => {
it('exits if foregroundImage is not a file with .png extension', async () => {
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.ANDROID);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
android: { adaptiveIcon: { foregroundImage: 'assets/icon.jpg' } },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
});
it('exits if foregroundImage is not a png file', async () => {
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.ANDROID);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
android: { adaptiveIcon: { foregroundImage: path.join(fixturesPath, 'icon-jpg.png') } },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
});
it('does not throw if foregroundImage is a png file', async () => {
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.ANDROID);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
android: { adaptiveIcon: { foregroundImage: path.join(fixturesPath, 'icon-alpha.png') } },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
});
});
// Validating iOS PNGs is currently disabled
// See https://github.com/expo/eas-cli/pull/1477 for context
xdescribe(Platform.IOS, () => {
it('exits if icon is not a file with .png extension', async () => {
const ctxMock = mock<CommonContext<Platform.IOS>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.IOS);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
ios: { icon: path.join(fixturesPath, 'assets/icon.jpg') },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
});
it('exits if icon is not a png file', async () => {
const ctxMock = mock<CommonContext<Platform.IOS>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.IOS);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
ios: { icon: path.join(fixturesPath, 'icon-jpg.png') },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
});
it('exits if icon has alpha channel (transparency)', async () => {
const ctxMock = mock<CommonContext<Platform.IOS>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.IOS);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
ios: { icon: path.join(fixturesPath, 'icon-alpha.png') },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
});
it('does not throw if icon is a png file and does not have alpha channel', async () => {
const ctxMock = mock<CommonContext<Platform.IOS>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.IOS);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
ios: { icon: path.join(fixturesPath, 'icon-no-alpha.png') },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
});
});
});
@@ -73,39 +73,39 @@ describe(bumpVersionAsync, () => {
await bumpVersionAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/repo',
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('3.0.0');
expect(fakeExp.android?.versionCode).toBe(124);
expect(appJSON.expo.version).toBe('3.0.0');
expect(appJSON.expo.android.versionCode).toBe(124);
const buildGradle = await getAppBuildGradleAsync('/repo');
const buildGradle = await getAppBuildGradleAsync('/app');
assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('124');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
});
it('bumps expo.version and gradle versionCode when strategy = BumpStrategy.SHORT_VERSION', async () => {
it('bumps expo.version and gradle versionCode when strategy = BumpStrategy.APP_VERSION', async () => {
const nativeVersionName = '1.0.0'; // this should be overwritten by bumpVersionAsync
const fakeExp = initBareWorkflowProject({ versionName: nativeVersionName });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
projectDir: '/repo',
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('3.0.1');
expect(fakeExp.android?.versionCode).toBe(123);
expect(appJSON.expo.version).toBe('3.0.1');
expect(appJSON.expo.android.versionCode).toBe(123);
const buildGradle = await getAppBuildGradleAsync('/repo');
const buildGradle = await getAppBuildGradleAsync('/app');
assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.1');
@@ -116,17 +116,17 @@ describe(bumpVersionAsync, () => {
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
projectDir: '/repo',
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('3.0.0');
expect(fakeExp.android?.versionCode).toBe(123);
expect(appJSON.expo.version).toBe('3.0.0');
expect(appJSON.expo.android.versionCode).toBe(123);
const buildGradle = await getAppBuildGradleAsync('/repo');
const buildGradle = await getAppBuildGradleAsync('/app');
assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
@@ -140,11 +140,11 @@ describe(bumpVersionInAppJsonAsync, () => {
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/repo',
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('5.0.0');
expect(fakeExp.android?.versionCode).toBe(127);
expect(appJSON.expo.version).toBe('5.0.0');
@@ -156,11 +156,11 @@ describe(bumpVersionInAppJsonAsync, () => {
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
projectDir: '/repo',
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('5.0.1');
expect(fakeExp.android?.versionCode).toBe(126);
expect(appJSON.expo.version).toBe('5.0.1');
@@ -172,11 +172,11 @@ describe(bumpVersionInAppJsonAsync, () => {
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.NOOP,
projectDir: '/repo',
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('5.0.0');
expect(fakeExp.android?.versionCode).toBe(126);
expect(appJSON.expo.version).toBe('5.0.0');
@@ -189,7 +189,7 @@ describe(maybeResolveVersionsAsync, () => {
it('reads the versions from native code', async () => {
const exp = initBareWorkflowProject();
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/repo',
'/app',
exp,
{} as BuildProfile<Platform.ANDROID>
);
@@ -201,7 +201,7 @@ describe(maybeResolveVersionsAsync, () => {
it('reads the versions from expo config', async () => {
const exp = initManagedProject();
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/repo',
'/app',
exp,
{} as BuildProfile<Platform.ANDROID>
);
@@ -240,7 +240,7 @@ function initBareWorkflowProject({
}`,
'./android/app/src/main/AndroidManifest.xml': 'fake',
},
'/repo'
'/app'
);
return fakeExp;
@@ -261,7 +261,7 @@ function initManagedProject(): ExpoConfig {
expo: fakeExp,
}),
},
'/repo'
'/app'
);
return fakeExp;
+33 -10
View File
@@ -1,10 +1,12 @@
import { Android, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { AppVersionSource } from '@expo/eas-json';
import chalk from 'chalk';
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 {
@@ -13,7 +15,6 @@ import {
} from '../../project/android/applicationId';
import { resolveGradleBuildContextAsync } from '../../project/android/gradle';
import { toggleConfirmAsync } from '../../prompts';
import { findAccountByName } from '../../user/Account';
import {
BuildRequestSender,
CredentialsResult,
@@ -23,10 +24,15 @@ import {
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
import { transformMetadata } from '../graphql';
import { logCredentialsSource } from '../utils/credentials';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import {
checkGoogleServicesFileAsync,
checkNodeEnvVariable,
validatePNGsForManagedProjectAsync,
} from '../validate';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
import { resolveRemoteVersionCodeAsync } from './version';
export async function createAndroidContextAsync(
ctx: CommonContext<Platform.ANDROID>
@@ -44,23 +50,34 @@ This means that it will most likely produce an AAB and you will not be able to i
Log.newLine();
const confirmed = await toggleConfirmAsync({ message: 'Would you like to proceed?' });
if (!confirmed) {
Log.error('Please update eas.json and come back again.');
Log.error('Update eas.json and come back again.');
process.exit(1);
}
}
checkNodeEnvVariable(ctx);
await checkGoogleServicesFileAsync(ctx);
await validatePNGsForManagedProjectAsync(ctx);
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
if (ctx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp, ctx.user);
}
const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext);
const versionCodeOverride =
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? await resolveRemoteVersionCodeAsync(ctx.graphqlClient, {
projectDir: ctx.projectDir,
projectId: ctx.projectId,
exp: ctx.exp,
applicationId,
buildProfile,
})
: undefined;
return { applicationId, gradleContext };
return { applicationId, gradleContext, versionCodeOverride };
}
export async function prepareAndroidBuildAsync(
@@ -72,10 +89,14 @@ export async function prepareAndroidBuildAsync(
return await ensureAndroidCredentialsAsync(ctx);
},
syncProjectConfigurationAsync: async () => {
await syncProjectConfigurationAsync({
await syncProjectConfigurationAsync(ctx.graphqlClient, {
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile: ctx.buildProfile,
localAutoIncrement:
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? false
: ctx.buildProfile.autoIncrement,
projectId: ctx.projectId,
});
},
prepareJobAsync: async (
@@ -87,14 +108,16 @@ 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);
return await BuildMutation.createAndroidBuildAsync({
return await BuildMutation.createAndroidBuildAsync(ctx.graphqlClient, {
appId,
job: graphqlJob,
metadata: graphqlMetadata,
buildParams,
});
},
});
@@ -118,7 +141,7 @@ async function ensureAndroidCredentialsAsync(
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
app: {
account: nullthrows(
findAccountByName(ctx.user.accounts, ctx.accountName),
ctx.user.accounts.find(a => a.name === ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.projectName,
@@ -13,8 +13,10 @@ export function transformJob(job: Android.Job): AndroidJobInput {
secrets: job.secrets,
builderEnvironment: job.builderEnvironment,
cache: job.cache,
version: job.version?.versionCode ? { versionCode: job.version.versionCode } : undefined,
gradleCommand: job.gradleCommand,
artifactPath: job.artifactPath,
applicationArchivePath: job.applicationArchivePath,
buildArtifactPaths: job.buildArtifactPaths,
username: job.username,
buildType: job.buildType && transformBuildType(job.buildType),
developmentClient: job.developmentClient,
@@ -5,7 +5,6 @@ import slash from 'slash';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
@@ -24,7 +23,7 @@ export async function prepareJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Job> {
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const username = getUsername(ctx.exp, ctx.user);
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
@@ -72,9 +71,15 @@ export async function prepareJobAsync(
updates: { channel: ctx.buildProfile.channel },
developmentClient: buildProfile.developmentClient,
gradleCommand: buildProfile.gradleCommand,
artifactPath: buildProfile.artifactPath,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths,
buildType,
username,
...(ctx.android.versionCodeOverride && {
version: {
versionCode: ctx.android.versionCodeOverride,
},
}),
experimental: {
prebuildCommand: ctx.buildProfile.prebuildCommand,
},
@@ -1,34 +1,39 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { AndroidVersionAutoIncrement, BuildProfile } from '@expo/eas-json';
import { AndroidVersionAutoIncrement } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import Log from '../../log';
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { syncUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function syncProjectConfigurationAsync({
projectDir,
exp,
buildProfile,
}: {
projectDir: string;
exp: ExpoConfig;
buildProfile: BuildProfile<Platform.ANDROID>;
}): Promise<void> {
export async function syncProjectConfigurationAsync(
graphqlClient: ExpoGraphqlClient,
{
projectDir,
exp,
localAutoIncrement,
projectId,
}: {
projectDir: string;
exp: ExpoConfig;
localAutoIncrement?: AndroidVersionAutoIncrement;
projectId: string;
}
): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const { autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
await syncUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId);
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
} else {
+129 -12
View File
@@ -1,16 +1,22 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { AndroidConfig, Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { AppPlatform } from '../../graphql/generated';
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log from '../../log';
import { ora } from '../../ora';
import {
getAppBuildGradleAsync,
parseGradleCommand,
resolveConfigValue,
} from '../../project/android/gradleUtils';
import { getNextVersionCode } from '../../project/android/versions';
import { resolveWorkflowAsync } from '../../project/workflow';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
@@ -46,9 +52,10 @@ export async function bumpVersionAsync({
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
Log.log('Updated versions in app.json');
await writeVersionsToBuildGradleAsync({
await updateNativeVersionsAsync({
projectDir,
exp,
version: exp.version,
versionCode: exp.android?.versionCode,
});
Log.log('Synchronized versions with build gradle');
}
@@ -74,7 +81,7 @@ export async function bumpVersionInAppJsonAsync({
await bumpAppVersionAsync({ appVersion, projectDir, exp });
} else {
const versionCode = AndroidConfig.Version.getVersionCode(exp);
const bumpedVersionCode = versionCode + 1;
const bumpedVersionCode = getNextVersionCode(versionCode);
Log.log(
`Bumping ${chalk.bold('expo.android.versionCode')} from ${chalk.bold(
versionCode
@@ -105,7 +112,7 @@ export async function maybeResolveVersionsAsync(
appBuildVersion:
resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1',
};
} catch (err: any) {
} catch {
return {};
}
} else {
@@ -116,21 +123,33 @@ export async function maybeResolveVersionsAsync(
}
}
async function writeVersionsToBuildGradleAsync({
export async function updateNativeVersionsAsync({
projectDir,
exp,
version,
versionCode,
}: {
projectDir: string;
exp: ExpoConfig;
}): Promise<string> {
version?: string;
versionCode?: number;
}): Promise<void> {
const buildGradle = await readBuildGradleAsync(projectDir);
if (!buildGradle) {
throw new Error('This project is missing a build.gradle file.');
}
let updatedBuildGradle = AndroidConfig.Version.setVersionName(exp, buildGradle);
updatedBuildGradle = AndroidConfig.Version.setVersionCode(exp, updatedBuildGradle);
let updatedBuildGradle = buildGradle;
if (version !== undefined) {
updatedBuildGradle = updatedBuildGradle.replace(
new RegExp(`versionName ".*"`),
`versionName "${version}"`
);
}
if (versionCode !== undefined) {
updatedBuildGradle = updatedBuildGradle.replace(
new RegExp(`versionCode.*`),
`versionCode ${versionCode}`
);
}
await writeBuildGradleAsync({ projectDir, buildGradle: updatedBuildGradle });
return updatedBuildGradle;
}
async function readBuildGradleAsync(projectDir: string): Promise<string | undefined> {
@@ -151,3 +170,101 @@ async function writeBuildGradleAsync({
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
await fs.writeFile(buildGradlePath, buildGradle);
}
/**
* Returns buildNumber that will be used for the next build. If current build profile
* has an 'autoIncrement' option set, it increments the version on server.
*/
export async function resolveRemoteVersionCodeAsync(
graphqlClient: ExpoGraphqlClient,
{
projectDir,
projectId,
exp,
applicationId,
buildProfile,
}: {
projectDir: string;
projectId: string;
exp: ExpoConfig;
applicationId: string;
buildProfile: BuildProfile<Platform.ANDROID>;
}
): Promise<string> {
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
projectId,
AppPlatform.Android,
applicationId
);
const localVersions = await maybeResolveVersionsAsync(projectDir, exp, buildProfile);
let currentBuildVersion: string;
if (remoteVersions?.buildVersion) {
currentBuildVersion = remoteVersions.buildVersion;
} else {
if (localVersions.appBuildVersion) {
Log.warn(
'No remote versions are configured for this project, versionCode will be initialized based on the value from the local project.'
);
currentBuildVersion = localVersions.appBuildVersion;
} else {
Log.error(
`Remote versions are not configured and EAS CLI was not able to read the current version from your project. Use "eas build:version:set" to initialize remote versions.`
);
throw new Error('Remote versions are not configured.');
}
}
if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) {
return currentBuildVersion;
} else if (!buildProfile.autoIncrement && !remoteVersions?.buildVersion) {
const spinner = ora(
`Initializing versionCode with ${chalk.bold(currentBuildVersion)}.`
).start();
try {
await AppVersionMutation.createAppVersionAsync(graphqlClient, {
appId: projectId,
platform: AppPlatform.Android,
applicationIdentifier: applicationId,
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
buildVersion: currentBuildVersion,
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.ANDROID) ?? undefined,
});
spinner.succeed(`Initialized versionCode with ${chalk.bold(currentBuildVersion)}.`);
} catch (err) {
spinner.fail(`Failed to initialize versionCode with ${chalk.bold(currentBuildVersion)}.`);
throw err;
}
return currentBuildVersion;
} else {
const nextBuildVersion = getNextVersionCode(currentBuildVersion);
const spinner = ora(
`Incrementing versionCode from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
nextBuildVersion
)}.`
).start();
try {
await AppVersionMutation.createAppVersionAsync(graphqlClient, {
appId: projectId,
platform: AppPlatform.Android,
applicationIdentifier: applicationId,
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
buildVersion: String(nextBuildVersion),
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.ANDROID) ?? undefined,
});
spinner.succeed(
`Incremented versionCode from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
nextBuildVersion
)}.`
);
} catch (err) {
spinner.fail(
`Failed to increment versionCode from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
nextBuildVersion
)}.`
);
throw err;
}
return String(nextBuildVersion);
}
}
+123 -54
View File
@@ -5,26 +5,28 @@ import cliProgress from 'cli-progress';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { BuildEvent } from '../analytics/AnalyticsManager';
import { withAnalyticsAsync } from '../analytics/common';
import { BuildEvent } from '../analytics/events';
import { getExpoWebsiteBaseUrl } from '../api';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
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 Log, { learnMore, link } from '../log';
import { Ora, ora } from '../ora';
import {
appPlatformDisplayNames,
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../platform';
import { uploadAsync } from '../uploads';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
@@ -53,11 +55,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,
@@ -65,21 +78,26 @@ export async function prepareBuildRequestForPlatformAsync<
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
const { ctx } = builder;
const credentialsResult = await withAnalyticsAsync(
ctx.analytics,
async () => await builder.ensureCredentialsAsync(ctx),
{
attemptEvent: BuildEvent.GATHER_CREDENTIALS_ATTEMPT,
successEvent: BuildEvent.GATHER_CREDENTIALS_SUCCESS,
failureEvent: BuildEvent.GATHER_CREDENTIALS_FAIL,
trackingCtx: ctx.trackingCtx,
properties: ctx.analyticsEventProperties,
}
);
await withAnalyticsAsync(async () => await builder.syncProjectConfigurationAsync(ctx), {
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
trackingCtx: ctx.trackingCtx,
});
await withAnalyticsAsync(
ctx.analytics,
async () => await builder.syncProjectConfigurationAsync(ctx),
{
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
properties: ctx.analyticsEventProperties,
}
);
if (await getVcsClient().isCommitRequiredAsync()) {
Log.addNewLineIfNone();
@@ -95,11 +113,12 @@ export async function prepareBuildRequestForPlatformAsync<
path: (await makeProjectTarballAsync()).path,
} as const)
: ({
type: ArchiveSourceType.S3,
type: ArchiveSourceType.GCS,
bucketKey: await uploadProjectAsync(ctx),
} as const);
const metadata = await collectMetadataAsync(ctx);
const buildParams = resolveBuildParamsInput(ctx);
const job = await builder.prepareJobAsync(ctx, {
projectArchive,
credentials: credentialsResult?.credentials,
@@ -107,11 +126,11 @@ export async function prepareBuildRequestForPlatformAsync<
return async () => {
if (ctx.localBuildOptions.enable) {
await runLocalBuildAsync(job, ctx.localBuildOptions);
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions);
return undefined;
} else {
try {
return await sendBuildRequestAsync(builder, job, metadata);
return await sendBuildRequestAsync(builder, job, metadata, buildParams);
} catch (error: any) {
handleBuildRequestError(error, job.platform);
}
@@ -121,18 +140,40 @@ export async function prepareBuildRequestForPlatformAsync<
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.');
Log.error('EAS Build API has changed. 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.'
`EAS Build is down for maintenance. Try again later. Check ${link(
'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.'
`EAS Build free tier is temporarily disabled and we are not accepting any new builds. Try again later. ${learnMore(
'https://expo.fyi/eas-build-queues'
)}`
);
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED_IOS'
) {
Log.error(
`EAS Build free tier is temporarily disabled for iOS and we are not accepting any new builds. Try again later. ${learnMore(
'https://expo.fyi/eas-build-queues'
)}`
);
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED_ANDROID'
) {
Log.error(
`EAS Build free tier is temporarily disabled for Android and we are not accepting any new builds. Try again later. ${learnMore(
'https://expo.fyi/eas-build-queues'
)}`
);
throw new Error('Build request failed.');
} else if (
@@ -144,7 +185,7 @@ function handleBuildRequestError(error: any, platform: Platform): never {
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.'
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, report the issue.'
);
}
throw error;
@@ -156,12 +197,31 @@ async function uploadProjectAsync<TPlatform extends Platform>(
let projectTarballPath;
try {
return await withAnalyticsAsync(
ctx.analytics,
async () => {
Log.newLine();
Log.log(
`Compressing project files and uploading to EAS Build. ${learnMore(
'https://expo.fyi/eas-build-archive'
)}`
);
const projectTarball = await makeProjectTarballAsync();
if (projectTarball.size > 1024 * 1024 * 100) {
Log.warn(
`Your project archive is ${formatBytes(
projectTarball.size
)}. You can reduce its size and the time it takes to upload by excluding files that are unnecessary for the build process in ${chalk.bold(
'.easignore'
)} file. ${learnMore('https://expo.fyi/eas-build-archive')}`
);
}
projectTarballPath = projectTarball.path;
const { bucketKey } = await uploadAsync(
UploadSessionType.EasBuildProjectSources,
const bucketKey = await uploadFileAtPathToGCSAsync(
ctx.graphqlClient,
UploadSessionType.EasBuildGcsProjectSources,
projectTarball.path,
createProgressTracker({
total: projectTarball.size,
@@ -169,10 +229,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 +238,7 @@ async function uploadProjectAsync<TPlatform extends Platform>(
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
successEvent: BuildEvent.PROJECT_UPLOAD_SUCCESS,
failureEvent: BuildEvent.PROJECT_UPLOAD_FAIL,
trackingCtx: ctx.trackingCtx,
properties: ctx.analyticsEventProperties,
}
);
} finally {
@@ -194,10 +251,12 @@ 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(
ctx.analytics,
async () => {
if (Log.isDebug) {
Log.log(`Starting ${requestedPlatformDisplayNames[job.platform]} build`);
@@ -206,7 +265,8 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
ctx.projectId,
job,
metadata
metadata,
buildParams
);
printDeprecationWarnings(deprecationInfo);
@@ -216,7 +276,7 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
attemptEvent: BuildEvent.BUILD_REQUEST_ATTEMPT,
successEvent: BuildEvent.BUILD_REQUEST_SUCCESS,
failureEvent: BuildEvent.BUILD_REQUEST_FAIL,
trackingCtx: ctx.trackingCtx,
properties: ctx.analyticsEventProperties,
}
);
}
@@ -224,38 +284,40 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
type MaybeBuildFragment = BuildFragment | null;
export async function waitForBuildEndAsync(
graphqlClient: ExpoGraphqlClient,
{ 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,
} = {}
{ intervalSec = 10 } = {}
): Promise<MaybeBuildFragment[]> {
const b = `build${buildIds.length > 1 ? 's' : ''}`;
Log.log(`Waiting for ${b} to complete. You can press Ctrl+C to exit.`);
const spinner = ora(`Waiting for ${b} to complete.`).start();
const endTime = new Date().getTime() + timeoutSec * 1000;
while (new Date().getTime() <= endTime) {
const builds = await getBuildsSafelyAsync(buildIds);
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();
}
while (true) {
const builds = await getBuildsSafelyAsync(graphqlClient, buildIds);
const { refetch } =
builds.length === 1
? await handleSingleBuildProgressAsync({ build: builds[0], accountName }, { spinner })
: await handleMultipleBuildsProgressAsync({ builds }, { spinner });
: await handleMultipleBuildsProgressAsync({ builds }, { spinner, originalSpinnerText });
if (!refetch) {
return builds;
}
await sleepAsync(intervalSec * 1000);
}
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[]> {
async function getBuildsSafelyAsync(
graphqlClient: ExpoGraphqlClient,
buildIds: string[]
): Promise<MaybeBuildFragment[]> {
const promises = buildIds.map(async buildId => {
try {
return await BuildQuery.byIdAsync(buildId, { useCache: false });
return await BuildQuery.byIdAsync(graphqlClient, buildId, { useCache: false });
} catch (err) {
Log.debug('Failed to fetch the build status', err);
return null;
@@ -327,7 +389,7 @@ async function handleSingleBuildProgressAsync(
);
}
Log.newLine();
Log.log('Waiting in queue');
Log.log(`Waiting in ${priorityToQueueDisplayName[build.priority]}`);
queueProgressBar.start(
build.initialQueuePosition + 1,
build.initialQueuePosition - build.queuePosition + 1,
@@ -360,6 +422,12 @@ async function handleSingleBuildProgressAsync(
return { refetch: true };
}
const priorityToQueueDisplayName: Record<BuildPriority, string> = {
[BuildPriority.Normal]: 'queue',
[BuildPriority.NormalPlus]: 'queue',
[BuildPriority.High]: 'priority queue',
};
const statusToDisplayName: Record<BuildStatus, string> = {
[BuildStatus.New]: 'waiting to enter the queue (concurrency limit reached)',
[BuildStatus.InQueue]: 'in queue',
@@ -373,7 +441,7 @@ const platforms = [AppPlatform.Android, AppPlatform.Ios];
async function handleMultipleBuildsProgressAsync(
{ builds: maybeBuilds }: { builds: MaybeBuildFragment[] },
{ spinner }: { spinner: Ora }
{ spinner, originalSpinnerText }: { spinner: Ora; originalSpinnerText: string }
): Promise<BuildProgressResult> {
const buildCount = maybeBuilds.length;
const builds = maybeBuilds.filter<BuildFragment>(isBuildFragment);
@@ -393,7 +461,7 @@ async function handleMultipleBuildsProgressAsync(
}
return { refetch: false };
} else {
spinner.text = formatPendingBuildsText(builds);
spinner.text = formatPendingBuildsText(originalSpinnerText, builds);
return { refetch: true };
}
}
@@ -412,9 +480,10 @@ function formatSettledBuildsText(builds: BuildFragment[]): string {
.join('\n ');
}
function formatPendingBuildsText(builds: BuildFragment[]): string {
return platforms
.map(platform => {
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 = '';
@@ -437,8 +506,8 @@ function formatPendingBuildsText(builds: BuildFragment[]): string {
return `${appPlatformEmojis[platform]} ${
appPlatformDisplayNames[platform]
} build - status: ${chalk.bold(status)}${extraInfo}`;
})
.join('\n ');
}),
].join('\n ');
}
function isBuildFragment(maybeBuild: MaybeBuildFragment): maybeBuild is BuildFragment {
+3 -3
View File
@@ -1,5 +1,5 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJson, EasJsonReader } from '@expo/eas-json';
import { EasJson, EasJsonAccessor } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
@@ -24,7 +24,7 @@ interface ConfigureParams {
export async function ensureProjectConfiguredAsync(
configureParams: ConfigureParams
): Promise<boolean> {
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(configureParams.projectDir))) {
if (await fs.pathExists(EasJsonAccessor.formatEasJsonPath(configureParams.projectDir))) {
return false;
}
@@ -89,7 +89,7 @@ const EAS_JSON_BARE_DEFAULT: EasJson = {
};
async function createEasJsonAsync(projectDir: string): Promise<void> {
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
const easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir);
const hasAndroidNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
+14 -5
View File
@@ -1,11 +1,12 @@
import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import { BuildProfile, EasJson } from '@expo/eas-json';
import { TrackingContext } from '../analytics/common';
import { Analytics, AnalyticsEventProperties } from '../analytics/AnalyticsManager';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
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';
@@ -16,31 +17,39 @@ export type CommonContext<T extends Platform> = Omit<BuildContext<T>, 'android'
export interface AndroidBuildContext {
applicationId: string;
gradleContext?: GradleBuildContext;
versionCodeOverride?: string;
}
export interface IosBuildContext {
bundleIdentifier: string;
applicationTargetBuildSettings: XCBuildConfiguration['buildSettings'];
applicationTarget: Target;
targets: Target[];
xcodeBuildContext: XcodeBuildContext;
buildNumberOverride?: string;
}
export interface BuildContext<T extends Platform> {
accountName: string;
easJsonCliConfig: EasJson['cli'];
buildProfile: BuildProfile<T>;
buildProfileName: string;
resourceClass: BuildResourceClass;
clearCache: boolean;
credentialsCtx: CredentialsContext;
exp: ExpoConfig;
localBuildOptions: LocalBuildOptions;
nonInteractive: boolean;
noWait: boolean;
runFromCI: boolean;
platform: T;
projectDir: string;
projectId: string;
projectName: string;
trackingCtx: TrackingContext;
message?: string;
analyticsEventProperties: AnalyticsEventProperties;
user: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
workflow: Workflow;
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
ios: T extends Platform.IOS ? IosBuildContext : undefined;
+50 -21
View File
@@ -1,17 +1,18 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { BuildProfile, EasJson } from '@expo/eas-json';
import JsonFile from '@expo/json-file';
import getenv from 'getenv';
import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
import { TrackingContext } from '../analytics/common';
import { Analytics, BuildEvent } from '../analytics/events';
import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/AnalyticsManager';
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { CredentialsContext } from '../credentials/context';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
import { BuildResourceClass } from '../graphql/generated';
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { findAccountByName } from '../user/Account';
import { ensureLoggedInAsync } from '../user/actions';
import { Actor } from '../user/User';
import { createAndroidContextAsync } from './android/build';
import { BuildContext, CommonContext } from './context';
import { createIosContextAsync } from './ios/build';
@@ -20,35 +21,53 @@ import { LocalBuildOptions } from './local';
export async function createBuildContextAsync<T extends Platform>({
buildProfileName,
buildProfile,
easJsonCliConfig,
clearCache = false,
localBuildOptions,
nonInteractive = false,
nonInteractive,
noWait,
platform,
projectDir,
resourceClass,
message,
actor,
graphqlClient,
analytics,
getDynamicProjectConfigAsync,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
easJsonCliConfig: EasJson['cli'];
clearCache: boolean;
localBuildOptions: LocalBuildOptions;
nonInteractive: boolean;
noWait: boolean;
platform: T;
projectDir: string;
resourceClass: BuildResourceClass;
message?: string;
actor: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
getDynamicProjectConfigAsync: DynamicConfigContextFn;
}): Promise<BuildContext<T>> {
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
const { exp, projectId } = await getDynamicProjectConfigAsync({ env: buildProfile.env });
const user = await ensureLoggedInAsync();
const accountName = getProjectAccountName(exp, user);
const projectName = exp.slug;
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const workflow = await resolveWorkflowAsync(projectDir, platform);
const accountId = findAccountByName(user.accounts, accountName)?.id;
const accountId = account.id;
const runFromCI = getenv.boolish('CI', false);
const credentialsCtx = new CredentialsContext({
exp,
projectInfo: { exp, projectId },
nonInteractive,
projectDir,
user,
user: actor,
graphqlClient,
analytics,
env: buildProfile.env,
easJsonCliConfig,
});
const devClientProperties = getDevClientEventProperties({
@@ -56,32 +75,42 @@ export async function createBuildContextAsync<T extends Platform>({
projectDir,
buildProfile,
});
const trackingCtx = {
const analyticsEventProperties = {
tracking_id: uuidv4(),
platform,
...(exp.sdkVersion && { sdk_version: exp.sdkVersion }),
...(accountId && { account_id: accountId }),
project_id: projectId,
project_type: workflow,
...devClientProperties,
no_wait: noWait,
run_from_ci: runFromCI,
};
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
analytics.logEvent(BuildEvent.BUILD_COMMAND, analyticsEventProperties);
const commonContext: CommonContext<T> = {
accountName,
accountName: account.name,
buildProfile,
buildProfileName,
resourceClass,
easJsonCliConfig,
clearCache,
credentialsCtx,
exp,
localBuildOptions,
nonInteractive,
noWait,
platform,
projectDir,
projectId,
projectName,
trackingCtx,
user,
analyticsEventProperties,
user: actor,
graphqlClient,
analytics,
workflow,
message,
runFromCI,
};
if (platform === Platform.ANDROID) {
const common = commonContext as CommonContext<Platform.ANDROID>;
@@ -106,7 +135,7 @@ function getDevClientEventProperties({
platform: Platform;
projectDir: string;
buildProfile: BuildProfile;
}): Partial<TrackingContext> {
}): Partial<AnalyticsEventProperties> {
let includesDevClient;
const version = tryGetDevClientVersion(projectDir);
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
+5
View File
@@ -16,6 +16,11 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
type: ProjectArchiveSourceType.S3,
bucketKey: archiveSource.bucketKey,
};
} else if (archiveSource.type === ArchiveSourceType.GCS) {
return {
type: ProjectArchiveSourceType.Gcs,
bucketKey: archiveSource.bucketKey,
};
} else if (archiveSource.type === ArchiveSourceType.URL) {
return {
type: ProjectArchiveSourceType.Url,
@@ -3,6 +3,7 @@ import { IOSConfig } from '@expo/config-plugins';
import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import type { XCBuildConfiguration } from 'xcode';
import { readPlistAsync } from '../../../utils/plist';
import {
@@ -17,6 +18,12 @@ import {
jest.mock('fs');
const getXCBuildConfigurationFromPbxproj = jest.spyOn(
IOSConfig.Target,
'getXCBuildConfigurationFromPbxproj'
);
const getPbxproj = jest.spyOn(IOSConfig.XcodeUtils, 'getPbxproj');
afterAll(async () => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
@@ -51,14 +58,34 @@ describe(evaluateTemplateString, () => {
// bare workflow
describe(bumpVersionAsync, () => {
beforeEach(() => {
getPbxproj.mockImplementation(() => ({} as any));
});
afterEach(() => {
getXCBuildConfigurationFromPbxproj.mockReset();
getPbxproj.mockReset();
});
it('bumps expo.ios.buildNumber and CFBundleVersion when strategy = BumpStrategy.BUILD_NUMBER', async () => {
const fakeExp = initBareWorkflowProject();
getXCBuildConfigurationFromPbxproj.mockImplementation(
() =>
({
buildSettings: {
INFOPLIST_FILE: 'myproject/Info.plist',
},
} as XCBuildConfiguration)
);
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
projectDir: '/repo',
exp: fakeExp,
buildSettings: {},
targets: [
{
targetName: 'example',
bundleIdentifier: 'example.com',
entitlements: {},
},
],
});
const appJSON = await fs.readJSON('/repo/app.json');
@@ -76,13 +103,25 @@ describe(bumpVersionAsync, () => {
it('bumps expo.ios.buildNumber and CFBundleVersion for non default Info.plist location', async () => {
const fakeExp = initBareWorkflowProject({ infoPlistName: 'Info2.plist' });
getXCBuildConfigurationFromPbxproj.mockImplementation(
() =>
({
buildSettings: {
INFOPLIST_FILE: '$(SRCROOT)/myproject/Info2.plist',
},
} as XCBuildConfiguration)
);
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
projectDir: '/repo',
exp: fakeExp,
buildSettings: {
INFOPLIST_FILE: '$(SRCROOT)/myproject/Info2.plist',
},
targets: [
{
targetName: 'example',
bundleIdentifier: 'example.com',
entitlements: {},
},
],
});
const appJSON = await fs.readJSON('/repo/app.json');
@@ -97,14 +136,27 @@ describe(bumpVersionAsync, () => {
expect(infoPlist['CFBundleVersion']).toBe('2');
});
it('bumps expo.version and CFBundleShortVersionString when strategy = BumpStrategy.SHORT_VERSION', async () => {
it('bumps expo.version and CFBundleShortVersionString when strategy = BumpStrategy.APP_VERSION', async () => {
const fakeExp = initBareWorkflowProject();
getXCBuildConfigurationFromPbxproj.mockImplementation(
() =>
({
buildSettings: {
INFOPLIST_FILE: 'myproject/Info.plist',
},
} as XCBuildConfiguration)
);
await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
projectDir: '/repo',
exp: fakeExp,
buildSettings: {},
targets: [
{
targetName: 'example',
bundleIdentifier: 'example.com',
entitlements: {},
},
],
});
const appJSON = await fs.readJSON('/repo/app.json');
@@ -121,12 +173,25 @@ describe(bumpVersionAsync, () => {
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initBareWorkflowProject();
getXCBuildConfigurationFromPbxproj.mockImplementation(
() =>
({
buildSettings: {
INFOPLIST_FILE: 'myproject/Info.plist',
},
} as XCBuildConfiguration)
);
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
projectDir: '/repo',
exp: fakeExp,
buildSettings: {},
targets: [
{
targetName: 'example',
bundleIdentifier: 'example.com',
entitlements: {},
},
],
});
const appJSON = await fs.readJSON('/repo/app.json');
@@ -288,9 +353,9 @@ describe(getInfoPlistPath, () => {
function initBareWorkflowProject({
appVersion = '1.0.0',
version = '1',
buildNumber = '1',
infoPlistName = 'Info.plist',
}: { appVersion?: string; version?: string; infoPlistName?: string } = {}): ExpoConfig {
}: { appVersion?: string; buildNumber?: string; infoPlistName?: string } = {}): ExpoConfig {
const fakeExp: ExpoConfig = {
name: 'myproject',
slug: 'myproject',
@@ -311,7 +376,7 @@ function initBareWorkflowProject({
<key>CFBundleShortVersionString</key>
<string>${appVersion}</string>
<key>CFBundleVersion</key>
<string>${version}</string>
<string>${buildNumber}</string>
</dict>
</plist>`,
},
+33 -28
View File
@@ -1,8 +1,8 @@
import { IOSConfig } from '@expo/config-plugins';
import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import type { XCBuildConfiguration } from 'xcode';
import { AppVersionSource } from '@expo/eas-json';
import { IosCredentials, Target } from '../../credentials/ios/types';
import { IosCredentials } 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';
@@ -10,11 +10,16 @@ import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/ta
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommonContext, IosBuildContext } from '../context';
import { transformMetadata } from '../graphql';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import {
checkGoogleServicesFileAsync,
checkNodeEnvVariable,
validatePNGsForManagedProjectAsync,
} from '../validate';
import { ensureIosCredentialsAsync } from './credentials';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
import { resolveRemoteBuildNumberAsync } from './version';
export async function createIosContextAsync(
ctx: CommonContext<Platform.IOS>
@@ -22,11 +27,12 @@ export async function createIosContextAsync(
const { buildProfile } = ctx;
if (ctx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp, ctx.user);
}
checkNodeEnvVariable(ctx);
await checkGoogleServicesFileAsync(ctx);
await validatePNGsForManagedProjectAsync(ctx);
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
@@ -43,14 +49,22 @@ export async function createIosContextAsync(
env: buildProfile.env,
});
const applicationTarget = findApplicationTarget(targets);
const applicationTargetBuildSettings = resolveBuildSettings(ctx, applicationTarget);
const buildNumberOverride =
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? await resolveRemoteBuildNumberAsync(ctx.graphqlClient, {
projectDir: ctx.projectDir,
projectId: ctx.projectId,
exp: ctx.exp,
applicationTarget,
buildProfile,
})
: undefined;
return {
bundleIdentifier: applicationTarget.bundleIdentifier,
applicationTarget,
applicationTargetBuildSettings,
targets,
xcodeBuildContext,
buildNumberOverride,
};
}
@@ -63,11 +77,15 @@ export async function prepareIosBuildAsync(
return ensureIosCredentialsAsync(ctx, ctx.ios.targets);
},
syncProjectConfigurationAsync: async () => {
await syncProjectConfigurationAsync({
await syncProjectConfigurationAsync(ctx.graphqlClient, {
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile: ctx.buildProfile,
buildSettings: ctx.ios.applicationTargetBuildSettings,
targets: ctx.ios.targets,
localAutoIncrement:
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? false
: ctx.buildProfile.autoIncrement,
projectId: ctx.projectId,
});
},
prepareJobAsync: async (
@@ -82,30 +100,17 @@ 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);
return await BuildMutation.createIosBuildAsync({
return await BuildMutation.createIosBuildAsync(ctx.graphqlClient, {
appId,
job: graphqlJob,
metadata: graphqlMetadata,
buildParams,
});
},
});
}
function resolveBuildSettings(
ctx: CommonContext<Platform.IOS>,
applicationTarget: Target
): XCBuildConfiguration['buildSettings'] {
if (ctx.workflow === Workflow.MANAGED) {
return {};
}
const project = IOSConfig.XcodeUtils.getPbxproj(ctx.projectDir);
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
targetName: applicationTarget.targetName,
buildConfiguration: applicationTarget.buildConfiguration,
});
return xcBuildConfiguration?.buildSettings ?? {};
}
@@ -1,7 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
import { getAppFromContextAsync } from '../../credentials/ios/actions/BuildCredentialsUtils';
import { IosCredentials, Target } from '../../credentials/ios/types';
import { CredentialsResult } from '../build';
import { BuildContext } from '../context';
@@ -16,7 +16,7 @@ export async function ensureIosCredentialsAsync(
}
const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, {
app: getAppFromContext(buildCtx.credentialsCtx),
app: await getAppFromContextAsync(buildCtx.credentialsCtx),
targets,
distribution: buildCtx.buildProfile.distribution ?? 'store',
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
+4 -4
View File
@@ -1,4 +1,4 @@
import { Env, Ios } from '@expo/eas-build-job';
import { Ios } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
@@ -14,9 +14,11 @@ export function transformJob(job: Ios.Job): IosJobInput {
secrets: transformIosSecrets(job.secrets),
builderEnvironment: job.builderEnvironment,
cache: job.cache,
version: job.version?.buildNumber ? { buildNumber: job.version.buildNumber } : undefined,
scheme: job.scheme,
buildConfiguration: job.buildConfiguration,
artifactPath: job.artifactPath,
applicationArchivePath: job.applicationArchivePath,
buildArtifactPaths: job.buildArtifactPaths,
username: job.username,
developmentClient: job.developmentClient,
simulator: job.simulator,
@@ -26,7 +28,6 @@ export function transformJob(job: Ios.Job): IosJobInput {
function transformIosSecrets(secrets: {
buildCredentials?: Ios.BuildCredentials;
env?: Env;
}): IosJobSecretsInput {
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
@@ -42,6 +43,5 @@ function transformIosSecrets(secrets: {
return {
buildCredentials,
environmentSecrets: secrets.env,
};
}
+9 -3
View File
@@ -4,7 +4,6 @@ import slash from 'slash';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
@@ -26,7 +25,7 @@ export async function prepareJobAsync(
): Promise<Job> {
const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const username = getUsername(ctx.exp, ctx.user);
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
if (jobData.credentials) {
const targetNames = Object.keys(jobData.credentials);
@@ -64,8 +63,15 @@ export async function prepareJobAsync(
simulator: ctx.buildProfile.simulator,
scheme: jobData.buildScheme,
buildConfiguration: ctx.buildProfile.buildConfiguration,
artifactPath: ctx.buildProfile.artifactPath,
applicationArchivePath:
ctx.buildProfile.applicationArchivePath ?? ctx.buildProfile.artifactPath,
buildArtifactPaths: ctx.buildProfile.buildArtifactPaths,
username,
...(ctx.ios.buildNumberOverride && {
version: {
buildNumber: ctx.ios.buildNumberOverride,
},
}),
experimental: {
prebuildCommand: ctx.buildProfile.prebuildCommand,
},
@@ -1,33 +1,38 @@
import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import { IosVersionAutoIncrement } from '@expo/eas-json';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { Target } from '../../credentials/ios/types';
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { syncUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function syncProjectConfigurationAsync({
projectDir,
exp,
buildProfile,
buildSettings,
}: {
projectDir: string;
exp: ExpoConfig;
buildProfile: BuildProfile<Platform.IOS>;
buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<void> {
export async function syncProjectConfigurationAsync(
graphqlClient: ExpoGraphqlClient,
{
projectDir,
exp,
targets,
localAutoIncrement,
projectId,
}: {
projectDir: string;
exp: ExpoConfig;
targets: Target[];
localAutoIncrement?: IosVersionAutoIncrement;
projectId: string;
}
): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const { autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
await syncUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId);
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, buildSettings });
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, targets });
} else {
await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
}
+185 -43
View File
@@ -1,16 +1,26 @@
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { ExpoConfig } from '@expo/config';
import { IOSConfig, Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import chalk from 'chalk';
import path from 'path';
import type { XCBuildConfiguration } from 'xcode';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { Target } from '../../credentials/ios/types';
import { AppPlatform } from '../../graphql/generated';
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log from '../../log';
import { ora } from '../../ora';
import { findApplicationTarget } from '../../project/ios/target';
import { getNextBuildNumber, isValidBuildNumber } from '../../project/ios/versions';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import uniqBy from '../../utils/expodash/uniqBy';
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync } from '../utils/version';
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
export enum BumpStrategy {
APP_VERSION,
@@ -22,21 +32,25 @@ export async function bumpVersionAsync({
bumpStrategy,
projectDir,
exp,
buildSettings,
targets,
}: {
projectDir: string;
exp: ExpoConfig;
bumpStrategy: BumpStrategy;
buildSettings: XCBuildConfiguration['buildSettings'];
targets: Target[];
}): Promise<void> {
if (bumpStrategy === BumpStrategy.NOOP) {
return;
}
ensureStaticConfigExists(projectDir);
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
Log.log('Updated versions in app.json');
await writeVersionsToInfoPlistAsync({ projectDir, exp, infoPlist, buildSettings });
await updateNativeVersionsAsync({
projectDir,
version: exp.version,
buildNumber: exp.ios?.buildNumber,
targets,
});
Log.log('Synchronized versions with Info.plist');
}
@@ -59,10 +73,8 @@ export async function bumpVersionInAppJsonAsync({
await bumpAppVersionAsync({ appVersion, projectDir, exp });
} else {
const buildNumber = IOSConfig.Version.getBuildNumber(exp);
if (buildNumber.match(/^\d+(\.\d+)*$/)) {
const comps = buildNumber.split('.');
comps[comps.length - 1] = String(Number(comps[comps.length - 1]) + 1);
const bumpedBuildNumber = comps.join('.');
if (isValidBuildNumber(buildNumber)) {
const bumpedBuildNumber = getNextBuildNumber(buildNumber);
Log.log(
`Bumping ${chalk.bold('expo.ios.buildNumber')} from ${chalk.bold(
buildNumber
@@ -121,12 +133,21 @@ export async function readBuildNumberAsync(
export async function maybeResolveVersionsAsync(
projectDir: string,
exp: ExpoConfig,
buildSettings: XCBuildConfiguration['buildSettings']
targets: Target[]
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
const applicationTarget = findApplicationTarget(targets);
try {
return {
appBuildVersion: await readBuildNumberAsync(projectDir, exp, buildSettings),
appVersion: await readShortVersionAsync(projectDir, exp, buildSettings),
appBuildVersion: await readBuildNumberAsync(
projectDir,
exp,
applicationTarget.buildSettings ?? {}
),
appVersion: await readShortVersionAsync(
projectDir,
exp,
applicationTarget.buildSettings ?? {}
),
};
} catch (err: any) {
Log.warn('Failed to read app versions.');
@@ -137,23 +158,6 @@ export async function maybeResolveVersionsAsync(
}
}
async function writeVersionsToInfoPlistAsync({
projectDir,
exp,
infoPlist,
buildSettings,
}: {
projectDir: string;
exp: ExpoConfig;
infoPlist: IOSConfig.InfoPlist;
buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<IOSConfig.InfoPlist> {
let updatedInfoPlist = IOSConfig.Version.setVersion(exp, infoPlist);
updatedInfoPlist = IOSConfig.Version.setBuildNumber(exp, updatedInfoPlist);
await writeInfoPlistAsync({ projectDir, infoPlist: updatedInfoPlist, buildSettings });
return updatedInfoPlist;
}
export function getInfoPlistPath(
projectDir: string,
buildSettings: XCBuildConfiguration['buildSettings']
@@ -181,24 +185,55 @@ async function readInfoPlistAsync(
return ((await readPlistAsync(infoPlistPath)) ?? {}) as IOSConfig.InfoPlist;
}
async function writeInfoPlistAsync({
export async function updateNativeVersionsAsync({
projectDir,
infoPlist,
buildSettings,
version,
buildNumber,
targets,
}: {
projectDir: string;
infoPlist: IOSConfig.InfoPlist;
buildSettings: XCBuildConfiguration['buildSettings'];
version?: string;
buildNumber?: string;
targets: Target[];
}): Promise<void> {
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
await writePlistAsync(infoPlistPath, infoPlist);
const project = IOSConfig.XcodeUtils.getPbxproj(projectDir);
const iosDir = path.join(projectDir, 'ios');
const infoPlistFiles: string[] = [];
for (const target of targets) {
const { targetName, buildConfiguration } = target;
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
targetName,
buildConfiguration,
});
const infoPlist = xcBuildConfiguration?.buildSettings?.INFOPLIST_FILE;
if (infoPlist) {
const evaluatedInfoPlistPath = trimQuotes(
evaluateTemplateString(infoPlist, {
SRCROOT: iosDir,
})
);
const absolutePath = path.isAbsolute(evaluatedInfoPlistPath)
? evaluatedInfoPlistPath
: path.join(iosDir, evaluatedInfoPlistPath);
infoPlistFiles.push(path.normalize(absolutePath));
}
}
const uniqueInfoPlistPaths = uniqBy(infoPlistFiles, i => i);
for (const infoPlistPath of uniqueInfoPlistPaths) {
const infoPlist = (await readPlistAsync(infoPlistPath)) as IOSConfig.InfoPlist;
if (buildNumber) {
infoPlist.CFBundleVersion = buildNumber;
}
if (version) {
infoPlist.CFBundleShortVersionString = version;
}
await writePlistAsync(infoPlistPath, infoPlist);
}
}
function ensureStaticConfigExists(projectDir: string): void {
const paths = getConfigFilePaths(projectDir);
if (!paths.staticConfigPath) {
throw new Error('autoIncrement option is not supported when using app.config.js');
}
function trimQuotes(s: string): string {
return s?.startsWith('"') && s.endsWith('"') ? s.slice(1, -1) : s;
}
export function evaluateTemplateString(
@@ -216,3 +251,110 @@ export function evaluateTemplateString(
}
});
}
/**
* Returns buildNumber that will be used for the next build. If current build profile
* has an 'autoIncrement' option set, it increments the version on server.
*/
export async function resolveRemoteBuildNumberAsync(
graphqlClient: ExpoGraphqlClient,
{
projectDir,
projectId,
exp,
applicationTarget,
buildProfile,
}: {
projectDir: string;
projectId: string;
exp: ExpoConfig;
applicationTarget: Target;
buildProfile: BuildProfile<Platform.IOS>;
}
): Promise<string> {
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
projectId,
AppPlatform.Ios,
applicationTarget.bundleIdentifier
);
const localBuildNumber = await readBuildNumberAsync(
projectDir,
exp,
applicationTarget.buildSettings ?? {}
);
const localShortVersion = await readShortVersionAsync(
projectDir,
exp,
applicationTarget.buildSettings ?? {}
);
let currentBuildVersion: string;
if (remoteVersions?.buildVersion) {
currentBuildVersion = remoteVersions.buildVersion;
} else {
if (localBuildNumber) {
Log.warn(
'No remote versions are configured for this project, buildNumber will be initialized based on the value from the local project.'
);
currentBuildVersion = localBuildNumber;
} else {
Log.error(
`Remote versions are not configured and EAS CLI was not able to read the current version from your project. Use "eas build:version:set" to initialize remote versions.`
);
throw new Error('Remote versions are not configured.');
}
}
if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) {
return currentBuildVersion;
} else if (!buildProfile.autoIncrement && !remoteVersions?.buildVersion) {
const spinner = ora(
`Initializing buildNumber with ${chalk.bold(currentBuildVersion)}.`
).start();
try {
await AppVersionMutation.createAppVersionAsync(graphqlClient, {
appId: projectId,
platform: AppPlatform.Ios,
applicationIdentifier: applicationTarget.bundleIdentifier,
storeVersion: localShortVersion ?? '1.0.0',
buildVersion: currentBuildVersion,
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.IOS) ?? undefined,
});
spinner.succeed(`Initialized buildNumber with ${chalk.bold(currentBuildVersion)}.`);
} catch (err) {
spinner.fail(`Failed to initialize buildNumber with ${chalk.bold(currentBuildVersion)}.`);
throw err;
}
return currentBuildVersion;
} else {
const nextBuildVersion = getNextBuildNumber(currentBuildVersion);
const spinner = ora(
`Incrementing buildNumber from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
nextBuildVersion
)}.`
).start();
try {
await AppVersionMutation.createAppVersionAsync(graphqlClient, {
appId: projectId,
platform: AppPlatform.Ios,
applicationIdentifier: applicationTarget.bundleIdentifier,
storeVersion: localShortVersion ?? '1.0.0',
buildVersion: nextBuildVersion,
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.IOS) ?? undefined,
});
spinner.succeed(
`Incremented buildNumber from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
nextBuildVersion
)}.`
);
} catch (err) {
spinner.fail(
`Failed to increment buildNumber from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
nextBuildVersion
)}.`
);
throw err;
}
return nextBuildVersion;
}
}
+15 -8
View File
@@ -1,4 +1,4 @@
import { Job } from '@expo/eas-build-job';
import { Job, Metadata } from '@expo/eas-build-job';
import spawnAsync from '@expo/spawn-async';
import { ChildProcess } from 'child_process';
import semver from 'semver';
@@ -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.78';
const PLUGIN_PACKAGE_VERSION = '0.0.120';
export interface LocalBuildOptions {
enable: boolean;
@@ -18,8 +18,12 @@ export interface LocalBuildOptions {
verbose?: boolean;
}
export async function runLocalBuildAsync(job: Job, options: LocalBuildOptions): Promise<void> {
const { command, args } = await getCommandAndArgsAsync(job);
export async function runLocalBuildAsync(
job: Job,
metadata: Metadata,
options: LocalBuildOptions
): Promise<void> {
const { command, args } = await getCommandAndArgsAsync(job, metadata);
let spinner;
if (!options.verbose) {
spinner = ora().start(options.skipNativeBuild ? 'Preparing project' : 'Building project');
@@ -53,15 +57,18 @@ export async function runLocalBuildAsync(job: Job, options: LocalBuildOptions):
}
}
async function getCommandAndArgsAsync(job: Job): Promise<{ command: string; args: string[] }> {
const jobBase64 = Buffer.from(JSON.stringify({ job })).toString('base64');
async function getCommandAndArgsAsync(
job: Job,
metadata: Metadata
): Promise<{ command: string; args: string[] }> {
const jobAndMetadataBase64 = Buffer.from(JSON.stringify({ job, metadata })).toString('base64');
if (process.env.EAS_LOCAL_BUILD_PLUGIN_PATH) {
return {
command: process.env.EAS_LOCAL_BUILD_PLUGIN_PATH,
args: [jobBase64],
args: [jobAndMetadataBase64],
};
} else {
const args = [`${PLUGIN_PACKAGE_NAME}@${PLUGIN_PACKAGE_VERSION}`, jobBase64];
const args = [`${PLUGIN_PACKAGE_NAME}@${PLUGIN_PACKAGE_VERSION}`, jobAndMetadataBase64];
if (await isAtLeastNpm7Async()) {
// npx shipped with npm >= 7.0.0 requires the "-y" flag to run commands without
// prompting the user to install a package that is used for the first time
+43 -9
View File
@@ -14,7 +14,6 @@ import {
readChannelSafelyAsync as readIosChannelSafelyAsync,
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
} from '../update/ios/UpdatesModule';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs';
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
@@ -24,13 +23,14 @@ import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios
export async function collectMetadataAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<Metadata> {
const vcsClient = getVcsClient();
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
const distribution =
('simulator' in ctx.buildProfile && ctx.buildProfile.simulator
? 'simulator'
: ctx.buildProfile.distribution) ?? 'store';
const metadata = {
trackingContext: ctx.trackingCtx,
const metadata: Metadata = {
trackingContext: ctx.analyticsEventProperties,
...(await maybeResolveVersionsAsync(ctx)),
cliVersion: easCliVersion,
workflow: ctx.workflow,
@@ -43,14 +43,20 @@ export async function collectMetadataAsync<T extends Platform>(
appName: ctx.exp.name,
appIdentifier: resolveAppIdentifier(ctx),
buildProfile: ctx.buildProfileName,
gitCommitHash: await getVcsClient().getCommitHashAsync(),
isGitWorkingTreeDirty: await getVcsClient().hasUncommittedChangesAsync(),
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
gitCommitHash: await vcsClient.getCommitHashAsync(),
gitCommitMessage: truncateGitCommitMessage(
(await vcsClient.getLastCommitMessageAsync()) ?? undefined
),
isGitWorkingTreeDirty: await vcsClient.hasUncommittedChangesAsync(),
username: getUsername(ctx.exp, ctx.user),
message: ctx.message,
...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
ctx as BuildContext<Platform.IOS>
),
}),
runWithNoWaitFlag: ctx.noWait,
runFromCI: ctx.runFromCI,
};
return sanitizeMetadata(metadata);
}
@@ -60,14 +66,32 @@ async function maybeResolveVersionsAsync<T extends Platform>(
): Promise<{ appBuildVersion?: string; appVersion?: string }> {
if (ctx.platform === Platform.IOS) {
const iosContext = ctx as BuildContext<Platform.IOS>;
return await maybeResolveIosVersionsAsync(
const resolvedVersion = await maybeResolveIosVersionsAsync(
ctx.projectDir,
ctx.exp,
iosContext.ios.applicationTargetBuildSettings
iosContext.ios.targets
);
if (iosContext.ios.buildNumberOverride) {
return {
...resolvedVersion,
appBuildVersion: iosContext.ios.buildNumberOverride,
};
}
return resolvedVersion;
} else if (ctx.platform === Platform.ANDROID) {
const androidCtx = ctx as BuildContext<Platform.ANDROID>;
return await maybeResolveAndroidVersionsAsync(ctx.projectDir, ctx.exp, androidCtx.buildProfile);
const resolvedVersion = await maybeResolveAndroidVersionsAsync(
ctx.projectDir,
ctx.exp,
androidCtx.buildProfile
);
if (androidCtx.android.versionCodeOverride) {
return {
...resolvedVersion,
appBuildVersion: androidCtx.android.versionCodeOverride,
};
}
return resolvedVersion;
} else {
throw new Error(`Unsupported platform ${ctx.platform}`);
}
@@ -149,3 +173,13 @@ function resolveIosEnterpriseProvisioning(
): IosEnterpriseProvisioning | undefined {
return ctx.buildProfile.enterpriseProvisioning;
}
export function truncateGitCommitMessage(
msg: string | undefined,
maxLength: number = 4096
): string | undefined {
if (msg === undefined) {
return undefined;
}
return msg.length > maxLength ? `${msg.substring(0, maxLength - 3)}...` : msg;
}
+178
View File
@@ -0,0 +1,178 @@
import chalk from 'chalk';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { AppPlatform, BuildFilter, BuildFragment } from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log from '../log';
import { promptAsync } from '../prompts';
import { fromNow } from '../utils/date';
import { printJsonOnlyOutput } from '../utils/json';
import { paginatedQueryWithConfirmPromptAsync } from '../utils/queries';
import { formatGraphQLBuild } from './utils/formatBuild';
export const BUILDS_LIMIT = 50;
export async function listAndRenderBuildsOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
projectDisplayName,
filter,
paginatedQueryOptions,
}: {
projectId: string;
projectDisplayName: string;
filter?: BuildFilter;
paginatedQueryOptions: PaginatedQueryOptions;
}
): Promise<void> {
if (paginatedQueryOptions.nonInteractive) {
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
offset: paginatedQueryOptions.offset,
filter,
});
renderPageOfBuilds({ builds, projectDisplayName, paginatedQueryOptions });
} else {
await paginatedQueryWithConfirmPromptAsync({
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
limit,
offset,
filter,
}),
promptOptions: {
title: 'Load more builds?',
renderListItems: builds =>
renderPageOfBuilds({ builds, projectDisplayName, paginatedQueryOptions }),
},
});
}
}
function formatBuildChoiceValue(value: string | undefined | null): string {
return value ? chalk.bold(value) : chalk.dim('Unknown');
}
function formatBuildChoiceTitleAndDescription(build: BuildFragment): {
title: string;
description: string;
} {
const splitCommitMessage = build.gitCommitMessage?.split('\n');
const formattedCommitData =
build.gitCommitHash && splitCommitMessage && splitCommitMessage.length > 0
? `${chalk.dim(build.gitCommitHash.slice(0, 7))} "${chalk.bold(
splitCommitMessage[0] + (splitCommitMessage.length > 1 ? '…' : '')
)}"`
: 'Unknown';
return {
title: `ID: ${chalk.dim(build.id)} (${chalk.dim(`${fromNow(new Date(build.updatedAt))} ago`)})`,
description: [
`\tVersion: ${formatBuildChoiceValue(build.appVersion)}`,
`\t${
build.platform === AppPlatform.Ios ? 'Build number' : 'Version code'
}: ${formatBuildChoiceValue(build.appBuildVersion)}`,
`\tCommit: ${formattedCommitData}`,
].join('\n'),
};
}
export async function listAndSelectBuildsOnAppAsync(
graphqlClient: ExpoGraphqlClient,
selectedPlatform: AppPlatform,
{
projectId,
projectDisplayName,
filter,
queryOptions,
selectPromptDisabledFunction,
warningMessage,
}: {
projectId: string;
projectDisplayName: string;
filter?: BuildFilter;
queryOptions: PaginatedQueryOptions;
selectPromptDisabledFunction?: (build: BuildFragment) => boolean;
warningMessage?: string;
}
): Promise<BuildFragment> {
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
limit: queryOptions.limit ?? BUILDS_LIMIT,
offset: queryOptions.offset,
filter,
});
if (builds.length === 0) {
throw new Error('Found no builds matching the provided criteria.');
}
const { selectedSimulatorBuild } = await promptAsync({
type: 'select',
message: `Select ${selectedPlatform === AppPlatform.Ios ? 'iOS' : 'Android'} ${
selectedPlatform === AppPlatform.Ios ? 'simulator' : 'emulator'
} build to run for ${projectDisplayName} app`,
name: 'selectedSimulatorBuild',
choices: builds.map(build => {
const buildChoice = formatBuildChoiceTitleAndDescription(build);
return {
title: buildChoice.title,
description: buildChoice.description,
value: build,
disabled: selectPromptDisabledFunction?.(build),
};
}),
warn: warningMessage,
});
return selectedSimulatorBuild;
}
export async function getLatestBuildAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
filter,
}: {
projectId: string;
filter?: BuildFilter;
}
): Promise<BuildFragment> {
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
limit: 1,
offset: 0,
filter,
});
if (builds.length === 0) {
throw new Error('Found no build matching the provided criteria.');
}
return builds[0];
}
function renderPageOfBuilds({
builds,
projectDisplayName,
paginatedQueryOptions,
}: {
builds: BuildFragment[];
projectDisplayName: string;
paginatedQueryOptions: PaginatedQueryOptions;
}): void {
if (paginatedQueryOptions.json) {
printJsonOnlyOutput(builds);
} else {
const list = builds.map(build => formatGraphQLBuild(build)).join(`\n\n${chalk.dim('———')}\n\n`);
Log.addNewLineIfNone();
Log.log(chalk.bold(`Builds for ${projectDisplayName}:`));
Log.log(`\n${list}`);
}
}
+156 -11
View File
@@ -1,9 +1,27 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, SubmitProfile } from '@expo/eas-json';
import {
AppVersionSource,
BuildProfile,
EasJson,
EasJsonAccessor,
EasJsonUtils,
SubmitProfile,
} from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { AppPlatform, BuildFragment, BuildStatus, SubmissionFragment } from '../graphql/generated';
import { Analytics } from '../analytics/AnalyticsManager';
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import {
AppPlatform,
BuildFragment,
BuildResourceClass,
BuildStatus,
BuildWithSubmissionsFragment,
SubmissionFragment,
} from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import { toAppPlatform, toPlatform } from '../graphql/types/AppPlatform';
import Log from '../log';
import {
@@ -14,12 +32,20 @@ import {
} from '../platform';
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
import { validateAppVersionRuntimePolicySupportAsync } from '../project/projectUtils';
import {
validateAppConfigForRemoteVersionSource,
validateBuildProfileVersionSettings,
} from '../project/remoteVersionSource';
import { createSubmissionContextAsync } from '../submit/context';
import {
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish,
submitAsync,
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { validateBuildProfileConfigMatchesProjectConfigAsync } from '../update/utils';
import { Actor } from '../user/User';
import { printJsonOnlyOutput } from '../utils/json';
import { ProfileData, getProfilesAsync } from '../utils/profiles';
import { getVcsClient } from '../vcs';
@@ -30,6 +56,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,9 +74,55 @@ export interface BuildFlags {
autoSubmit: boolean;
submitProfile?: string;
localBuildOptions: LocalBuildOptions;
userInputResourceClass?: UserInputResourceClass;
message?: string;
}
export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFlags): Promise<void> {
const iosUserInputResourceClassToBuildResourceClassMapping: Record<
UserInputResourceClass,
BuildResourceClass
> = {
[UserInputResourceClass.DEFAULT]: BuildResourceClass.IosDefault,
[UserInputResourceClass.LARGE]: BuildResourceClass.IosLarge,
[UserInputResourceClass.M1_EXPERIMENTAL]: BuildResourceClass.IosM1Large,
};
const androidUserInputResourceClassToBuildResourceClassMapping: Record<
Exclude<UserInputResourceClass, UserInputResourceClass.M1_EXPERIMENTAL>,
BuildResourceClass
> = {
[UserInputResourceClass.DEFAULT]: BuildResourceClass.AndroidDefault,
[UserInputResourceClass.LARGE]: BuildResourceClass.AndroidLarge,
};
function resolveResourceClass(
platform: Platform,
resourceClassInput: UserInputResourceClass
): BuildResourceClass {
if (platform !== Platform.IOS && resourceClassInput === UserInputResourceClass.M1_EXPERIMENTAL) {
throw new Error(
`Resource class ${UserInputResourceClass.M1_EXPERIMENTAL} is only available for iOS builds`
);
}
return platform === Platform.ANDROID
? androidUserInputResourceClassToBuildResourceClassMapping[
resourceClassInput as Exclude<
UserInputResourceClass,
UserInputResourceClass.M1_EXPERIMENTAL
>
]
: iosUserInputResourceClassToBuildResourceClassMapping[resourceClassInput];
}
export async function runBuildAndSubmitAsync(
graphqlClient: ExpoGraphqlClient,
analytics: Analytics,
projectDir: string,
flags: BuildFlags,
actor: Actor,
getDynamicProjectConfigAsync: DynamicConfigContextFn
): Promise<void> {
await getVcsClient().ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
@@ -57,11 +130,14 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
projectDir,
nonInteractive: flags.nonInteractive,
});
const easJsonAccessor = new EasJsonAccessor(projectDir);
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
const platforms = toPlatforms(flags.requestedPlatform);
const buildProfiles = await getProfilesAsync({
type: 'build',
projectDir,
easJsonAccessor,
platforms,
profileName: flags.profile ?? undefined,
});
@@ -72,8 +148,12 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
buildProfiles,
});
for (const buildProfile of buildProfiles) {
validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig);
}
const startedBuilds: {
build: BuildFragment;
build: BuildWithSubmissionsFragment | BuildFragment;
buildProfile: ProfileData<'build'>;
}[] = [];
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
@@ -84,6 +164,15 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
flags,
moreBuilds: platforms.length > 1,
buildProfile,
resourceClass: resolveResourceClass(
buildProfile.platform,
flags.userInputResourceClass ?? UserInputResourceClass.DEFAULT
),
easJsonCliConfig,
actor,
graphqlClient,
analytics,
getDynamicProjectConfigAsync,
});
if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile });
@@ -102,7 +191,7 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) {
const submitProfiles = await getProfilesAsync({
projectDir,
easJsonAccessor,
platforms,
profileName: flags.submitProfile,
type: 'submit',
@@ -122,6 +211,10 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
submitProfile,
nonInteractive: flags.nonInteractive,
});
startedBuild.build = await BuildQuery.withSubmissionsByIdAsync(
graphqlClient,
startedBuild.build.id
);
submissions.push(submission);
}
@@ -138,20 +231,37 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
}
const { accountName } = Object.values(buildCtxByPlatform)[0];
const builds = await waitForBuildEndAsync({
const builds = await waitForBuildEndAsync(graphqlClient, {
buildIds: startedBuilds.map(({ build }) => build.id),
accountName,
});
printBuildResults(builds, flags.json);
if (!flags.json) {
printBuildResults(builds);
}
const haveAllBuildsFailedOrCanceled = builds.every(
build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status)
);
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
if (flags.json) {
printJsonOnlyOutput(builds);
}
exitWithNonZeroCodeIfSomeBuildsFailed(builds);
} else {
// the following function also exits with non zero code if any of the submissions failed
await waitForSubmissionsToCompleteAsync(submissions);
const completedSubmissions = await waitForSubmissionsToCompleteAsync(
graphqlClient,
submissions
);
if (flags.json) {
printJsonOnlyOutput(
await Promise.all(
builds
.filter((i): i is BuildWithSubmissionsFragment => !!i)
.map(build => BuildQuery.withSubmissionsByIdAsync(graphqlClient, build.id))
)
);
}
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
}
}
@@ -160,20 +270,40 @@ async function prepareAndStartBuildAsync({
flags,
moreBuilds,
buildProfile,
resourceClass,
easJsonCliConfig,
actor,
graphqlClient,
analytics,
getDynamicProjectConfigAsync,
}: {
projectDir: string;
flags: BuildFlags;
moreBuilds: boolean;
buildProfile: ProfileData<'build'>;
resourceClass: BuildResourceClass;
easJsonCliConfig: EasJson['cli'];
actor: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
getDynamicProjectConfigAsync: DynamicConfigContextFn;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName,
resourceClass,
clearCache: flags.clearCache,
buildProfile: buildProfile.profile,
nonInteractive: flags.nonInteractive,
noWait: !flags.wait,
platform: buildProfile.platform,
projectDir,
localBuildOptions: flags.localBuildOptions,
easJsonCliConfig,
message: flags.message,
actor,
graphqlClient,
analytics,
getDynamicProjectConfigAsync,
});
if (moreBuilds) {
@@ -186,6 +316,17 @@ async function prepareAndStartBuildAsync({
);
}
await validateBuildProfileConfigMatchesProjectConfigAsync(
buildCtx.exp,
buildProfile,
buildCtx.projectId,
flags.nonInteractive
);
await validateAppVersionRuntimePolicySupportAsync(buildCtx.projectDir, buildCtx.exp);
if (easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE) {
validateAppConfigForRemoteVersionSource(buildCtx.exp, buildProfile.platform);
}
if (buildCtx.workflow === Workflow.MANAGED) {
if (!sdkVersionChecked) {
await checkExpoSdkIsSupportedAsync(buildCtx);
@@ -235,13 +376,17 @@ async function prepareAndStartSubmissionAsync({
const submissionCtx = await createSubmissionContextAsync({
platform,
projectDir,
projectId: build.project.id,
profile: submitProfile,
archiveFlags: { id: build.id },
nonInteractive,
env: buildProfile.env,
credentialsCtx: buildCtx.credentialsCtx,
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
actor: buildCtx.user,
graphqlClient: buildCtx.graphqlClient,
analytics: buildCtx.analytics,
projectId: buildCtx.projectId,
exp: buildCtx.exp,
});
if (moreBuilds) {
+10
View File
@@ -12,3 +12,13 @@ export enum BuildDistributionType {
INTERNAL = 'internal',
SIMULATOR = 'simulator',
}
export enum UserInputResourceClass {
DEFAULT = 'default',
LARGE = 'large',
/**
* @experimental
* This resource class is not yet ready to be used in production. For testing purposes only. Might be deprecated / deleted at any time.
*/
M1_EXPERIMENTAL = 'm1-experimental',
}
@@ -14,7 +14,6 @@ import {
} from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
import { printJsonOnlyOutput } from '../../utils/json';
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
export function printLogsUrls(builds: BuildFragment[]): void {
@@ -32,18 +31,14 @@ export function printLogsUrls(builds: BuildFragment[]): void {
}
}
export function printBuildResults(builds: (BuildFragment | null)[], json: boolean): void {
if (json) {
printJsonOnlyOutput(builds);
export function printBuildResults(builds: (BuildFragment | null)[]): void {
Log.newLine();
if (builds.length === 1) {
const [build] = builds;
assert(build, 'Build should be defined');
printBuildResult(build);
} else {
Log.newLine();
if (builds.length === 1) {
const [build] = builds;
assert(build, 'Build should be defined');
printBuildResult(build);
} else {
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
}
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
}
}
@@ -99,14 +94,14 @@ export function printDeprecationWarnings(deprecationInfo?: EasBuildDeprecationIn
return;
}
if (deprecationInfo.type === EasBuildDeprecationInfoType.Internal) {
Log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
Log.warn('This command is using API that soon will be deprecated. Upgrade EAS CLI.');
Log.warn("Changes won't affect your project config.");
Log.warn(deprecationInfo.message);
} else if (deprecationInfo.type === EasBuildDeprecationInfoType.UserFacing) {
Log.warn('This command is using API that soon will be deprecated.');
Log.warn(deprecationInfo.message);
} else {
Log.warn('An unexpected warning was encountered. Please report it as a bug:');
Log.warn('An unexpected warning was encountered. Report it as a bug:');
Log.warn(deprecationInfo);
}
}
+10 -64
View File
@@ -1,4 +1,3 @@
import admzip from 'adm-zip';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
@@ -29,7 +28,7 @@ export async function maybeBailOnRepoStatusAsync(): Promise<void> {
});
if (!answer) {
throw new Error('Please commit all changes. Aborting...');
throw new Error('Commit all changes. Aborting...');
}
}
@@ -40,12 +39,12 @@ export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<vo
Log.addNewLineIfNone();
Log.warn(`${chalk.bold('Warning!')} Your repository working tree is dirty.`);
Log.log(
`This operation needs to be run on a clean working tree, please ${chalk.bold(
'commit all your changes before proceeding'
`This operation needs to be run on a clean working tree. ${chalk.bold(
'Commit all your changes before proceeding'
)}.`
);
if (nonInteractive) {
throw new Error('Please commit all changes. Aborting...');
throw new Error('Commit all changes. Aborting...');
}
const answer = await confirmAsync({
message: `Commit changes to git?`,
@@ -53,7 +52,7 @@ export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<vo
if (answer) {
await commitPromptAsync({ commitAllFiles: true });
} else {
throw new Error('Please commit all changes. Aborting...');
throw new Error('Commit all changes. Aborting...');
}
}
@@ -71,7 +70,11 @@ export async function commitPromptAsync({
initial: initialCommitMessage,
validate: (input: string) => input !== '',
});
await getVcsClient().commitAsync({ commitAllFiles, commitMessage: message });
await getVcsClient().commitAsync({
commitAllFiles,
commitMessage: message,
nonInteractive: false,
});
}
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
@@ -123,63 +126,6 @@ export async function makeProjectTarballAsync(): Promise<{ path: string; size: n
return { size, path: tarPath };
}
export async function makeZipAsync(pathToZip: string): Promise<{ path: string; size: number }> {
const spinner = ora('Compressing project files');
await fs.mkdirp(getTmpDirectory());
const shallowClonePath = path.join(getTmpDirectory(), `${uuidv4()}-shallow-clone`);
try {
process.chdir(pathToZip);
await getVcsClient().makeShallowCopyAsync(shallowClonePath);
process.chdir('..');
const zipPath = path.join(getTmpDirectory(), `${uuidv4()}.zip`);
// If the compression takes longer then a second, show the spinner.
// This can happen when the user has a lot of resources or doesn't ignore their CocoaPods.
// A basic project on a Mac can compress in roughly ~40ms.
// A fairly complex project without CocoaPods ignored can take up to 30s.
const timer = setTimeout(
() => {
spinner.start();
},
Log.isDebug ? 1 : 1000
);
// TODO: Possibly warn after more time about unoptimized assets.
const compressTimerLabel = 'makeProjectZipAsync';
startTimer(compressTimerLabel);
const zip = new admzip();
zip.addLocalFolder(shallowClonePath);
await zipAsync(zip, zipPath);
clearTimeout(timer);
const { size } = await fs.stat(zipPath);
const duration = endTimer(compressTimerLabel);
if (spinner.isSpinning) {
const prettyTime = formatMilliseconds(duration);
spinner.succeed(
`Compressed project files ${chalk.dim(`${prettyTime} (${formatBytes(size)})`)}`
);
}
return { size, path: zipPath };
} catch (err) {
if (spinner.isSpinning) {
spinner.fail();
}
throw err;
} finally {
await fs.remove(shallowClonePath);
}
}
async function zipAsync(zip: admzip, targetFileName?: string): Promise<void> {
return new Promise((resolve, reject) => {
zip.writeZip(targetFileName, (error: Error | null) => (error ? reject(error) : resolve()));
});
}
enum ShouldCommitChanges {
Yes,
ShowDiffFirst,
+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();
}
+137
View File
@@ -1,8 +1,11 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { Errors } from '@oclif/core';
import fs from 'fs-extra';
import path from 'path';
import semver from 'semver';
import Log, { learnMore } from '../log';
import { isPNGAsync } from '../utils/image';
import { getVcsClient } from '../vcs';
import { CommonContext } from './context';
@@ -50,3 +53,137 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
function isInsideDirectory(file: string, directory: string): boolean {
return file.startsWith(directory);
}
export async function validatePNGsForManagedProjectAsync<T extends Platform>(
ctx: CommonContext<T>
): Promise<void> {
if (ctx.workflow !== Workflow.MANAGED) {
return;
}
// don't run PNG checks on SDK 47 and newer
// see https://github.com/expo/eas-cli/pull/1477#issuecomment-1293914917
if (!ctx.exp.sdkVersion || semver.satisfies(ctx.exp.sdkVersion, '>= 47.0.0')) {
return;
}
if (ctx.platform === Platform.ANDROID) {
await validateAndroidPNGsAsync(ctx as CommonContext<Platform.ANDROID>);
}
// Validating iOS PNGs is currently disabled
// See https://github.com/expo/eas-cli/pull/1477 for context
//
// else {
// await validateIosPNGsAsync(ctx as CommonContext<Platform.IOS>);
// }
}
type ConfigPng = { configPath: string; pngPath: string | undefined };
async function validateAndroidPNGsAsync(ctx: CommonContext<Platform.ANDROID>): Promise<void> {
const pngs: ConfigPng[] = [
{
configPath: 'exp.icon',
pngPath: ctx.exp.icon,
},
{
configPath: 'exp.android.icon',
pngPath: ctx.exp.android?.icon,
},
{
configPath: 'exp.android.adaptiveIcon.foregroundImage',
pngPath: ctx.exp.android?.adaptiveIcon?.foregroundImage,
},
{
configPath: 'exp.android.adaptiveIcon.backgroundImage',
pngPath: ctx.exp.android?.adaptiveIcon?.backgroundImage,
},
{
configPath: 'exp.splash.image',
pngPath: ctx.exp.splash?.image,
},
{
configPath: 'exp.notification.icon',
pngPath: ctx.exp.notification?.icon,
},
];
await validatePNGsAsync(pngs);
}
// Validating iOS PNGs is currently disabled
// See https://github.com/expo/eas-cli/pull/1477 for context
//
// async function validateIosPNGsAsync(ctx: CommonContext<Platform.IOS>): Promise<void> {
// const pngs: ConfigPng[] = [
// {
// configPath: 'exp.icon',
// pngPath: ctx.exp.icon,
// },
// {
// configPath: 'exp.ios.icon',
// pngPath: ctx.exp.ios?.icon,
// },
// {
// configPath: 'exp.splash.image',
// pngPath: ctx.exp.splash?.image,
// },
// {
// configPath: 'exp.notification.icon',
// pngPath: ctx.exp.notification?.icon,
// },
// ];
// await validatePNGsAsync(pngs);
// const icon = ctx.exp.ios?.icon ?? ctx.exp.icon;
// if (!icon) {
// return;
// }
// const iconConfigPath = `expo${ctx.exp.ios?.icon ? '.ios' : ''}.icon`;
// try {
// await ensurePNGIsNotTransparentAsync(icon);
// } catch (err: any) {
// if (err instanceof ImageTransparencyError) {
// Log.error(
// `Your iOS app icon (${iconConfigPath}) can't have transparency if you wish to upload your app to the Apple App Store.`
// );
// Log.error(learnMore('https://expo.fyi/remove-alpha-channel', { dim: false }));
// Errors.exit(1);
// } else {
// throw err;
// }
// }
// }
async function validatePNGsAsync(configPngs: ConfigPng[]): Promise<void> {
const validationPromises = configPngs.map(configPng => validatePNGAsync(configPng));
const validationResults = await Promise.allSettled(validationPromises);
const failedValidations = validationResults.filter(
(result): result is PromiseRejectedResult => result.status === 'rejected'
);
if (failedValidations.length === 0) {
return;
}
Log.error('PNG images validation failed:');
for (const { reason } of failedValidations) {
const error: Error = reason;
Log.error(`- ${error.message}`);
}
Errors.exit(1);
}
async function validatePNGAsync({ configPath, pngPath }: ConfigPng): Promise<void> {
if (!pngPath) {
return;
}
if (!pngPath.endsWith('.png')) {
throw new Error(`"${configPath}" is not a PNG file`);
}
if (!(await isPNGAsync(pngPath))) {
throw new Error(`"${configPath}" is not valid PNG`);
}
}
+264
View File
@@ -0,0 +1,264 @@
import chalk from 'chalk';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { withErrorHandlingAsync } from '../graphql/client';
import {
CreateUpdateChannelOnAppMutation,
CreateUpdateChannelOnAppMutationVariables,
ViewBranchesOnUpdateChannelQueryVariables,
ViewUpdateChannelsOnAppQueryVariables,
} from '../graphql/generated';
import { BranchQuery, UpdateBranchOnChannelObject } from '../graphql/queries/BranchQuery';
import { ChannelQuery, UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import Log from '../log';
import formatFields from '../utils/formatFields';
import { printJsonOnlyOutput } from '../utils/json';
import {
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
import { logChannelDetails } from './utils';
export const CHANNELS_LIMIT = 25;
export async function selectChannelOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
selectionPromptTitle,
paginatedQueryOptions,
}: {
projectId: string;
selectionPromptTitle: string;
paginatedQueryOptions: PaginatedQueryOptions;
}
): Promise<UpdateChannelObject> {
if (paginatedQueryOptions.nonInteractive) {
throw new Error('Unable to select a channel in non-interactive mode.');
}
const updateChannel = await paginatedQueryWithSelectPromptAsync({
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
queryChannelsOnAppAsync(graphqlClient, { appId: projectId, limit, offset }),
promptOptions: {
title: selectionPromptTitle,
createDisplayTextForSelectionPromptListItem: updateChannel => updateChannel.name,
getIdentifierForQueryItem: updateChannel => updateChannel.id,
},
});
if (!updateChannel) {
throw new Error(`Could not find any channels for app "${projectId}"`);
}
return updateChannel;
}
export async function listAndRenderChannelsOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
paginatedQueryOptions,
}: {
projectId: string;
paginatedQueryOptions: PaginatedQueryOptions;
}
): Promise<void> {
if (paginatedQueryOptions.nonInteractive) {
const channels = await queryChannelsOnAppAsync(graphqlClient, {
appId: projectId,
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
offset: paginatedQueryOptions.offset,
});
renderPageOfChannels(channels, paginatedQueryOptions);
} else {
await paginatedQueryWithConfirmPromptAsync({
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
queryChannelsOnAppAsync(graphqlClient, { limit, offset, appId: projectId }),
promptOptions: {
title: 'Load more channels?',
renderListItems: channels => renderPageOfChannels(channels, paginatedQueryOptions),
},
});
}
}
export async function listAndRenderBranchesAndUpdatesOnChannelAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId: appId,
channelName,
paginatedQueryOptions,
}: {
projectId: string;
channelName: string;
paginatedQueryOptions: PaginatedQueryOptions;
}
): Promise<void> {
const channel = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, { appId, channelName });
renderChannelHeaderContent({ channelName: channel.name, channelId: channel.id });
if (paginatedQueryOptions.nonInteractive) {
const branches = await queryBranchesAndUpdateGroupsOnChannelAsync(graphqlClient, {
appId,
channelName,
offset: paginatedQueryOptions.offset,
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
});
renderPageOfBranchesOnChannel(channel, branches, paginatedQueryOptions);
} else {
await paginatedQueryWithConfirmPromptAsync({
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
queryBranchesAndUpdateGroupsOnChannelAsync(graphqlClient, {
channelName,
appId,
offset,
limit,
}),
promptOptions: {
title: 'Load more channels?',
renderListItems: branches =>
renderPageOfBranchesOnChannel(channel, branches, paginatedQueryOptions),
},
});
}
}
async function queryChannelsOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, offset, limit }: ViewUpdateChannelsOnAppQueryVariables
): Promise<UpdateChannelObject[]> {
return await ChannelQuery.viewUpdateChannelsOnAppAsync(graphqlClient, {
appId,
offset,
limit,
});
}
async function queryBranchesAndUpdateGroupsOnChannelAsync(
graphqlClient: ExpoGraphqlClient,
args: ViewBranchesOnUpdateChannelQueryVariables
): Promise<UpdateBranchOnChannelObject[]> {
return await BranchQuery.listBranchesOnChannelAsync(graphqlClient, args);
}
function renderPageOfChannels(
currentPage: UpdateChannelObject[],
{ json }: PaginatedQueryOptions
): void {
if (json) {
printJsonOnlyOutput({ currentPage });
} else {
for (const channel of currentPage) {
renderChannelHeaderContent({ channelName: channel.name, channelId: channel.id });
Log.addNewLineIfNone();
logChannelDetails(channel);
if (currentPage.indexOf(channel) < currentPage.length - 1) {
Log.log(`\n${chalk.dim('———')}\n`);
}
}
}
}
function renderPageOfBranchesOnChannel(
channel: UpdateChannelObject,
currentPage: UpdateBranchOnChannelObject[],
{ json }: PaginatedQueryOptions
): void {
const channelWithNewBranches = { ...channel, updateBranches: currentPage };
if (json) {
printJsonOnlyOutput({ currentPage: channelWithNewBranches });
} else {
// The channel details contain both the branch and latest update group
Log.addNewLineIfNone();
logChannelDetails(channelWithNewBranches);
}
}
function renderChannelHeaderContent({
channelName,
channelId,
}: {
channelName: string;
channelId: string;
}): void {
Log.addNewLineIfNone();
Log.log(chalk.bold('Channel:'));
Log.log(
formatFields([
{ label: 'Name', value: channelName },
{ label: 'ID', value: channelId },
])
);
Log.addNewLineIfNone();
Log.log(chalk`{bold Branches pointed at this channel and their most recent update group:}`);
}
export async function createChannelOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
branchId,
channelName,
}: {
appId: string;
branchId: string;
channelName: string;
}
): Promise<CreateUpdateChannelOnAppMutation> {
// Point the new channel at a branch with its same name.
const branchMapping = JSON.stringify({
data: [{ branchId, branchMappingLogic: 'true' }],
version: 0,
});
return await withErrorHandlingAsync(
graphqlClient
.mutation<CreateUpdateChannelOnAppMutation, CreateUpdateChannelOnAppMutationVariables>(
gql`
mutation CreateUpdateChannelOnApp($appId: ID!, $name: String!, $branchMapping: String!) {
updateChannel {
createUpdateChannelForApp(appId: $appId, name: $name, branchMapping: $branchMapping) {
id
name
branchMapping
}
}
}
`,
{
appId,
name: channelName,
branchMapping,
}
)
.toPromise()
);
}
export async function ensureChannelExistsAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, branchId, channelName }: { appId: string; branchId: string; channelName: string }
): Promise<void> {
try {
await createChannelOnAppAsync(graphqlClient, {
appId,
channelName,
branchId,
});
} catch (e: any) {
const isIgnorableError =
e.graphQLErrors?.length === 1 &&
e.graphQLErrors[0].extensions.errorCode === 'CHANNEL_ALREADY_EXISTS';
if (!isIgnorableError) {
throw e;
}
}
}
+117
View File
@@ -0,0 +1,117 @@
import assert from 'assert';
import chalk from 'chalk';
import { UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import Log from '../log';
import {
FormattedBranchDescription,
formatBranch,
getUpdateGroupDescriptionsWithBranch,
} from '../update/utils';
export type BranchMapping = {
version: number;
data: {
branchId: string;
branchMappingLogic: {
operand: number;
clientKey: string;
branchMappingOperator: string;
} & string;
}[];
};
/**
* Get the branch mapping and determine whether it is a rollout.
* Ensure that the branch mapping is properly formatted.
*/
export function getBranchMapping(branchMappingString?: string): {
branchMapping: BranchMapping;
isRollout: boolean;
rolloutPercent?: number;
} {
if (!branchMappingString) {
throw new Error('Missing branch mapping.');
}
let branchMapping: BranchMapping;
try {
branchMapping = JSON.parse(branchMappingString);
} catch {
throw new Error(`Could not parse branchMapping string into a JSON: "${branchMappingString}"`);
}
assert(branchMapping, 'Branch Mapping must be defined.');
if (branchMapping.version !== 0) {
throw new Error('Branch mapping must be version 0.');
}
const isRollout = branchMapping.data.length === 2;
const rolloutPercent = branchMapping.data[0].branchMappingLogic.operand;
switch (branchMapping.data.length) {
case 0:
break;
case 1:
if (branchMapping.data[0].branchMappingLogic !== 'true') {
throw new Error('Branch mapping logic for a single branch must be "true"');
}
break;
case 2:
if (branchMapping.data[0].branchMappingLogic.clientKey !== 'rolloutToken') {
throw new Error('Client key of initial branch mapping must be "rolloutToken"');
}
if (branchMapping.data[0].branchMappingLogic.branchMappingOperator !== 'hash_lt') {
throw new Error('Branch mapping operator of initial branch mapping must be "hash_lt"');
}
if (rolloutPercent == null) {
throw new Error('Branch mapping is missing a "rolloutPercent"');
}
if (branchMapping.data[1].branchMappingLogic !== 'true') {
throw new Error('Branch mapping logic for a the second branch of a rollout must be "true"');
}
break;
default:
throw new Error('Branch mapping data must have length less than or equal to 2.');
}
return { branchMapping, isRollout, rolloutPercent };
}
export function logChannelDetails(channel: UpdateChannelObject): void {
const { branchMapping, isRollout, rolloutPercent } = getBranchMapping(channel.branchMapping);
if (branchMapping.data.length > 2) {
throw new Error('Branch Mapping data must have length less than or equal to 2.');
}
const rolloutBranchIds = branchMapping.data.map(data => data.branchId);
const branchDescription = channel.updateBranches.flatMap(branch => {
const updateGroupWithBranchDescriptions = getUpdateGroupDescriptionsWithBranch(
branch.updateGroups
);
const isRolloutBranch = isRollout && rolloutBranchIds.includes(branch.id);
const isBaseBranch = rolloutBranchIds.length > 0 && rolloutBranchIds[0] === branch.id;
let rolloutPercentNumber: number | undefined = undefined;
if (isRolloutBranch) {
rolloutPercentNumber = isBaseBranch ? rolloutPercent! * 100 : (1 - rolloutPercent!) * 100;
}
return updateGroupWithBranchDescriptions.map(
({ branch, ...updateGroup }): FormattedBranchDescription => ({
branch,
branchRolloutPercentage: rolloutPercentNumber,
update: updateGroup,
})
);
});
if (branchDescription.length === 0) {
Log.log(chalk.dim('No branches are pointed to this channel.'));
} else {
Log.log(
branchDescription
.map(description => formatBranch(description))
.join(`\n\n${chalk.dim('———')}\n\n`)
);
}
}
+154 -45
View File
@@ -1,69 +1,178 @@
import { EasJsonReader } from '@expo/eas-json';
import { Command } from '@oclif/core';
import semver from 'semver';
import nullthrows from 'nullthrows';
import {
AnalyticsEvent,
flushAsync as flushAnalyticsAsync,
initAsync as initAnalyticsAsync,
logEvent,
} from '../analytics/rudderstackClient';
import { findProjectRootAsync } from '../project/projectUtils';
import { getUserAsync } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { setVcsClient } from '../vcs';
import GitClient from '../vcs/clients/git';
AnalyticsWithOrchestration,
CommandEvent,
createAnalyticsAsync,
} from '../analytics/AnalyticsManager';
import SessionManager from '../user/SessionManager';
import AnalyticsContextField from './context/AnalyticsContextField';
import ContextField from './context/ContextField';
import { DynamicProjectConfigContextField } from './context/DynamicProjectConfigContextField';
import LoggedInContextField from './context/LoggedInContextField';
import MaybeLoggedInContextField from './context/MaybeLoggedInContextField';
import { OptionalProjectConfigContextField } from './context/OptionalProjectConfigContextField';
import ProjectConfigContextField from './context/ProjectConfigContextField';
import ProjectDirContextField from './context/ProjectDirContextField';
import SessionManagementContextField from './context/SessionManagementContextField';
type ContextInput<
T extends {
[name: string]: any;
} = object
> = {
[P in keyof T]: ContextField<T[P]>;
};
type ContextOutput<
T extends {
[name: string]: any;
} = object
> = {
[P in keyof T]: T[P];
};
export default abstract class EasCommand extends Command {
protected static readonly ContextOptions = {
/**
* Require this command to be run when logged-in. Returns the logged-in actor and a logged-in
* graphql client in the context.
*/
LoggedIn: {
loggedIn: new LoggedInContextField(),
},
/**
* Do not require this command to be run when logged-in, but if it is get the logged-in actor and a
* maybe-logged-in graphql client.
*/
MaybeLoggedIn: {
maybeLoggedIn: new MaybeLoggedInContextField(),
},
/**
* Specify this context requirement if the command needs to mutate the user session.
*/
SessionManagment: {
sessionManager: new SessionManagementContextField(),
},
/**
* Require the project to be identified and registered on server if this command is being
* run within a project directory, null otherwise.
*/
OptionalProjectConfig: {
projectConfig: new OptionalProjectConfigContextField(),
},
/**
* Require this command to be run in a project directory. Return the project directory in the context.
*/
ProjectDir: {
projectDir: new ProjectDirContextField(),
},
/**
* Provides functions to load the project config when dynamic config options are needed (custom Env for example).
*/
DynamicProjectConfig: {
// eslint-disable-next-line async-protect/async-suffix
getDynamicProjectConfigAsync: new DynamicProjectConfigContextField(),
},
/**
* Require the project to be identified and registered on server. Returns the project config in the context.
* This also requires the user to be logged in (getProjectIdAsync requires logged in), so also expose that context.
* Exposing the loggedIn context here helps us guarantee user identification for logging purposes.
*/
ProjectConfig: {
loggedIn: new LoggedInContextField(),
projectConfig: new ProjectConfigContextField(),
},
/**
* Analytics manager. Returns the analytics manager in the context for use by the command.
*/
Analytics: {
analytics: new AnalyticsContextField(),
},
};
/**
* When user data is unavailable locally, determines if the command will
* force the user to log in
* Context allows for subclasses (commands) to declare their prerequisites in a type-safe manner.
* These declarative definitions each output a context property that is the result of the prerequisite being
* satisfied. These allow a unified common interface to be shared amongst commands in order to provide a more
* consistent CLI experience.
*
* For example, let's say a command needs the EAS project ID to make a GraphQL mutation. It should declare that
* it requires the `ProjectConfig` context, and then call `getContextAsync` to get the project ID.
*/
protected requiresAuthentication = true;
protected mustBeRunInsideProject = true;
static contextDefinition: ContextInput = {};
/**
* Execute the context in the contextDefinition to satisfy command prerequisites.
*/
protected async getContextAsync<
C extends {
[name: string]: any;
} = object
>(
commandClass: { contextDefinition: ContextInput<C> },
{ nonInteractive }: { nonInteractive: boolean }
): Promise<ContextOutput<C>> {
const contextDefinition = commandClass.contextDefinition;
// do these serially so that they don't do things like ask for login twice in parallel
const contextValuePairs = [];
for (const [contextKey, contextField] of Object.entries(contextDefinition)) {
contextValuePairs.push([
contextKey,
await contextField.getValueAsync({
nonInteractive,
sessionManager: this.sessionManager,
analytics: this.analytics,
}),
]);
}
return Object.fromEntries(contextValuePairs);
}
/**
* The user session manager. Responsible for coordinating all user session related state.
* If needed in a subclass, use the SessionManager ContextOption.
*/
private sessionManagerInternal?: SessionManager;
private get sessionManager(): SessionManager {
return nullthrows(this.sessionManagerInternal);
}
/**
* The analytics manager. Used for logging analytics.
* It is set up here to ensure a consistent set up.
*/
private analyticsInternal?: AnalyticsWithOrchestration;
private get analytics(): AnalyticsWithOrchestration {
return nullthrows(this.analyticsInternal);
}
protected abstract runAsync(): Promise<any>;
// eslint-disable-next-line async-protect/async-suffix
async run(): Promise<any> {
await initAnalyticsAsync();
if (this.mustBeRunInsideProject) {
await this.applyCliConfigAsync();
}
this.analyticsInternal = await createAnalyticsAsync();
this.sessionManagerInternal = new SessionManager(this.analytics);
if (this.requiresAuthentication) {
const { flags } = await this.parse();
const nonInteractive = (flags as any)['non-interactive'] ?? false;
await ensureLoggedInAsync({ nonInteractive });
} else {
await getUserAsync();
}
logEvent(AnalyticsEvent.ACTION, {
// this is needed for logEvent call below as it identifies the user in the analytics system
// if possible
await this.sessionManager.getUserAsync();
this.analytics.logEvent(CommandEvent.ACTION, {
// id is assigned by oclif in constructor based on the filepath:
// commands/submit === submit, commands/build/list === build:list
action: `eas ${this.id}`,
});
return this.runAsync();
}
// eslint-disable-next-line async-protect/async-suffix
async finally(err: Error): Promise<any> {
await flushAnalyticsAsync();
override async finally(err: Error): Promise<any> {
await this.analytics.flushAsync();
return super.finally(err);
}
private async applyCliConfigAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const easJsonReader = new EasJsonReader(projectDir);
const config = await easJsonReader.getCliConfigAsync();
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
throw new Error(
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
);
}
if (config?.requireCommit) {
setVcsClient(new GitClient());
}
}
}
@@ -1,18 +1,14 @@
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 { AnalyticsWithOrchestration, createAnalyticsAsync } from '../../analytics/AnalyticsManager';
import SessionManager from '../../user/SessionManager';
import EasCommand from '../EasCommand';
jest.mock('../../user/User');
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
jest.mock('../../analytics/rudderstackClient', () => {
const { AnalyticsEvent } = jest.requireActual('../../analytics/rudderstackClient');
jest.mock('../../user/SessionManager');
jest.mock('../../analytics/AnalyticsManager', () => {
const { CommandEvent } = jest.requireActual('../../analytics/AnalyticsManager');
return {
AnalyticsEvent,
logEvent: jest.fn(),
initAsync: jest.fn(),
flushAsync: jest.fn(),
CommandEvent,
createAnalyticsAsync: jest.fn(),
};
});
@@ -27,24 +23,20 @@ afterAll(() => {
process.argv = originalProcessArgv;
});
const analytics: AnalyticsWithOrchestration = {
logEvent: jest.fn((): void => {}),
setActor: jest.fn((): void => {}),
flushAsync: jest.fn(async (): Promise<void> => {}),
};
beforeEach(() => {
jest
.mocked(getUserAsync)
.mockReset()
.mockImplementation(async () => mockJester);
jest
.mocked(ensureLoggedInAsync)
.mockReset()
.mockImplementation(async () => mockJester);
jest.mocked(initAsync).mockReset();
jest.mocked(flushAsync).mockReset();
jest.mocked(logEvent).mockReset();
jest.resetAllMocks();
jest.mocked(createAnalyticsAsync).mockResolvedValue(analytics);
});
const createTestEasCommand = (authValue: boolean): typeof EasCommand => {
const createTestEasCommand = (): typeof EasCommand => {
class TestEasCommand extends EasCommand {
requiresAuthentication = authValue;
async runAsync(): Promise<void> {}
}
@@ -61,40 +53,33 @@ 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);
it('ensures the user data is read', async () => {
const TestEasCommand = createTestEasCommand();
await TestEasCommand.run();
expect(ensureLoggedInAsync).toHaveBeenCalled();
const sessionManagerSpy = jest.spyOn(SessionManager.prototype, 'getUserAsync');
expect(sessionManagerSpy).toBeCalledTimes(1);
}, 15_000);
it('ensures the user data is read from cache', async () => {
const TestEasCommand = createTestEasCommand(false);
await TestEasCommand.run();
expect(jest.mocked(getUserAsync)).toBeCalledTimes(1);
expect(jest.mocked(ensureLoggedInAsync)).not.toBeCalled();
});
it('initializes analytics', async () => {
const TestEasCommand = createTestEasCommand(true);
const TestEasCommand = createTestEasCommand();
await TestEasCommand.run();
expect(initAsync).toHaveBeenCalled();
expect(createAnalyticsAsync).toHaveBeenCalled();
});
it('flushes analytics', async () => {
const TestEasCommand = createTestEasCommand(true);
const TestEasCommand = createTestEasCommand();
await TestEasCommand.run();
expect(flushAsync).toHaveBeenCalled();
expect(analytics.flushAsync).toHaveBeenCalled();
});
it('logs events', async () => {
const TestEasCommand = createTestEasCommand(true);
const TestEasCommand = createTestEasCommand();
await TestEasCommand.run();
expect(logEvent).toHaveBeenCalledWith('action', {
expect(analytics.logEvent).toHaveBeenCalledWith('action', {
action: `eas ${TestEasCommand.id}`,
});
});
@@ -102,14 +87,14 @@ describe(EasCommand.name, () => {
describe('after exceptions', () => {
it('flushes analytics', async () => {
const TestEasCommand = createTestEasCommand(true);
const TestEasCommand = createTestEasCommand();
try {
await TestEasCommand.run().then(() => {
throw new Error('foo');
});
} catch (error) {}
} catch {}
expect(flushAsync).toHaveBeenCalled();
expect(analytics.flushAsync).toHaveBeenCalled();
});
});
});
@@ -0,0 +1,8 @@
import { Analytics } from '../../analytics/AnalyticsManager';
import ContextField, { ContextOptions } from './ContextField';
export default class AnalyticsContextField extends ContextField<Analytics> {
async getValueAsync({ analytics }: ContextOptions): Promise<Analytics> {
return analytics;
}
}
@@ -0,0 +1,12 @@
import { Analytics } from '../../analytics/AnalyticsManager';
import SessionManager from '../../user/SessionManager';
export interface ContextOptions {
sessionManager: SessionManager;
analytics: Analytics;
nonInteractive: boolean;
}
export default abstract class ContextField<T> {
abstract getValueAsync(options: ContextOptions): Promise<T>;
}
@@ -0,0 +1,34 @@
import { ExpoConfig } from '@expo/config-types';
import { ExpoConfigOptions, getExpoConfig } from '../../project/expoConfig';
import ContextField, { ContextOptions } from './ContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
export type DynamicConfigContextFn = (options?: ExpoConfigOptions) => Promise<{
projectId: string;
exp: ExpoConfig;
projectDir: string;
}>;
export class DynamicProjectConfigContextField extends ContextField<DynamicConfigContextFn> {
async getValueAsync({
nonInteractive,
sessionManager,
}: ContextOptions): Promise<DynamicConfigContextFn> {
return async (options?: ExpoConfigOptions) => {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
const expBefore = getExpoConfig(projectDir, options);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
env: options?.env,
});
const exp = getExpoConfig(projectDir, options);
return {
exp,
projectDir,
projectId,
};
};
}
}
@@ -0,0 +1,30 @@
import { Actor } from '../../user/User';
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
import FeatureGating from '../gating/FeatureGating';
import ContextField, { ContextOptions } from './ContextField';
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
export default class LoggedInContextField extends ContextField<{
actor: Actor;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
}> {
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<{
actor: Actor;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
}> {
const { actor, authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const featureGateServerValues: { [key: string]: boolean } = actor?.featureGates ?? {};
const graphqlClient = createGraphqlClient(authenticationInfo);
return {
actor,
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
graphqlClient,
};
}
}
@@ -0,0 +1,32 @@
import { Actor } from '../../user/User';
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
import FeatureGating from '../gating/FeatureGating';
import ContextField, { ContextOptions } from './ContextField';
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
export default class MaybeLoggedInContextField extends ContextField<{
actor: Actor | null;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
}> {
async getValueAsync({ sessionManager }: ContextOptions): Promise<{
actor: Actor | null;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
}> {
const authenticationInfo = {
accessToken: sessionManager.getAccessToken(),
sessionSecret: sessionManager.getSessionSecret(),
};
const graphqlClient = createGraphqlClient(authenticationInfo);
const actor = await sessionManager.getUserAsync();
const featureGateServerValues: { [key: string]: boolean } = actor?.featureGates ?? {};
return {
actor: actor ?? null,
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
graphqlClient,
};
}
}
@@ -0,0 +1,49 @@
import { ExpoConfig } from '@expo/config-types';
import { InvalidEasJsonError } from '@expo/eas-json/build/errors';
import { getExpoConfig } from '../../project/expoConfig';
import ContextField, { ContextOptions } from './ContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
export class OptionalProjectConfigContextField extends ContextField<
| {
projectId: string;
exp: ExpoConfig;
projectDir: string;
}
| undefined
> {
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<
| {
projectId: string;
exp: ExpoConfig;
projectDir: string;
}
| undefined
> {
let projectDir: string;
try {
projectDir = await findProjectDirAndVerifyProjectSetupAsync();
if (!projectDir) {
return undefined;
}
} catch (e) {
if (e instanceof InvalidEasJsonError) {
throw e;
}
return undefined;
}
const expBefore = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
});
const exp = getExpoConfig(projectDir);
return {
exp,
projectDir,
projectId,
};
}
}
@@ -0,0 +1,31 @@
import { ExpoConfig } from '@expo/config-types';
import { getExpoConfig } from '../../project/expoConfig';
import ContextField, { ContextOptions } from './ContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
export default class ProjectConfigContextField extends ContextField<{
projectId: string;
exp: ExpoConfig;
projectDir: string;
}> {
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<{
projectId: string;
exp: ExpoConfig;
projectDir: string;
}> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
const expBefore = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
});
const exp = getExpoConfig(projectDir);
return {
projectId,
exp,
projectDir,
};
}
}
@@ -0,0 +1,8 @@
import ContextField from './ContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
export default class ProjectDirContextField extends ContextField<string> {
async getValueAsync(): Promise<string> {
return await findProjectDirAndVerifyProjectSetupAsync();
}
}
@@ -0,0 +1,8 @@
import SessionManager from '../../user/SessionManager';
import ContextField, { ContextOptions } from './ContextField';
export default class SessionManagementContextField extends ContextField<SessionManager> {
async getValueAsync({ sessionManager }: ContextOptions): Promise<SessionManager> {
return sessionManager;
}
}
@@ -0,0 +1,62 @@
import { vol } from 'memfs';
import { findProjectRootAsync } from '../findProjectDirAndVerifyProjectSetupAsync';
jest.mock('@expo/config');
jest.mock('fs');
jest.mock('../../../../prompts');
beforeEach(() => {
jest.resetAllMocks();
});
describe(findProjectRootAsync, () => {
beforeEach(() => {
vol.reset();
});
it('throws if not inside the project directory', async () => {
vol.fromJSON(
{
'./README.md': '1',
},
'/app'
);
await expect(findProjectRootAsync({ cwd: '/app' })).rejects.toThrow(
'Run this command inside a project directory.'
);
});
it('defaults to process.cwd() if defaultToProcessCwd = true', async () => {
const spy = jest.spyOn(process, 'cwd').mockReturnValue('/this/is/fake/process/cwd');
try {
vol.fromJSON(
{
'./README.md': '1',
},
'/app'
);
const projectDir = await findProjectRootAsync({
cwd: '/app',
defaultToProcessCwd: true,
});
expect(projectDir).toBe('/this/is/fake/process/cwd');
} finally {
spy.mockRestore();
}
});
it('returns the root directory of the project', async () => {
vol.fromJSON(
{
'./README.md': '1',
'./package.json': '2',
'./src/index.ts': '3',
},
'/app'
);
const projectRoot = await findProjectRootAsync({ cwd: '/app/src' });
expect(projectRoot).toBe('/app');
});
});
@@ -0,0 +1,184 @@
import { getConfig, modifyConfigAsync } from '@expo/config';
import { vol } from 'memfs';
import { anything, instance, mock, when } from 'ts-mockito';
import { Role } from '../../../../graphql/generated';
import { AppQuery } from '../../../../graphql/queries/AppQuery';
import { learnMore } from '../../../../log';
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import SessionManager from '../../../../user/SessionManager';
import { findProjectRootAsync } from '../findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from '../getProjectIdAsync';
jest.mock('@expo/config');
jest.mock('fs');
jest.mock('../../../../graphql/queries/AppQuery');
jest.mock('../../contextUtils/findProjectDirAndVerifyProjectSetupAsync');
jest.mock('../../../../user/User');
jest.mock('../../../../ora', () => ({
ora: () => ({
start: () => ({ succeed: () => {}, fail: () => {} }),
}),
}));
jest.mock('../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
describe(getProjectIdAsync, () => {
let sessionManager: SessionManager;
beforeEach(() => {
const sessionManagerMock = mock<SessionManager>();
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
actor: {
__typename: 'User',
id: 'user_id',
username: 'notnotbrent',
primaryAccount: {
id: 'account_id_1',
name: 'notnotbrent',
users: [{ role: Role.Owner, actor: { id: 'user_id' } }],
},
accounts: [
{
id: 'account_id_1',
name: 'notnotbrent',
users: [{ role: Role.Owner, actor: { id: 'user_id' } }],
},
{
id: 'account_id_2',
name: 'dominik',
users: [{ role: Role.ViewOnly, actor: { id: 'user_id' } }],
},
],
isExpoAdmin: false,
featureGates: {},
},
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
});
sessionManager = instance(sessionManagerMock);
vol.fromJSON(
{
'./README.md': '1',
'./package.json': '2',
'./src/index.ts': '3',
},
'/app'
);
jest.mocked(findProjectRootAsync).mockResolvedValue('/app');
});
it('gets the project ID from app config if exists', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@notnotbrent/test',
slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).resolves.toEqual('1234');
});
it('throws when the owner is out of sync', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', owner: 'wat', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@notnotbrent/test',
slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'test', owner: 'wat', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Project identified by "extra.eas.projectId" (notnotbrent) is not owned by owner specified in the "owner" field (wat). ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
});
it('throws when the slug is out of sync', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@notnotbrent/test',
slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Slug for project identified by "extra.eas.projectId" (test) does not match the "slug" field (wat). ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
});
it('fetches the project ID when not in app config, and sets it in the config', async () => {
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'success',
config: { expo: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } } },
});
jest
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
.mockImplementation(async () => '2345');
const projectId = await getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'test' },
{
nonInteractive: false,
}
);
expect(projectId).toEqual('2345');
expect(modifyConfigAsync).toHaveBeenCalledTimes(1);
expect(modifyConfigAsync).toHaveBeenCalledWith(
'/app',
{
extra: { eas: { projectId: '2345' } },
},
{ skipSDKVersionRequirement: true }
);
});
it('throws if writing the ID back to the config fails', async () => {
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'fail',
config: null,
});
jest
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
.mockImplementation(async () => '4567');
await expect(
getProjectIdAsync(sessionManager, { name: 'test', slug: 'test' }, { nonInteractive: false })
).rejects.toThrow();
});
});
@@ -0,0 +1,64 @@
import {
AnyVariables,
Client,
OperationContext,
OperationResult,
PromisifiedSource,
TypedDocumentNode,
cacheExchange,
createClient as createUrqlClient,
dedupExchange,
fetchExchange,
} from '@urql/core';
import { retryExchange } from '@urql/exchange-retry';
import { DocumentNode } from 'graphql';
import fetch from 'node-fetch';
import { getExpoApiBaseUrl } from '../../../api';
import { httpsProxyAgent } from '../../../fetch';
export interface ExpoGraphqlClient extends Client {
query<Data = any, Variables extends AnyVariables = AnyVariables>(
query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
variables: Variables,
context: Partial<OperationContext> & { additionalTypenames: string[] }
): PromisifiedSource<OperationResult<Data, Variables>>;
}
export function createGraphqlClient(authInfo: {
accessToken: string | null;
sessionSecret: string | null;
}): ExpoGraphqlClient {
return createUrqlClient({
url: getExpoApiBaseUrl() + '/graphql',
exchanges: [
dedupExchange,
cacheExchange,
retryExchange({
maxDelayMs: 4000,
retryIf: (err, operation) => {
return !!(
err &&
!operation.context.noRetry &&
(err.networkError || err.graphQLErrors.some(e => e?.extensions?.isTransient))
);
},
}),
fetchExchange,
],
// @ts-expect-error Type 'typeof fetch' is not assignable to type '(input: RequestInfo, init?: RequestInit | undefined) => Promise<Response>'.
fetch,
fetchOptions: (): RequestInit => {
const headers: Record<string, string> = {};
if (authInfo.accessToken) {
headers.authorization = `Bearer ${authInfo.accessToken}`;
} else if (authInfo.sessionSecret) {
headers['expo-session'] = authInfo.sessionSecret;
}
return {
...(httpsProxyAgent ? { agent: httpsProxyAgent } : {}),
headers,
};
},
}) as ExpoGraphqlClient;
}
@@ -0,0 +1,118 @@
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import * as PackageManagerUtils from '@expo/package-manager';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import pkgDir from 'pkg-dir';
import semver from 'semver';
import { learnMore } from '../../../log';
import { easCliVersion } from '../../../utils/easCli';
import { getVcsClient, setVcsClient } from '../../../vcs';
import GitClient from '../../../vcs/clients/git';
async function applyCliConfigAsync(projectDir: string): Promise<void> {
const easJsonAccessor = new EasJsonAccessor(projectDir);
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
throw new Error(
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
);
}
if (config?.requireCommit) {
setVcsClient(new GitClient());
}
}
async function ensureEasCliIsNotInDependenciesAsync(projectDir: string): Promise<void> {
let printCliVersionWarning = false;
const consoleWarn = (msg?: string): void => {
if (msg) {
// eslint-disable-next-line no-console
console.warn(chalk.yellow(msg));
} else {
// eslint-disable-next-line no-console
console.warn();
}
};
if (await isEasCliInDependenciesAsync(projectDir)) {
printCliVersionWarning = true;
consoleWarn(`Found ${chalk.bold('eas-cli')} in your project dependencies.`);
}
const maybeRepoRoot = PackageManagerUtils.findWorkspaceRoot(projectDir) ?? projectDir;
if (maybeRepoRoot !== projectDir && (await isEasCliInDependenciesAsync(maybeRepoRoot))) {
printCliVersionWarning = true;
consoleWarn(`Found ${chalk.bold('eas-cli')} in your monorepo dependencies.`);
}
if (printCliVersionWarning) {
consoleWarn(
`It's recommended to use the ${chalk.bold(
'"cli.version"'
)} field in eas.json to enforce the ${chalk.bold('eas-cli')} version for your project.`
);
consoleWarn(
learnMore('https://github.com/expo/eas-cli#enforcing-eas-cli-version-for-your-project')
);
consoleWarn();
}
}
async function isEasCliInDependenciesAsync(dir: string): Promise<boolean> {
const packageJsonPath = path.join(dir, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
return (
packageJson?.dependencies?.['eas-cli'] !== undefined ||
packageJson?.devDependencies?.['eas-cli'] !== undefined
);
}
/**
* @returns the project root directory.
*
* @deprecated Should not be used outside of context functions.
*/
export async function findProjectRootAsync({
cwd,
defaultToProcessCwd = false,
}: {
cwd?: string;
defaultToProcessCwd?: boolean;
} = {}): Promise<string> {
const projectRootDir = await pkgDir(cwd);
if (!projectRootDir) {
if (!defaultToProcessCwd) {
throw new Error('Run this command inside a project directory.');
} else {
return process.cwd();
}
} else {
let vcsRoot;
try {
vcsRoot = path.normalize(await getVcsClient().getRootPathAsync());
} catch {}
if (vcsRoot && vcsRoot.startsWith(projectRootDir) && vcsRoot !== projectRootDir) {
throw new Error(
`package.json is outside of the current git repository (project root: ${projectRootDir}, git root: ${vcsRoot}.`
);
}
return projectRootDir;
}
}
/**
* Determine the project root directory and ensure some constraints about the project setup
* like CLI version and dependencies.
* @returns the project root directory
*
* @deprecated Should not be used outside of context functions.
*/
export async function findProjectDirAndVerifyProjectSetupAsync(): Promise<string> {
const projectDir = await findProjectRootAsync();
await applyCliConfigAsync(projectDir);
await ensureEasCliIsNotInDependenciesAsync(projectDir);
return projectDir;
}
@@ -0,0 +1,161 @@
import { getProjectConfigDescription, modifyConfigAsync } from '@expo/config';
import { ExpoConfig } from '@expo/config-types';
import { Env } from '@expo/eas-build-job';
import chalk from 'chalk';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import Log, { learnMore } from '../../../log';
import { ora } from '../../../ora';
import { getExpoConfig } from '../../../project/expoConfig';
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { toAppPrivacy } from '../../../project/projectUtils';
import SessionManager from '../../../user/SessionManager';
import { Actor } from '../../../user/User';
import { createGraphqlClient } from './createGraphqlClient';
import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync';
/**
* Save an EAS project ID to the appropriate field in the app config.
*
* @deprecated Should not be used outside of context functions except in the init command.
*/
export async function saveProjectIdToAppConfigAsync(
projectDir: string,
projectId: string,
options: { env?: Env } = {}
): Promise<void> {
const exp = getExpoConfig(projectDir, options);
const result = await modifyConfigAsync(
projectDir,
{
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
},
{ skipSDKVersionRequirement: true }
);
switch (result.type) {
case 'success':
break;
case 'warn': {
Log.warn();
Log.warn(
`Warning: Your project uses dynamic app configuration, and the EAS project ID can't automatically be added to it.`
);
Log.warn(
chalk.dim(
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs'
)
);
Log.warn();
Log.warn(
`To complete the setup process, set "${chalk.bold(
'extra.eas.projectId'
)}" in your ${chalk.bold(getProjectConfigDescription(projectDir))}:`
);
Log.warn();
Log.warn(chalk.bold(JSON.stringify({ expo: { extra: { eas: { projectId } } } }, null, 2)));
Log.warn();
throw new Error(result.message);
}
case 'fail':
throw new Error(result.message);
default:
throw new Error('Unexpected result type from modifyConfigAsync');
}
}
/**
* Get the EAS project ID from the app config. If the project ID is not set in the config,
* use the owner/slug to identify an EAS project on the server (asking for confirmation first),
* and attempt to save the EAS project ID to the appropriate field in the app config. If unable to
* save to the app config, throw an error.
*
* @returns the EAS project ID
*
* @deprecated Should not be used outside of context functions.
*/
export async function getProjectIdAsync(
sessionManager: SessionManager,
exp: ExpoConfig,
options: { env?: Env; nonInteractive: boolean }
): Promise<string> {
// all codepaths in this function require a logged-in user with access to the owning account
// since they either query the app via graphql or create it, which includes getting info about
// the owner
const { actor, authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive: options.nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const localProjectId = exp.extra?.eas?.projectId;
if (localProjectId) {
// check that the local project ID matches account and slug
const appForProjectId = await AppQuery.byIdAsync(graphqlClient, localProjectId);
if (exp.owner && exp.owner !== appForProjectId.ownerAccount.name) {
throw new Error(
`Project config: Project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) is not owned by owner specified in the "owner" field (${exp.owner}). ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
}
if (exp.slug && exp.slug !== appForProjectId.slug) {
throw new Error(
`Project config: Slug for project identified by "extra.eas.projectId" (${
appForProjectId.slug
}) does not match the "slug" field (${exp.slug}). ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
}
return localProjectId;
}
const projectDir = await findProjectRootAsync();
if (!projectDir) {
throw new Error('This command must be run inside a project directory.');
}
Log.warn('EAS project not configured.');
const getAccountNameForEASProjectSync = (exp: ExpoConfig, user: Actor): string => {
if (exp.owner) {
return exp.owner;
}
switch (user.__typename) {
case 'User':
return user.username;
case 'Robot':
throw new Error(
'The "owner" manifest property is required when using robot users. See: https://docs.expo.dev/versions/latest/config/app/#owner'
);
}
};
const projectId = await fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync(
graphqlClient,
{
accountName: getAccountNameForEASProjectSync(exp, actor),
projectName: exp.slug,
privacy: toAppPrivacy(exp.privacy),
},
{
nonInteractive: options.nonInteractive,
},
actor
);
const spinner = ora(`Linking local project to EAS project ${projectId}`).start();
try {
await saveProjectIdToAppConfigAsync(projectDir, projectId, options);
spinner.succeed(`Linked local project to EAS project ${projectId}`);
} catch (e: any) {
spinner.fail();
throw e;
}
return projectId;
}
@@ -0,0 +1,23 @@
import { Flags } from '@oclif/core';
export const EasNonInteractiveAndJsonFlags = {
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
dependsOn: ['non-interactive'],
}),
'non-interactive': Flags.boolean({
description: 'Run the command in non-interactive mode.',
}),
};
export const EASNonInteractiveFlag = {
'non-interactive': Flags.boolean({
description: 'Run the command in non-interactive mode.',
}),
};
export const EasJsonOnlyFlag = {
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
}),
};
@@ -0,0 +1,40 @@
import nullthrows from 'nullthrows';
import env from '../../env';
import { FeatureGateKey } from './FeatureGateKey';
function parseFeatureGateEnvVariableValue(value: string | undefined): string[] {
if (!value) {
return [];
}
return value.split(',').map(v => v.trim());
}
export default class FeatureGateEnvOverrides {
private readonly map = new Map<string, boolean>();
constructor() {
const overrideEnableGateKeys = new Set(parseFeatureGateEnvVariableValue(env.featureGateEnable));
const overrideDisableGateKeys = new Set(
parseFeatureGateEnvVariableValue(env.featureGateDisable)
);
for (const overrideEnableKey of overrideEnableGateKeys) {
if (overrideDisableGateKeys.has(overrideEnableKey)) {
continue;
}
this.map.set(overrideEnableKey, true);
}
for (const overrideDisableGateKey of overrideDisableGateKeys) {
this.map.set(overrideDisableGateKey, false);
}
}
public isOverridden(key: FeatureGateKey): boolean {
return this.map.has(key) ?? false;
}
public getOverride(key: FeatureGateKey): boolean {
return nullthrows(this.map.get(key));
}
}
@@ -0,0 +1,8 @@
export enum FeatureGateKey {
// for tests
TEST = 'test',
}
export const featureGateDefaultValueWhenNoServerValue: Record<FeatureGateKey, boolean> = {
[FeatureGateKey.TEST]: true,
};
@@ -0,0 +1,15 @@
import { FeatureGateKey } from './FeatureGateKey';
export const setOverride = (_key: FeatureGateKey, _enabled: boolean): void => {
throw new Error('Must use mocked FeatureGateTestOverrides');
};
export const removeOverride = (_key: FeatureGateKey): void => {
throw new Error('Must use mocked FeatureGateTestOverrides');
};
export const isOverridden = (_key: FeatureGateKey): boolean => false;
export const getOverride = (_key: FeatureGateKey): boolean => {
throw new Error('Must use mocked FeatureGateTestOverrides');
};
@@ -0,0 +1,69 @@
import FeatureGateEnvOverrides from './FeatureGateEnvOverrides';
import { FeatureGateKey, featureGateDefaultValueWhenNoServerValue } from './FeatureGateKey';
import * as FeatureGateTestOverrides from './FeatureGateTestOverrides';
export default class FeatureGating {
constructor(
private readonly serverValues: { [key: string]: boolean },
private readonly envOverrides: FeatureGateEnvOverrides
) {}
public isEnabled(experimentKey: FeatureGateKey): boolean {
if (process.env.NODE_ENV === 'test' && FeatureGateTestOverrides.isOverridden(experimentKey)) {
return FeatureGateTestOverrides.getOverride(experimentKey);
}
if (this.envOverrides.isOverridden(experimentKey)) {
return this.envOverrides.getOverride(experimentKey);
}
if (experimentKey in this.serverValues) {
return this.serverValues[experimentKey];
}
return featureGateDefaultValueWhenNoServerValue[experimentKey];
}
/**
* Test gate override methods
*/
public static overrideKeyForScope(
key: FeatureGateKey,
enabled: boolean,
scope: () => void
): void {
if (process.env.NODE_ENV !== 'test') {
throw new Error(`Cannot overrideKeyForScope outside of test environment`);
}
FeatureGateTestOverrides.setOverride(key, enabled);
try {
scope();
} finally {
FeatureGateTestOverrides.removeOverride(key);
}
}
public static async overrideKeyForScopeAsync(
key: FeatureGateKey,
enabled: boolean,
scope: () => Promise<void>
): Promise<void> {
if (process.env.NODE_ENV !== 'test') {
throw new Error(`Cannot overrideKeyForScopeAsync outside of test environment`);
}
FeatureGateTestOverrides.setOverride(key, enabled);
try {
await scope();
} finally {
FeatureGateTestOverrides.removeOverride(key);
}
}
public static overrideKeyForEachInTest(key: FeatureGateKey, enabled: boolean): void {
beforeEach(() => FeatureGateTestOverrides.setOverride(key, enabled));
afterEach(() => FeatureGateTestOverrides.removeOverride(key));
}
}
@@ -0,0 +1,20 @@
import { FeatureGateKey } from '../FeatureGateKey';
const map = new Map();
export const setOverride = (key: FeatureGateKey, enabled: boolean): void => {
map.set(key, enabled);
};
export const removeOverride = (key: FeatureGateKey): void => {
map.delete(key);
};
export const isOverridden = (key: FeatureGateKey): boolean => map.has(key);
export const getOverride = (key: FeatureGateKey): boolean => {
if (map.has(key)) {
return map.get(key);
}
throw new Error(`Key ${key} not overridden`);
};
@@ -0,0 +1,92 @@
import env from '../../../env';
import FeatureGateEnvOverrides from '../FeatureGateEnvOverrides';
import { FeatureGateKey } from '../FeatureGateKey';
import FeatureGating from '../FeatureGating';
jest.mock('../FeatureGateTestOverrides');
describe(FeatureGating, () => {
it('supports scoped overriding', () => {
const featureGating = new FeatureGating({}, new FeatureGateEnvOverrides());
FeatureGating.overrideKeyForScope(FeatureGateKey.TEST, false, () => {
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(false);
});
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
});
it('supports async scoped overriding', async () => {
const featureGating = new FeatureGating({}, new FeatureGateEnvOverrides());
await FeatureGating.overrideKeyForScopeAsync(FeatureGateKey.TEST, false, async () => {
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(false);
});
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
});
it('uses value from the server, falls back to defined value', () => {
const featureGating = new FeatureGating(
{ [FeatureGateKey.TEST]: true },
new FeatureGateEnvOverrides()
);
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
const featureGating2 = new FeatureGating(
{ [FeatureGateKey.TEST]: false },
new FeatureGateEnvOverrides()
);
expect(featureGating2.isEnabled(FeatureGateKey.TEST)).toBe(false);
const featureGating3 = new FeatureGating({}, new FeatureGateEnvOverrides());
expect(featureGating3.isEnabled(FeatureGateKey.TEST)).toBe(true);
});
describe('environment variable overriding', () => {
afterAll(() => {
env.featureGateEnable = undefined;
env.featureGateDisable = undefined;
});
it('supports env overrides affirmative', () => {
env.featureGateEnable = [FeatureGateKey.TEST].join(',');
env.featureGateDisable = [].join(',');
const featureGating = new FeatureGating(
{ [FeatureGateKey.TEST]: false },
new FeatureGateEnvOverrides()
);
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
});
it('supports env overrides negative', () => {
env.featureGateEnable = [].join(',');
env.featureGateDisable = [FeatureGateKey.TEST].join(',');
const featureGating = new FeatureGating(
{ [FeatureGateKey.TEST]: true },
new FeatureGateEnvOverrides()
);
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(false);
});
});
});
describe('jest scoped overriding', () => {
FeatureGating.overrideKeyForEachInTest(FeatureGateKey.TEST, false);
it('supports jest scoped overriding', () => {
const featureGating = new FeatureGating(
{ [FeatureGateKey.TEST]: true },
new FeatureGateEnvOverrides()
);
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(false);
});
});
describe('removal of jest scoped overriding', () => {
it('jest scoped overriding is only for describe scope above', () => {
const featureGating = new FeatureGating(
{ [FeatureGateKey.TEST]: true },
new FeatureGateEnvOverrides()
);
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
});
});
@@ -0,0 +1,65 @@
import { Flags } from '@oclif/core';
import { OptionFlag } from '@oclif/core/lib/interfaces';
import { EasNonInteractiveAndJsonFlags } from './flags';
export const getPaginatedQueryOptions = (
flags: Partial<
Record<keyof typeof EasPaginatedQueryFlags | keyof typeof EasNonInteractiveAndJsonFlags, any>
>
): PaginatedQueryOptions => {
return {
...('limit' in flags && { limit: flags.limit }),
offset: flags.offset ?? 0,
nonInteractive: flags['non-interactive'] ?? false,
json: flags.json ?? false,
};
};
const parseFlagInputStringAsInteger = (
input: string,
flagName: string,
lowerLimit: number,
upperLimit: number
): number => {
const inputAsNumber = Number(input);
if (isNaN(inputAsNumber)) {
throw new Error(`Unable to parse ${input} as a number`);
}
if (inputAsNumber < lowerLimit || inputAsNumber > upperLimit) {
throw new Error(`--${flagName} must be between ${lowerLimit} and ${upperLimit}`);
}
return inputAsNumber;
};
export const getLimitFlagWithCustomValues = ({
defaultTo,
limit,
}: {
defaultTo: number;
limit: number;
}): OptionFlag<number | undefined> =>
Flags.integer({
description: `The number of items to fetch each query. Defaults to ${defaultTo} and is capped at ${limit}.`,
// eslint-disable-next-line async-protect/async-suffix
parse: async input => parseFlagInputStringAsInteger(input, 'limit', 1, limit),
});
export const EasPaginatedQueryFlags = {
offset: Flags.integer({
description: 'Start queries from specified index. Use for paginating results. Defaults to 0.',
// eslint-disable-next-line async-protect/async-suffix
parse: async input =>
parseFlagInputStringAsInteger(input, 'offset', 0, Number.MAX_SAFE_INTEGER),
}),
limit: getLimitFlagWithCustomValues({ defaultTo: 50, limit: 100 }),
};
// options required to control a paginated query
// from user input
export type PaginatedQueryOptions = {
nonInteractive: boolean;
json: boolean;
limit?: number;
offset: number;
};
@@ -1,16 +1,17 @@
import EasCommand from '../../commandUtils/EasCommand';
import Log from '../../log';
import { showLoginPromptAsync } from '../../user/actions';
export default class AccountLogin extends EasCommand {
static description = 'log in with your Expo account';
static aliases = ['login'];
static override description = 'log in with your Expo account';
static override aliases = ['login'];
protected mustBeRunInsideProject = false;
protected requiresAuthentication = false;
static override contextDefinition = {
...this.ContextOptions.SessionManagment,
};
async runAsync(): Promise<void> {
await showLoginPromptAsync();
const { sessionManager } = await this.getContextAsync(AccountLogin, { nonInteractive: false });
await sessionManager.showLoginPromptAsync();
Log.log('Logged in');
}
}
@@ -1,16 +1,17 @@
import EasCommand from '../../commandUtils/EasCommand';
import Log from '../../log';
import { logoutAsync } from '../../user/User';
export default class AccountLogout extends EasCommand {
static description = 'log out';
static aliases = ['logout'];
static override description = 'log out';
static override aliases = ['logout'];
protected mustBeRunInsideProject = false;
protected requiresAuthentication = false;
static override contextDefinition = {
...this.ContextOptions.SessionManagment,
};
async runAsync(): Promise<void> {
await logoutAsync();
const { sessionManager } = await this.getContextAsync(AccountLogout, { nonInteractive: false });
await sessionManager.logoutAsync();
Log.log('Logged out');
}
}
+11 -8
View File
@@ -2,19 +2,22 @@ import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import Log from '../../log';
import { getActorDisplayName, getUserAsync } from '../../user/User';
import { getActorDisplayName } from '../../user/User';
export default class AccountView extends EasCommand {
static description = 'show the username you are logged in as';
static aliases = ['whoami'];
static override description = 'show the username you are logged in as';
static override aliases = ['whoami'];
protected mustBeRunInsideProject = false;
protected requiresAuthentication = false;
static override contextDefinition = {
...this.ContextOptions.MaybeLoggedIn,
};
async runAsync(): Promise<void> {
const user = await getUserAsync();
if (user) {
Log.log(chalk.green(getActorDisplayName(user)));
const {
maybeLoggedIn: { actor },
} = await this.getContextAsync(AccountView, { nonInteractive: true });
if (actor) {
Log.log(chalk.green(getActorDisplayName(actor)));
} else {
Log.warn('Not logged in');
process.exit(1);
Loaded 100 of 587 files, more files were not shown because too many files have changed in this diff. Show more