Compare commits

...
99 Commits
Author SHA1 Message Date
Cedric van Putten 1072b75927 v3.9.1 2023-04-10 21:50:01 +02:00
Cedric van Putten c0c9d2e7df [eas-cli] Update apple utilities to avoid the maintenance error (#1780)
* [eas-cli] Update apple utilities to avoid the maintenance error

* [eas-cli] Add changelog entry

* [eas-cli] Dont change the eas-cli version

* [eas-cli] Fix changelog entry

* [eas-cli] fix it again
2023-04-10 21:48:24 +02:00
Dominik Sokal 19b831667f Update bug_report.yml (#1777) 2023-04-07 14:16:55 +02:00
Szymon Dziedzic 09db7da23b [ENG-7968][eas-cli][eas-json] don't use defaults for cacheDefaultPaths in the EAS CLI (#1769)
* [eas-cli][eas-json] don't use cacheDefaultPaths

* update deps

* update CHANGELOG.md

* update CHANGELOG.md

* fix changelog

* fix changelog again
2023-04-06 15:25:56 +02:00
Expo CI f08dde2469 update CHANGELOG.md 2023-04-06 12:49:28 +00:00
gabrieldonadel 213b5d240c v3.9.0 2023-04-06 09:28:23 -03:00
Gabriel Donadel Dall'AgnolandDominik Sokal 16ebdce913 [eas-cli] Fix expoCommandAsync to support the new cli bin path (#1772)
* [eas-cli] Fix expoCommandAsync to support the new cli bin path

* update CHANGELOG.md

* Apply suggestions from code review

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

---------

Co-authored-by: Dominik Sokal <[email protected]>
2023-04-06 09:22:04 -03:00
Dominik Sokal 121ff7a718 validate custom build config in CLI (#1770) 2023-04-06 10:34:25 +02:00
Wojciech Kozyra 360393f647 [eas-json] Add support for running on server (#1766)
* [eas-json] Add support for running on server

* review feedback

* update CHANGELOG.md
2023-04-06 09:55:04 +02:00
Wojciech Kozyra 82d66054dc [eas-cli] Do not require commits in eas build:internal (#1760)
* [eas-cli] Do not require commits in eas build:internal

* lint fix

* alternative approach

* update CHANGELOG.md
2023-03-30 12:25:10 +02:00
Will Schurman d8849dbf8c [eas-cli] Unhide channel:rollout command and modernize (#1758) 2023-03-28 09:54:45 -07:00
Dominik Sokal 4d6b5f435b [ENG-7981] use the eas-cli npm tag for eas-cli-local-build-plugin updates (#1759)
* use the eas-cli npm tag for eas-cli-local-build-plugin updates

* update CHANGELOG.md
2023-03-28 12:06:16 +02:00
Will Schurman 8fe7dfea29 [eas-cli] Add info about code signing and roll back to embedded in update info (#1755) 2023-03-24 13:42:59 -07:00
Will Schurman 8750155c25 [eas-cli] Add support for roll back to embedded updates (#1754) 2023-03-24 13:40:52 -07:00
Expo CI a0fce1c773 update CHANGELOG.md 2023-03-16 13:32:36 +00:00
Cedric van Putten cad3dca5df v3.8.1 2023-03-16 14:09:02 +01:00
Cedric van Putten 2ef6e79395 [eas-cli] Add missing changelog entry 2023-03-16 14:03:51 +01:00
Cedric van Putten 44270814a7 [eas-cli] Add test case to ensure public config is used as update (#1745)
* [eas-cli] Add test case to ensure public config is used as update

* [eas-cli] Use the public config when creating updates
2023-03-15 23:12:47 +01:00
Radosław Krzemień 95155681db [eas-cli] [ENG-6945] Don't use capitalised base error messages (#1744)
* Capitalize command id

In case the base error message is not provided and the command id starts with lower case character it will be capitalized to unify the behaviour with other cases

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Decapitalize command id

After some discussion it was agreed to decapitalize the base error messages instead, and unify the behaviour this way

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Remove redundant decapitalisation

Seems like `oclif` guarantees the command ids to be lower case in which case the explicit decapitalisation is redundant

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* update CHANGELOG.md
2023-03-14 15:42:41 +01:00
Radosław KrzemieńandWojciech Kozyra 53fd599d0a [eas-cli] [ENG-6945] Unify error display method (#1738)
* Unify error display method

Overriden the `Command`'s `catch` method to catch all the errors thrown by the commands, log the error message there the same way as it was previously handled in `src/build/build.ts` and re-throw generic `Error` instance with predefined base error message.
To avoid duplication, old `Log.error()` calls have been removed from `src/build/build.ts`.
Also added tests to see if the `catch()` method works as intended.

* Adjust base error messages

Base error messages have been adjusted after @dsokal suggested that the error may be caused by something different from a request and displaying message about a failed request is misleading

* update CHANGELOG.md

* Add test for unclean error message

Checks if the `[GraphQL] ` prefix gets removed when logging the error message

* Update handling GraphQL errors

Post-review update to how graphQL errors are handled

* Print different message for graphQL errors

When a graphQL error is caught a different base error message is printed

* Print original error to debug

When a graphQL error is caught and the message is cleaned, an original message is printed to debug (or just the same message otherwise)

* Add custom error classes

Created some custom error classes for known server side defined errors

* Log the error, not the message

Changed the debug log to log the error object, not the error message

* Fix

Fixed testing leftover

* Change phrasing

Edited the phrasing in default messages related to EAS Build

* Update if condition

Changed the if condition after review comment

* Create error base class

Created a new base class for the errors expected in build.ts, so they can be handled separately in `catch()`

* Move EasCommand properties

Moved the class properties above the class methods

* Remove helper method

Removed the helper method after review comment

* Change const type

Updated the type of SERVER_SIDE_DEFINED_ERRORS constant

* Change default base error message

Set the default base error message to `<command_name> command failed.`

* Remove comments

Removed leftover comments

* Change optional property

Changed empty string to optional property

* Post-review tweaks

Self-explanatory

* Fix failing test

One of tests started failing after changing the coalescing operators (|| -> ??). This fixes that test

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Remove error class

Removed `GenericGraphQLError` class after @wkozyra95 comment, since atm there's no need to handle it differently than a generic `Error`

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Move `EasCommandError` base class

Moved `EasCommandError` base class after @wkozyra95 comment. The class is not specific to `Build` command and so it has been moved to `commandUtils` directory

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Make message mandatory

Made `message` parameter in `EasCommandError`s mandatory, to remove need for providing a fallback string and making the server the source of truth for the errors messages

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Update packages/eas-cli/src/commandUtils/EasCommand.ts

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

* Move `baseGraphQLErrorMessage` property

Made the property into a constant in a global scope, since atm it is not changed in any of the subclasses

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Suppress ESLint warning

ESLint erroneously complains about the constructor of `EasCommandError` to be useless, which is not true is the constructor of `Error` parent class allows for optional `message` parameter and the constructor of `EasCommandError` does not

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Add error handling tests

Added a bunch of new tests related to the `handleBuildRequestError` function

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Directly export `handleBuildRequestError`

Dropped the `testExports` convention after team discussion

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

---------

Co-authored-by: Wojciech Kozyra <[email protected]>
2023-03-14 11:26:27 +01:00
dependabot[bot] b51201d010 Bump @sideway/formula from 3.0.0 to 3.0.1 (#1742)
Bumps [@sideway/formula](https://github.com/sideway/formula) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/sideway/formula/releases)
- [Commits](https://github.com/sideway/formula/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: "@sideway/formula"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-13 12:52:53 +01:00
Expo CI f86c079733 update CHANGELOG.md 2023-03-13 11:51:39 +00:00
Szymon Dziedzic 843ec7407b v3.8.0 2023-03-13 12:44:41 +01:00
Szymon Dziedzic 537cd7872a [ENG-7709][eas-cli][eas-json] add m-medium and m-large resource classes (#1739)
* [eas-cli][eas-json] add `m-medium` and `m-large` resource classes

* add deprecation warning for m1-medium resource class

* update deprecation messages

* apply suggested changes

* update CHANGELOG.md
2023-03-13 12:33:48 +01:00
Dominik Sokal 007c40bed4 [gha] send slack notification on failed tests (#1720) 2023-02-24 16:26:23 +01:00
Dominik Sokal ba9f8d8ddd Update CHANGELOG.md 2023-02-24 09:54:39 +01:00
Expo CI e4bd933e81 update CHANGELOG.md 2023-02-24 08:27:04 +00:00
Evan Bacon a6434bf6b8 v3.7.2 2023-02-24 02:18:15 -06:00
Evan Bacon 19bad84109 [eas-cli] bump apple-utils to support hashcash for authentication (#1719)
* bump apple-utils to support hashcash for authentication

* update CHANGELOG.md
2023-02-24 02:16:06 -06:00
Expo CI 5d9f8f9d76 update CHANGELOG.md 2023-02-23 21:08:09 +00:00
Brent Vatne d9d884d211 v3.7.1 2023-02-23 12:59:33 -08:00
Brent Vatne f902bb845b Add changelog entry 2023-02-23 12:59:05 -08:00
Brent Vatne 03c76c1e0e [eas-cli] Bump apple-utils 2023-02-23 12:58:33 -08:00
Expo CI 1d0d329cdb update CHANGELOG.md 2023-02-23 10:48:34 +00:00
Dominik Sokal 7ed1efb7d6 v3.7.0 2023-02-23 11:23:55 +01:00
Dominik Sokal b7bfec0366 [ENG-7565] add basic support for custom builds (#1710)
* custom builds - wip

* upgrade @expo/eas-build-job

* [eas-json] clean up build profile schema + types

* validate if custom build config exists

* don't configure credentials for custom builds

* set buildMode in metadata

* address PR feedback

* regenerate graphql code
2023-02-23 10:16:22 +01:00
Frederick Widjaja a5308f0d37 Support variant detection for package*UniversalApk Gradle Commands (#1708)
* Support variant detection for package*UniversalApk Gradle Commands

* Update CHANGELOG.md
2023-02-23 09:31:54 +01:00
Cedric van Putten 3ad644bb9d [eas-cli] Provide more context about uploaded assets when publishing (#1488) 2023-02-22 17:39:44 -08:00
Szymon Dziedzic 233e5451a8 update changelog 2023-02-20 11:40:47 +01:00
Szymon Dziedzic ce954cdecc v3.6.1 2023-02-20 11:19:14 +01:00
Szymon Dziedzic 59bd1caf98 [ENG-7376][eas-cli] link to build phase in error message (#1699)
* [eas-cli] link to build phase in error message

* add better fallback

* use fallback for all links

* use terminalLink.isSupported

* improvements after code review

* use old fallback

* update CHANGELOG.md
2023-02-17 15:09:54 +00:00
dependabot[bot] 86877fdda3 Bump undici from 5.8.2 to 5.19.1 (#1706)
Bumps [undici](https://github.com/nodejs/undici) from 5.8.2 to 5.19.1.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v5.8.2...v5.19.1)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-17 09:00:13 +00:00
Will Schurman 9299c9b7cc [eas-cli] Clarify missing owner field error message for Robot users (#1702)
* [eas-cli] Clarify missing owner field error message for Robot users

* update CHANGELOG.md

* Address comment
2023-02-16 18:34:25 +00:00
Szymon Dziedzic 64fb2e5b3a [ci] fix release script (#1704)
* fix release script

* apply suggested changes
2023-02-16 11:19:53 +00:00
Szymon Dziedzic 91fdbac773 [eas-cli] display build message data when picking build in submit, build:run and build:resign commands (#1700)
* [eas-cli] display build message data when picking build in `submit`, `build:run` and `build:resign` commands

* display only 200 first characters of message if it exists

* print only defined values

* add '...' to message if trimmed

* use array instead of object

* update CHANGELOG.md
2023-02-16 10:36:23 +00:00
Szymon Dziedzic 507eadb914 [ci] commit release changelog update using expo bot PAT (#1697) 2023-02-15 16:09:18 +00:00
Wojciech Kozyra f1cde7a291 [eas-cli] Fix README.md (#1701) 2023-02-15 13:01:56 +00:00
Wojciech Kozyra 8b9ad47d16 [eas-cli] Validate platform for local builds earlier (#1698)
* [eas-cli] Validate platform for local builds earlier

* update CHANGELOG.md
2023-02-15 11:28:19 +00:00
Wojciech Kozyra 192984b47d [eas-cli] Disable analytics for environments behind proxy (#1696)
* [eas-cli] Disable analytics for environments behind proxy

* update CHANGELOG.md
2023-02-15 09:46:55 +00:00
Szymon Dziedzic 2c36810ce7 update changelog 2023-02-14 14:31:10 +01:00
Szymon Dziedzic bfa63f95fe v3.6.0 2023-02-14 13:32:03 +01:00
Szymon Dziedzic c414b9f975 [ENG-7389][eas-json] add Xcode 14.2 image to schema (#1693)
* add Xcode 14.2 image to schema

* update CHANGELOG.md
2023-02-14 11:21:07 +00:00
Raul Riera 6946a085cb feat: return the build message when one is available (#1691)
* Return the message when available

* Update CHANGELOG.md

* run generate-graphql-code
2023-02-13 15:56:41 +00:00
Will Schurman 317d2fcbb6 [eas-cli] Warn when project ID but no owner specified and mismatch logged in user (#1667) 2023-02-06 10:39:49 -08:00
Will Schurman dd61b4bb70 [eas-cli] Fix suggested application identifier to match owning account name (#1670) 2023-02-06 10:32:22 -08:00
Jon Samp 80fbf7cf62 Revert "[eas-cli] use appVersion instead of sdkVersion (#1588)" (#1669)
* Revert "[eas-cli] use appVersion instead of sdkVersion (#1588)"

This reverts commit 9ef8483882.

* co main changelog

* update CHANGELOG.md
2023-02-06 11:23:39 -06:00
Expo CI 26b6a17160 update CHANGELOG.md 2023-02-05 20:27:34 +00:00
Brent Vatne 7f4e2eef95 v3.5.2 2023-02-05 12:20:17 -08:00
Brent Vatne 88b683655c [eas-cli] Update subdomain to developer-mdn (#1673) 2023-02-05 12:19:31 -08:00
Will Schurman f3aa3d98d6 [eas-cli] Update test certificates (#1671) 2023-02-03 20:20:59 -08:00
dependabot[bot] f852b9329d Bump http-cache-semantics from 4.1.0 to 4.1.1 (#1668)
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-03 15:50:33 +01:00
Expo CI a2340531b0 update CHANGELOG.md 2023-02-01 12:56:13 +00:00
Szymon Dziedzic 39df3a1364 v3.5.1 2023-02-01 13:49:38 +01:00
Szymon Dziedzic 2f1d454582 [eas-cli] fix issue with the eas.json envs being not available when resolving local config (#1666)
* [eas-cli] fix issue with `eas.json` envs not available when resolving local config

* fix type definitions to prevent it from happening again

* update CHANGELOG.md
2023-02-01 13:44:32 +01:00
Expo CI e2560ea867 update CHANGELOG.md 2023-01-31 13:59:54 +00:00
Wojciech Kozyra 346d136b8b v3.5.0 2023-01-31 14:36:55 +01:00
Wojciech Kozyra dd77312510 [eas-cli] Update build metadata for internal builds (#1651) 2023-01-31 14:28:53 +01:00
Wojciech Kozyra 120a965368 [ENG-7367][eas-cli] Fix using local credentials with enterprise provisioning (#1657) 2023-01-30 12:26:22 +01:00
dependabot[bot] 050336b6fd Bump ua-parser-js from 0.7.31 to 0.7.33 (#1658)
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.31 to 0.7.33.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.31...0.7.33)

---
updated-dependencies:
- dependency-name: ua-parser-js
  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>
2023-01-27 12:02:48 +01:00
Szymon Dziedzic 7fc3e3fcdb [ENG-7351][eas-cli] set m1-medium resource class for builds which don't specify resource class for SDK 48 and RN 0.71 (#1655)
* [eas-cli] set `m1-medium` resource class for builds which don't specify resource class for SDK 48 and RN 0.71

* refactor resolveBuildResourceClassAsync function

* apply suggested changes

* drop warning for ios large resource classes

* call getDynamicProjectConfigAsync in create context function

* update CHANGELOG.md
2023-01-26 17:15:04 +01:00
Quinlan Jung 2d117e5b4a [ENG-7328][eas-cli] fix undefined branchMappingLogic (#1653)
* [eas-cli] fix undefined branchMappingLogic

* spaces

* update CHANGELOG.md
2023-01-25 11:28:25 -08:00
Szymon Dziedzic 131377bd2c fix [#1650](https://github.com/expo/eas-cli/pull/1650) changelog entry 2023-01-25 14:10:06 +01:00
Szymon Dziedzic ed61b0c41b [ENG-7094][eas-cli] move image validation to server side (#1650)
* [eas-cli] move image validation to server side

* update error log

* apply suggested changes

* update CHANGELOG.md
2023-01-24 13:54:43 +01:00
Expo CI 9f4d3bfc56 update CHANGELOG.md 2023-01-23 11:26:08 +00:00
Dominik Sokal 306e7b1d71 v3.4.1 2023-01-23 12:13:29 +01:00
Dominik Sokal a2b73844f0 upgrade eas-cli-local-build-plugin to 0.0.128 2023-01-23 12:09:33 +01:00
Dominik Sokal ebbe786cd0 fix local builds (#1649)
* fix local builds

* update CHANGELOG.md
2023-01-23 12:08:01 +01:00
Expo CI 190172f06c update CHANGELOG.md 2023-01-20 12:59:11 +00:00
Dominik Sokal 2232a5ed59 v3.4.0 2023-01-20 13:51:06 +01:00
Dominik Sokal ef3e9079e5 [ENG-7283] add a prompt to switch iOS builds to M1 if queue is long (#1642)
* add a prompt to switch iOS builds to M1 if queue is long

* update CHANGELOG.md
2023-01-20 13:46:46 +01:00
Wojciech Kozyra ec10eff1e6 [eas-cli] Print warning about EAS_NO_VCS option to stderr (#1641)
* [eas-cli] Print warning about EAS_NO_VCS option to stderr

* update CHANGELOG.md
2023-01-20 11:31:50 +01:00
Szymon Dziedzic 3e3ec87622 [ENG-7223][ci] run tests on each changelog workflow automated commit (#1640)
* [ci] add test changelog workflow

* update main changelog workflow

* delete changelog_2 workflow

* use correct command

* select last commit author as automated changelog change author
2023-01-20 11:01:12 +01:00
Wojciech Kozyra f1cf92d63a [eas-cli] Print error message sent from server (#1639) 2023-01-19 14:17:59 +01:00
Brent Vatne 7096daa116 [eas-cli] Switch to using M1 by default when running build:configure (#1637) 2023-01-18 11:35:24 -08:00
Dominik Sokal 6429e70a9a print disabled resource class labels (#1635)
* print disabled resource class labels

* update CHANGELOG.md
2023-01-18 18:26:21 +01:00
Wojciech Kozyra 083381da82 [eas-cli] Automatically promote new cli release to staging on EAS Build (#1624) 2023-01-16 10:08:46 +01:00
Dominik Sokal 1c1af2da86 remove nx and upgrade lerna (https://github.com/lerna/lerna/pull/3495) 2023-01-13 10:40:06 +01:00
Wojciech Kozyra 72ce75e1ee [eas-cli] Base implementation for running builds directly from GitHub (#1568) 2023-01-13 10:01:53 +01:00
Wojciech Kozyra ea553a1025 [eas-cli] Remove workaround for gitignore with trailing backslash (#1622)
* [eas-cli] Remove workaround (fixed upstream)

* update CHANGELOG.md
2023-01-12 15:27:23 +01:00
Expo CI 3a6bcb3f2e update CHANGELOG.md 2023-01-12 11:06:59 +00:00
Dominik Sokal 8b27255ce2 v3.3.2 2023-01-12 11:59:22 +01:00
Szymon Dziedzic 1f0ae27ea0 [eas-cli] improve handling SSOUser type (#1621)
* [eas-cli] improve handling SSOUser type

* update CHANGELOG.md
2023-01-12 11:53:45 +01:00
Dominik Sokal 62d26ca32b add nx to root deps 2023-01-12 11:22:44 +01:00
Dominik Sokal 3aebb54d11 upgrade dependencies (#1617)
* upgrade dependencies

* fixes

* fix types

* update CHANGELOG.md
2023-01-12 10:26:23 +01:00
Cedric van Putten 0bf42e4cd4 [eas-cli] Add missing key information about updates in json outputs (#1619)
* Revert "Revert "[eas-cli] Add missing key information about updates in json outputs (#1611)""

This reverts commit d20db0fff4.

* [eas-cli] Fix missing update fragment in publish mutation

* [docs] Update changelog to reflect the revert and new entry
2023-01-12 10:06:48 +01:00
Expo CI ec4ef42f3c update CHANGELOG.md 2023-01-11 22:25:41 +00:00
James Ide a70222d35d v3.3.1 2023-01-11 14:18:03 -08:00
James Ide d20db0fff4 Revert "[eas-cli] Add missing key information about updates in json outputs (#1611)"
Why
---
This reverts commit e42031eaef. A customer reported an issue with publishing updates.

How
---
Ran `git revert` and then re-generated the GraphQL schemas due to merge conflicts using `yarn generate-graphql-code`.

Test Plan
---
Automated tests. Ran `easd update --auto` in a test project.
2023-01-11 12:43:35 -08:00
Expo CI 00ef6087f8 update CHANGELOG.md 2023-01-11 16:22:58 +00:00
121 changed files with 11317 additions and 5054 deletions

No files matched your search

+1 -1
View File
@@ -37,7 +37,7 @@ body:
required: true
attributes:
label: Environment
description: Run `npx expo-env-info` and `expo doctor`, and paste the output here.
description: Run `npx expo-env-info` and `npx expo-doctor`, and paste the output here.
- type: textarea
validations:
required: false
@@ -0,0 +1,4 @@
{
"printWidth": 100,
"singleQuote": true
}
@@ -0,0 +1,53 @@
name: notify-slack-on-fail-or-recover
inputs:
channel:
required: true
type: string
slack_webhook:
required: true
type: string
runs:
using: 'composite'
steps:
- name: Get previous workflow run status
uses: actions/github-script@v6
id: run-status
with:
script: |
const script = require('.github/internal-actions/notify-slack-on-fail-or-recover/main.js');
return await script({github, context, core});
- name: Commit subject
shell: bash
run: |
subject="$(git show --format=%s --quiet | sed 's/"/\\"/g')"
echo "${{ fromJSON(steps.run-status.outputs.result).previous }}"
echo "SUBJECT=$subject" >> "$GITHUB_ENV"
- name: Send Slack Success Notification
if: fromJSON(steps.run-status.outputs.result).previous == 'failure' && fromJSON(steps.run-status.outputs.result).current == 'success'
uses: rtCamp/action-slack-notify@v2
env:
SLACK_CHANNEL: ${{ inputs.channel }}
SLACK_COLOR: good
SLACK_ICON: https://github.com/expo-bot.png?size=48
SLACK_TITLE: 'Fixed, was failing'
SLACK_MESSAGE: ${{ env.SUBJECT }}
SLACK_USERNAME: 'EAS CLI CI'
SLACK_WEBHOOK: ${{ inputs.slack_webhook }}
SLACK_FOOTER: ''
- name: Send Slack Failure Notification
if: fromJSON(steps.run-status.outputs.result).current == 'failure'
uses: rtCamp/action-slack-notify@v2
env:
SLACK_CHANNEL: ${{ inputs.channel }}
SLACK_COLOR: danger
SLACK_ICON: https://github.com/expo-bot.png?size=48
SLACK_TITLE: 'Failed'
SLACK_MESSAGE: ${{ env.SUBJECT }}
SLACK_USERNAME: 'EAS CLI CI'
SLACK_WEBHOOK: ${{ inputs.slack_webhook }}
SLACK_FOOTER: ''
@@ -0,0 +1,155 @@
function updateOctokitInstance(github) {
updateGithubRestActions(github);
addConvenienceMethods(github);
}
function addConvenienceMethods(github) {
Object.assign(github, {
// checks if a run is a failure from the perspective of sending Slack
// notifications. Works for both runs and run attempts, which should be the
// same object shapes.
// When a run has not been completed, it fetches the jobs for the workflow
// to check if the job is "failing" instead of "failed".
async isFailedRun(run) {
console.log(`checking run: ${run.id}`);
console.log(`status: ${run.status}`);
console.log(`conclusion: ${run.conclusion}`);
console.log(`run_attempt: ${run.run_attempt}`);
if (run.status === 'completed') {
console.log('completed -- checking conclusion');
return run.conclusion === 'failure' || run.conclusion === 'timed_out';
}
const owner = run.repository.owner.login;
const repo = run.repository.name;
const jobsResp = await github.rest.actions.listJobsForWorkflowRun({
owner,
repo,
run_id: run.id,
});
console.log('checking jobs');
const jobs = jobsResp.data;
for (const job of jobs.jobs) {
if (job.conclusion === 'failure' || job.conclusion === 'timed_out') {
console.log('job failed');
return true;
} else if (job.status === 'completed') {
continue;
}
for (const step of job.steps) {
if (step.conclusion === 'failure') {
console.log('job step failed');
return true;
}
}
}
return false;
},
});
}
// github-script's Octokit/REST plugins are out of date, so we have to
// add our own custom endpoint method
// see https://octokit.github.io/rest.js/v18#custom-endpoint-methods for
// documentation
function updateGithubRestActions(github) {
Object.assign(github.rest.actions, {
getWorkflowRunAttempt: github.request.defaults({
method: 'GET',
url: '/repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}',
params: {
owner: {
required: true,
type: 'string',
},
repo: {
required: true,
type: 'string',
},
run_id: {
required: true,
type: 'number',
},
attempt_number: {
required: true,
type: 'number',
},
},
}),
});
}
// Returns { current: 'failure' } | { current: 'success', previous: 'success' | 'failure' } to indicate the failure status
// of the current and previous runs
module.exports = async ({ github }) => {
const { GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_REF_NAME } = process.env;
const [owner, repo] = GITHUB_REPOSITORY.split('/');
updateOctokitInstance(github);
const workflowRunResp = await github.rest.actions.getWorkflowRun({
owner,
repo,
run_id: GITHUB_RUN_ID,
});
const currentRun = workflowRunResp.data;
const listWorkflowRunsResp = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: currentRun.workflow_id,
branch: GITHUB_REF_NAME,
});
const { workflow_runs } = listWorkflowRunsResp.data;
if (workflow_runs.length === 1 && workflow_runs[0].run_attempt === 1) {
return false;
}
const previousRuns = workflow_runs.filter(
(run) => run.status === 'completed' && run.conclusion !== 'cancelled'
);
if (await github.isFailedRun(currentRun)) {
return { current: 'failure', previous: null };
}
if (currentRun.run_attempt === 1 && previousRuns.length > 0) {
return {
current: 'success',
previous: (await github.isFailedRun(previousRuns[0])) ? 'failure' : 'success',
};
}
for (let i = currentRun.run_attempt - 1; i > 0; i--) {
const previousAttemptResp = await github.rest.actions.getWorkflowRunAttempt({
owner,
repo,
run_id: currentRun.id,
attempt_number: i,
});
const previousAttempt = previousAttemptResp.data;
if (
previousAttempt.conclusion === 'success' ||
previousAttempt.conclusion === 'failure' ||
previousAttempt.conclusion === 'timed_out'
) {
return {
current: 'success',
previous: (await github.isFailedRun(previousAttempt)) ? 'failure' : 'success',
};
}
}
if (previousRuns.length > 0) {
return {
current: 'success',
previous: (await github.isFailedRun(previousRuns[0])) ? 'failure' : 'success',
};
}
return { current: 'success', previous: 'success' };
};
@@ -0,0 +1,12 @@
{
"name": "notify-slack-on-fail-or-recover",
"version": "1.0.0",
"description": "A GitHub Action to notify Expo Slack of build failures and recovery.",
"main": "main.js",
"author": "Expo",
"license": "UNLICENSED",
"private": true,
"devDependencies": {
"prettier": "^2.5.1"
}
}
@@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
[email protected]:
version "2.5.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
+2
View File
@@ -10,6 +10,8 @@ jobs:
if: ${{ github.event.issue.pull_request && github.event.issue.user.login == github.event.comment.user.login && startsWith(github.event.comment.body, '/changelog-entry ') }}
steps:
- uses: actions/checkout@v2
with:
token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/setup-node@v2
with:
node-version: 18
+17 -7
View File
@@ -36,10 +36,10 @@ jobs:
yarn pretarball-ci
yarn oclif pack:tarballs --no-xz --targets linux-x64,linux-arm
- id: x64
run: echo "::set-output name=filename::$(ls eas-*-x64.tar.gz)"
run: echo "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- id: arm
run: echo "::set-output name=filename::$(ls eas-*-arm.tar.gz)"
run: echo "filename=$(ls eas-*-arm.tar.gz)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- name: Upload linux-x64 tarball
uses: actions/upload-release-asset@v1
@@ -77,7 +77,7 @@ jobs:
yarn pretarball-ci
yarn oclif pack:tarballs --no-xz --targets darwin-x64
- id: x64
run: echo "::set-output name=filename::$(ls eas-*-x64.tar.gz)"
run: echo "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- name: Upload darwin-x64 tarball
uses: actions/upload-release-asset@v1
@@ -105,10 +105,10 @@ jobs:
yarn oclif pack:win
working-directory: ${{ github.workspace }}/packages/eas-cli
- id: x64
run: echo "::set-output name=filename::$(ls eas-*-x64.exe)"
run: echo "filename=$(ls eas-*-x64.exe)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
- id: x86
run: echo "::set-output name=filename::$(ls eas-*-x86.exe)"
run: echo "filename=$(ls eas-*-x86.exe)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
- name: Upload x64 installer
uses: actions/upload-release-asset@v1
@@ -147,12 +147,22 @@ jobs:
run: yarn lerna publish from-package --yes --no-verify-access
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
- name: Add latest-eas-build-staging tag
run: |
VERSION=$(cat lerna.json | jq -r .version)
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
npm dist-tag add eas-cli@$VERSION latest-eas-build-staging
echo "Run \"npm dist-tag add eas-cli@$VERSION latest-eas-build\" to promote it to production."
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
release-changelog:
name: Update changelog and publish release
runs-on: ubuntu-latest
needs: publish-to-npm
steps:
- uses: actions/checkout@v2
with:
token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/setup-node@v2
with:
node-version: 18
@@ -189,9 +199,9 @@ jobs:
env:
SLACK_CHANNEL: eas-cli
SLACK_COLOR: ${{ job.status }}
SLACK_ICON: https://github.com/expo-ci.png?size=48
SLACK_ICON: https://github.com/expo-bot.png?size=48
SLACK_TITLE: 'Released eas-cli@${{ env.EAS_CLI_VERSION }}'
SLACK_MESSAGE: ${{ env.SLACK_CHANGELOG }}
SLACK_USERNAME: 'Expo CI'
SLACK_USERNAME: 'EAS CLI CI'
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_FOOTER: ''
+12
View File
@@ -39,3 +39,15 @@ jobs:
if: ${{ matrix.coverage }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
notify-slack:
runs-on: ubuntu-latest
needs: test
name: Notify Slack
if: ${{ github.ref == 'refs/heads/main' && always() }}
steps:
- uses: actions/checkout@v2
- name: Notify Slack
uses: ./.github/internal-actions/notify-slack-on-fail-or-recover
with:
channel: eas-cli
slack_webhook: ${{ secrets.SLACK_WEBHOOK }}
+162 -2
View File
@@ -8,6 +8,168 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
### 🐛 Bug fixes
- Update apple utilities to work around the maintenance error. ([#1780](https://github.com/expo/eas-cli/pull/1780) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
- Don't use defaults for the `cache.cacheDefaultPaths` build profile field. ([#1769](https://github.com/expo/eas-cli/pull/1769) by [@szdziedzic](https://github.com/szdziedzic))
## [3.9.0](https://github.com/expo/eas-cli/releases/tag/v3.9.0) - 2023-04-06
### 🎉 New features
- Add support for roll back to embedded updates. ([#1754](https://github.com/expo/eas-cli/pull/1754), [#1755](https://github.com/expo/eas-cli/pull/1755) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
- Update `expoCommandAsync` to support the new Expo CLI bin path. ([#1772](https://github.com/expo/eas-cli/pull/1772) by [@gabrieldonadel](https://github.com/gabrieldonadel))
### 🧹 Chores
- Use the `eas-cli` npm tag for checking for the local build plugin updates. ([#1759](https://github.com/expo/eas-cli/pull/1759) by [@dsokal](https://github.com/dsokal))
- Ignore `requireCommit` option in `eas build:internal`. ([#1760](https://github.com/expo/eas-cli/pull/1760) by [@wkozyra95](https://github.com/wkozyra95))
- Support parsing eas.json from a string. ([#1766](https://github.com/expo/eas-cli/pull/1766) by [@wkozyra95](https://github.com/wkozyra95))
## [3.8.1](https://github.com/expo/eas-cli/releases/tag/v3.8.1) - 2023-03-16
### 🐛 Bug fixes
- Ensure public config is used as update. ([#1745](https://github.com/expo/eas-cli/pull/1745) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
- Unify how the command errors are displayed. ([#1738](https://github.com/expo/eas-cli/pull/1738) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
- Unify the case of base error messages for command failures. ([#1744](https://github.com/expo/eas-cli/pull/1744) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
## [3.8.0](https://github.com/expo/eas-cli/releases/tag/v3.8.0) - 2023-03-13
### 🎉 New features
- Add new `m-medium` and `m-large` resource classes. ([#1739](https://github.com/expo/eas-cli/pull/1739) by [@szdziedzic](https://github.com/szdziedzic))
## [3.7.2](https://github.com/expo/eas-cli/releases/tag/v3.7.2) - 2023-02-24
### 🐛 Bug fixes
- Implement Apple's proprietary Hashcash algorithm for signing authentication requests. ([#1719](https://github.com/expo/eas-cli/pull/1719) by [@EvanBacon](https://github.com/EvanBacon))
## [3.7.1](https://github.com/expo/eas-cli/releases/tag/v3.7.1) - 2023-02-23
### 🐛 Bug fixes
- Bump `@expo/apple-utils` to fix Apple developer auth. ([03c76c1](https://github.com/expo/eas-cli/commit/03c76c1e0efb9ec55e40b45e33c80b3479f920c3) by [@brentvatne](https://github.com/brentvatne))
## [3.7.0](https://github.com/expo/eas-cli/releases/tag/v3.7.0) - 2023-02-23
### 🎉 New features
- Support variant detection for package*UniversalApk Gradle Commands. ([#1708](https://github.com/expo/eas-cli/pull/1708) by [@frw](https://github.com/frw))
## [3.6.1](https://github.com/expo/eas-cli/releases/tag/v3.6.1) - 2023-02-20
### 🐛 Bug fixes
- Disable analytics when running behind a proxy. ([#1696](https://github.com/expo/eas-cli/pull/1696) by [@wkozyra95](https://github.com/wkozyra95))
- Clarify missing owner field error message for Robot users. ([#1702](https://github.com/expo/eas-cli/pull/1702) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Validate the platform for local builds earlier. ([#1698](https://github.com/expo/eas-cli/pull/1698) by [@wkozyra95](https://github.com/wkozyra95))
- Display build message when picking build in `eas submit`, `eas build:resign`, and `eas build:run` commands. ([#1700](https://github.com/expo/eas-cli/pull/1700) by [@szdziedzic](https://github.com/szdziedzic))
- Add a link directly to a build phase logs in the EAS CLI build error message. ([#1699](https://github.com/expo/eas-cli/pull/1699) by [@szdziedzic](https://github.com/szdziedzic))
## [3.6.0](https://github.com/expo/eas-cli/releases/tag/v3.6.0) - 2023-02-14
### 🎉 New features
- Use `sdkVersion` as default runtime version policy when running `eas update:configure`. ([#1669](https://github.com/expo/eas-cli/pull/1669) by [@jonsamp](https://github.com/jonsamp))
- Warn when project ID but no owner specified and mismatch logged in user. ([#1667](https://github.com/expo/eas-cli/pull/1667) by [@wschurman](https://github.com/wschurman))
- Return the build `message` if one is available. ([#1691](https://github.com/expo/eas-cli/pull/1691) by [@raulriera](https://github.com/raulriera))
### 🐛 Bug fixes
- Fix suggested application identifier to match owning account name. ([#1670](https://github.com/expo/eas-cli/pull/1670) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Add Xcode 14.2 image to the VSCode schema. ([#1693](https://github.com/expo/eas-cli/pull/1693) by [@szdziedzic](https://github.com/szdziedzic))
## [3.5.2](https://github.com/expo/eas-cli/releases/tag/v3.5.2) - 2023-02-05
### 🐛 Bug fixes
- Use the new developer-mdn.apple.com subdomain instead of developer.apple.com. ([#1673](https://github.com/expo/eas-cli/pull/1673) by [@brentvatne](https://github.com/brentvatne))
## [3.5.1](https://github.com/expo/eas-cli/releases/tag/v3.5.1) - 2023-02-01
### 🐛 Bug fixes
- Fix the issue with the `eas.json` envs being not available when resolving dynamic config. ([#1666](https://github.com/expo/eas-cli/pull/1666) by [@szdziedzic](https://github.com/szdziedzic))
## [3.5.0](https://github.com/expo/eas-cli/releases/tag/v3.5.0) - 2023-01-31
### 🎉 New features
- Set `m1-medium` resource class for SDK version `>=48` and RN version `>=0.71` builds with unspecified resource class. ([#1655](https://github.com/expo/eas-cli/pull/1655) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Fix undefined branchMappingLogic. ([#1653](https://github.com/expo/eas-cli/pull/1653) by [@quinlanj](https://github.com/quinlanj))
- Fix using local credentials for internal distribution builds with universal provisioning. ([#1657](https://github.com/expo/eas-cli/pull/1657) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Move image validation to the server side and better handle server validation errors in the eas-cli. ([#1650](https://github.com/expo/eas-cli/pull/1650) by [@szdziedzic](https://github.com/szdziedzic))
- Update metadata on the worker when using git-based builds. ([#1651](https://github.com/expo/eas-cli/pull/1651) by [@wkozyra95](https://github.com/wkozyra95))
## [3.4.1](https://github.com/expo/eas-cli/releases/tag/v3.4.1) - 2023-01-23
### 🐛 Bug fixes
- Fix incorrect exit code for successful local builds. ([#1649](https://github.com/expo/eas-cli/pull/1649) by [@dsokal](https://github.com/dsokal))
## [3.4.0](https://github.com/expo/eas-cli/releases/tag/v3.4.0) - 2023-01-20
### 🎉 New features
- Add `--json` flag to `eas config` command. ([#1568](https://github.com/expo/eas-cli/pull/1568) by [@wkozyra95](https://github.com/wkozyra95))
- Add M1 resource class configuration to default eas.json when running eas build:configure on a new project. ([#1637](https://github.com/expo/eas-cli/pull/1637) by [@brentvatne](https://github.com/brentvatne))
- Add prompt to switch iOS builds to M1 if the build queue is long. ([#1642](https://github.com/expo/eas-cli/pull/1642) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix running commands with `--json` flag when EAS_NO_VCS is set. ([#1641](https://github.com/expo/eas-cli/pull/1641) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Remove unnecessary workaround for trailing backslash in .gitignore. ([#1622](https://github.com/expo/eas-cli/pull/1622) by [@wkozyra95](https://github.com/wkozyra95))
- Internal command that will be run on EAS worker when building from GitHub. ([#1568](https://github.com/expo/eas-cli/pull/1568) by [@wkozyra95](https://github.com/wkozyra95))
- Make the disabled tier error message more descriptive. ([#1635](https://github.com/expo/eas-cli/pull/1635) by [@dsokal](https://github.com/dsokal))
- Control some of the EAS Build error messages server-side. ([#1639](https://github.com/expo/eas-cli/pull/1639) by [@wkozyra95](https://github.com/wkozyra95))
## [3.3.2](https://github.com/expo/eas-cli/releases/tag/v3.3.2) - 2023-01-12
### 🐛 Bug fixes
- Add missing key information about updates in `eas update --json` and `eas update:view --json`. ([#1619](https://github.com/expo/eas-cli/pull/1619) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
- Upgrade dependencies. ([#1617](https://github.com/expo/eas-cli/pull/1617) by [@dsokal](https://github.com/dsokal))
- Improve handling `SSOUser` type. ([#1621](https://github.com/expo/eas-cli/pull/1621) by [@szdziedzic](https://github.com/szdziedzic))
## [3.3.1](https://github.com/expo/eas-cli/releases/tag/v3.3.1) - 2023-01-11
### 🐛 Bug fixes
- Revert missing key information about updates in `eas update --json` and `eas update:view --json`. ([d20db0f](https://github.com/expo/eas-cli/commit/d20db0fff42a6f4512fea426a66cb6f168e37f7c) by [@ide](https://github.com/ide))
## [3.3.0](https://github.com/expo/eas-cli/releases/tag/v3.3.0) - 2023-01-11
### 🎉 New features
- Add ability to select resource class for a build in build profile. ([#1609](https://github.com/expo/eas-cli/pull/1609) by [@dsokal](https://github.com/dsokal))
- Deprecate `--resource-class` flag. ([#1615](https://github.com/expo/eas-cli/pull/1615) by [@dsokal](https://github.com/dsokal))
- Add new resource classes and update already available. ([#1616](https://github.com/expo/eas-cli/pull/1616) by [@szdziedzic](https://github.com/szdziedzic))
@@ -16,8 +178,6 @@ This is the log of notable changes to EAS CLI and related packages.
- Add missing key information about updates in `eas update --json` and `eas update:view --json`. ([#1611](https://github.com/expo/eas-cli/pull/1611) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
## [3.2.1](https://github.com/expo/eas-cli/releases/tag/v3.2.1) - 2023-01-09
### 🐛 Bug fixes
+4
View File
@@ -12,3 +12,7 @@ The algorithm works as follows:
- If there are any entries in the "🛠 Breaking changes" section, bump the MAJOR version.
- Otherwise, if there are any entries in the "🎉 New features", bump the MINOR version.
- Otherwise, bump the PATCH version.
## Updating EAS CLI version on EAS Build
The release process promotes the new version to staging. After verifying it works there, you can promote it to production with `npm dist-tag add eas-cli@{version} latest-eas-build`.
+2 -1
View File
@@ -1,5 +1,6 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "3.3.0"
"version": "3.9.1",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+13 -12
View File
@@ -1,4 +1,5 @@
{
"name": "eas-cli-root",
"private": true,
"workspaces": [
"packages/*"
@@ -13,24 +14,24 @@
"eas": "packages/eas-cli/bin/run",
"lint": "eslint .",
"lint-changelog": "./scripts/bin/run lint-changelog",
"release": "./scripts/bin/run release",
"release": "lerna run rebuild && ./scripts/bin/run release",
"test": "jest",
"clean": "lerna run clean && rm -rf node_modules coverage"
},
"devDependencies": {
"@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",
"@types/jest": "29.2.5",
"@types/node": "18.11.18",
"eslint": "8.31.0",
"eslint-config-universe": "11.1.1",
"eslint-plugin-async-protect": "3.0.0",
"eslint-plugin-node": "11.1.0",
"jest": "29.2.2",
"jest-watch-typeahead": "2.2.0",
"lerna": "6.0.1",
"oclif": "3.2.22",
"prettier": "2.7.1",
"jest": "29.3.1",
"jest-watch-typeahead": "2.2.1",
"lerna": "6.4.1",
"oclif": "3.4.3",
"prettier": "2.8.2",
"ts-jest": "29.0.3",
"typescript": "4.8.4"
"typescript": "4.9.4"
},
"volta": {
"node": "18.6.0",
+84 -107
View File
@@ -79,6 +79,7 @@ eas --help COMMAND
* [`eas channel:create [NAME]`](#eas-channelcreate-name)
* [`eas channel:edit [NAME]`](#eas-channeledit-name)
* [`eas channel:list`](#eas-channellist)
* [`eas channel:rollout [CHANNEL]`](#eas-channelrollout-channel)
* [`eas channel:view [NAME]`](#eas-channelview-name)
* [`eas config`](#eas-config)
* [`eas credentials`](#eas-credentials)
@@ -97,8 +98,6 @@ eas --help COMMAND
* [`eas open`](#eas-open)
* [`eas project:info`](#eas-projectinfo)
* [`eas project:init`](#eas-projectinit)
* [`eas run`](#eas-run)
* [`eas run:run`](#eas-runrun)
* [`eas secret:create`](#eas-secretcreate)
* [`eas secret:delete`](#eas-secretdelete)
* [`eas secret:list`](#eas-secretlist)
@@ -132,7 +131,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -149,7 +148,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -166,7 +165,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -180,7 +179,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -209,7 +208,7 @@ EXAMPLES
$ eas autocomplete --refresh-cache
```
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v1.3.4/src/commands/autocomplete/index.ts)_
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v1.3.10/src/commands/autocomplete/index.ts)_
## `eas branch:create [NAME]`
@@ -230,7 +229,7 @@ DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -251,7 +250,7 @@ DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -271,7 +270,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -291,7 +290,7 @@ DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -314,7 +313,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -344,7 +343,7 @@ DESCRIPTION
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -361,7 +360,7 @@ DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -378,7 +377,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:inspect`
@@ -413,7 +412,7 @@ DESCRIPTION
inspect the state of the project at specific build stages, useful for troubleshooting
```
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -450,7 +449,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -476,7 +475,7 @@ DESCRIPTION
re-sign a build archive
```
_See code: [src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/build/resign.ts)_
_See code: [src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -500,7 +499,7 @@ DESCRIPTION
run simulator/emulator builds from eas-cli
```
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/build/run.ts)_
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -547,7 +546,7 @@ DESCRIPTION
Update version of an app.
```
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/build/version/set.ts)_
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -566,7 +565,7 @@ DESCRIPTION
Update a version in native code with a value stored on EAS servers
```
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/build/version/sync.ts)_
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -583,7 +582,7 @@ DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -604,7 +603,7 @@ DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -626,7 +625,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -646,7 +645,31 @@ DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:rollout [CHANNEL]`
Roll a new branch out on a channel incrementally.
```
USAGE
$ eas channel:rollout [CHANNEL] [--branch <value>] [--percent <value>] [--end] [--json --non-interactive]
ARGUMENTS
CHANNEL channel on which the rollout should be done
FLAGS
--branch=<value> branch to rollout
--end end the rollout
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--percent=<value> percent of users to send to the new branch
DESCRIPTION
Roll a new branch out on a channel incrementally.
```
_See code: [src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -669,7 +692,7 @@ DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -677,18 +700,20 @@ display project configuration (app.json + eas.json)
```
USAGE
$ eas config [-p android|ios] [-e <value>]
$ eas config [-p android|ios] [-e <value>] [--json --non-interactive]
FLAGS
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined in
eas.json.
-p, --platform=(android|ios)
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -705,7 +730,7 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -719,7 +744,7 @@ DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -739,7 +764,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -760,7 +785,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -774,7 +799,7 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -788,7 +813,7 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -808,7 +833,7 @@ DESCRIPTION
display help for eas-cli
```
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.1.16/src/commands/help.ts)_
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.1.22/src/commands/help.ts)_
## `eas init`
@@ -876,7 +901,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/metadata/lint.ts)_
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -893,7 +918,7 @@ DESCRIPTION
generate the local store configuration from the app stores
```
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -910,7 +935,7 @@ DESCRIPTION
sync the local store configuration to the app stores
```
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas open`
@@ -924,7 +949,7 @@ DESCRIPTION
open the project page in a web browser
```
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/open.ts)_
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -938,7 +963,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -960,55 +985,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/project/init.ts)_
## `eas run`
run simulator build
```
USAGE
$ eas run [--latest | --id <value> | --path <value> | --url <value>] [-p android|ios|all] [--offset
<value>] [--limit <value>]
FLAGS
-p, --platform=(android|ios|all)
--id=<value> ID of the simulator build to run
--latest Run the latest simulator build for specified platform
--limit=<value> The number of items to fetch each query. Defaults to 50 and is capped at 100.
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
--path=<value> Path to the simulator build file file
--url=<value> Simulator build archive url
DESCRIPTION
run simulator build
```
_See code: [src/commands/run/index.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/run/index.ts)_
## `eas run:run`
run simulator build
```
USAGE
$ eas run:run [--latest | --id <value> | --path <value> | --url <value>] [-p android|ios|all] [--offset
<value>] [--limit <value>]
FLAGS
-p, --platform=(android|ios|all)
--id=<value> ID of the simulator build to run
--latest Run the latest simulator build for specified platform
--limit=<value> The number of items to fetch each query. Defaults to 50 and is capped at 100.
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
--path=<value> Path to the simulator build file file
--url=<value> Simulator build archive url
DESCRIPTION
run simulator build
```
_See code: [src/commands/run/run.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/run/run.ts)_
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/project/init.ts)_
## `eas secret:create`
@@ -1031,7 +1008,7 @@ DESCRIPTION
create an environment secret on the current project or owner account
```
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -1049,7 +1026,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -1063,7 +1040,7 @@ DESCRIPTION
list environment secrets available for your current app
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/secret/list.ts)_
## `eas secret:push`
@@ -1083,7 +1060,7 @@ DESCRIPTION
read environment secrets from env file and store on the server
```
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/secret/push.ts)_
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/secret/push.ts)_
## `eas submit`
@@ -1113,7 +1090,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1145,7 +1122,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/update/index.ts)_
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1163,7 +1140,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1184,7 +1161,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:list`
@@ -1206,11 +1183,11 @@ DESCRIPTION
view the recent updates
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/update/list.ts)_
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
rollback to an existing update
roll back to an existing update
```
USAGE
@@ -1227,10 +1204,10 @@ FLAGS
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
rollback to an existing update
roll back to an existing update
```
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/update/republish.ts)_
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:view GROUPID`
@@ -1250,7 +1227,7 @@ DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/update/view.ts)_
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1271,7 +1248,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1291,7 +1268,7 @@ DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1309,7 +1286,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1331,7 +1308,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1348,7 +1325,7 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v3.3.0/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v3.9.1/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
+5076 -2616
View File
File diff suppressed because it is too large. Load diff
+37 -35
View File
@@ -1,39 +1,40 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "3.3.0",
"version": "3.9.1",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.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": "3.3.0",
"@expo/json-file": "8.2.36",
"@expo/apple-utils": "1.2.0",
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "7.0.3",
"@expo/config-plugins": "5.0.4",
"@expo/config-types": "47.0.0",
"@expo/eas-build-job": "1.0.7",
"@expo/eas-json": "3.9.1",
"@expo/json-file": "8.2.37",
"@expo/multipart-body-parser": "1.1.0",
"@expo/osascript": "2.0.33",
"@expo/package-manager": "0.0.56",
"@expo/package-manager": "0.0.57",
"@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/plist": "0.0.20",
"@expo/plugin-help": "5.1.22",
"@expo/plugin-warn-if-update-available": "2.5.0",
"@expo/prebuild-config": "5.0.7",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.0",
"@expo/steps": "1.0.8",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "1.19.2",
"@oclif/plugin-autocomplete": "1.3.4",
"@oclif/core": "1.23.2",
"@oclif/plugin-autocomplete": "1.3.10",
"@segment/ajv-human-errors": "^2.1.2",
"@urql/core": "3.0.5",
"@urql/core": "3.1.1",
"@urql/exchange-retry": "1.0.0",
"ajv": "^8.11.0",
"ajv": "8.11.0",
"ajv-formats": "2.1.1",
"better-opn": "3.0.2",
"chalk": "4.1.2",
@@ -52,14 +53,14 @@
"graphql": "16.6.0",
"graphql-tag": "2.12.6",
"https-proxy-agent": "5.0.1",
"ignore": "5.2.0",
"ignore": "5.2.4",
"indent-string": "4.0.0",
"jks-js": "1.1.0",
"joi": "17.6.4",
"joi": "17.7.0",
"keychain": "1.3.0",
"log-symbols": "4.1.0",
"mime": "3.0.0",
"minimatch": "5.1.0",
"minimatch": "5.1.2",
"nanoid": "3.3.4",
"node-fetch": "2.6.7",
"node-forge": "1.3.1",
@@ -74,47 +75,47 @@
"resolve-from": "5.0.0",
"semver": "7.3.8",
"slash": "3.0.0",
"tar": "6.1.11",
"tar": "6.1.13",
"terminal-link": "2.1.1",
"tslib": "2.4.0",
"tslib": "2.4.1",
"turndown": "7.1.1",
"untildify": "4.0.0",
"uuid": "9.0.0",
"wrap-ansi": "7.0.0"
},
"devDependencies": {
"@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",
"@graphql-codegen/cli": "2.16.3",
"@graphql-codegen/introspection": "2.2.3",
"@graphql-codegen/typescript": "2.8.7",
"@graphql-codegen/typescript-operations": "2.5.12",
"@types/cli-progress": "3.11.0",
"@types/dateformat": "3.0.1",
"@types/envinfo": "7.8.1",
"@types/express": "4.17.14",
"@types/express": "4.17.15",
"@types/fs-extra": "9.0.13",
"@types/getenv": "^1.0.0",
"@types/mime": "3.0.1",
"@types/node-fetch": "2.6.2",
"@types/node-forge": "1.3.0",
"@types/node-forge": "1.3.1",
"@types/pngjs": "6.0.1",
"@types/promise-retry": "1.1.3",
"@types/prompts": "2.4.1",
"@types/semver": "7.3.12",
"@types/prompts": "2.4.2",
"@types/semver": "7.3.13",
"@types/tar": "6.1.3",
"@types/tough-cookie": "4.0.2",
"@types/uuid": "8.3.4",
"@types/uuid": "9.0.0",
"@types/wrap-ansi": "3.0.0",
"axios": "0.27.2",
"eslint-plugin-graphql": "4.0.0",
"express": "4.18.2",
"memfs": "3.4.7",
"memfs": "3.4.13",
"mockdate": "3.0.5",
"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"
"typescript": "4.9.4"
},
"engines": {
"node": ">=14.0.0"
@@ -202,7 +203,8 @@
"repository": "expo/eas-cli",
"scripts": {
"postpack": "rimraf oclif.manifest.json",
"prepack": "rimraf build && yarn build && node ./scripts/prepack.js",
"prepack": "yarn rebuild && node ./scripts/prepack.js",
"rebuild": "rimraf build && yarn build",
"pretarball-ci": "./scripts/pretarball-ci.sh",
"build": "tsc --project tsconfig.build.json",
"watch": "yarn build --watch --preserveWatchOutput",
+2 -2
View File
@@ -437,8 +437,8 @@
"$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)"
"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-mdn.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-mdn.apple.com/news/?id=091202019a)"
},
"seventeenPlus": {
"type": "boolean",
@@ -133,7 +133,9 @@ export async function createAnalyticsAsync(): Promise<AnalyticsWithOrchestration
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, false);
}
const analyticsEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, true);
const analyticsEnabled =
!process.env.https_proxy && // disable analytics if running behind proxy
(await UserSettings.getAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, true));
if (!analyticsEnabled) {
return new NoOpAnalytics();
}
@@ -3,7 +3,13 @@ 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 {
AppPlatform,
BuildFragment,
BuildPriority,
BuildResourceClass,
BuildStatus,
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { confirmAsync, selectAsync } from '../../prompts';
@@ -85,5 +91,6 @@ function createMockBuildFragment({
},
},
status: BuildStatus.InQueue,
resourceClass: BuildResourceClass.AndroidMedium,
};
}
@@ -0,0 +1,362 @@
import { Platform } from '@expo/eas-build-job';
import { CombinedError } from '@urql/core';
import { GraphQLError } from 'graphql/error';
import { EasCommandError } from '../../commandUtils/errors';
import Build from '../../commands/build';
import Log, { link } from '../../log';
import { handleBuildRequestError } from '../build';
import {
EasBuildDownForMaintenanceError,
EasBuildFreeTierDisabledAndroidError,
EasBuildFreeTierDisabledError,
EasBuildFreeTierDisabledIOSError,
EasBuildTooManyPendingBuildsError,
RequestValidationError,
TurtleDeprecatedJobFormatError,
} from '../errors';
beforeEach(() => {
jest.resetAllMocks();
});
const EXPECTED_STRINGIFIED_GRAPHQL_ERROR_JSON = `[
{
"message": "Error 1"
},
{
"message": "Error 2"
},
{
"message": "Error 3"
}
]`;
const EXPECTED_EAS_BUILD_DOWN_MESSAGE = `EAS Build is down for maintenance. Try again later. Check ${link(
'https://status.expo.dev/'
)} for updates.`;
const EXPECTED_MAX_BUILD_COUNT_MESSAGE_ANDROID = `You have already reached the maximum number of pending Android builds for your account. Try again later.`;
const EXPECTED_MAX_BUILD_COUNT_MESSAGE_IOS = `You have already reached the maximum number of pending iOS builds for your account. Try again later.`;
const EXPECTED_GENERIC_MESSAGE =
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, report the issue.';
const getGraphQLError = (message: string, errorCode: string): GraphQLError => {
return new GraphQLError(message, null, null, null, null, null, {
errorCode,
});
};
const assertReThrownError = (
caughtError: CombinedError | Error,
expectedType: typeof EasCommandError,
expectedMessage: string = 'Error 1'
): void => {
expect(caughtError).toBeInstanceOf(expectedType);
expect((caughtError as Error).message).toEqual(expectedMessage);
};
describe(Build.name, () => {
describe('handle build request errors', () => {
describe('logs graphQL errors to debug if present', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const logDebugSpy = jest.spyOn(Log, 'debug');
const graphQLErrors = ['Error 1', 'Error 2', 'Error 3'];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch {}
expect(logDebugSpy).toBeCalledWith(EXPECTED_STRINGIFIED_GRAPHQL_ERROR_JSON);
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const logDebugSpy = jest.spyOn(Log, 'debug');
const graphQLErrors = ['Error 1', 'Error 2', 'Error 3'];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch {}
expect(logDebugSpy).toBeCalledWith(EXPECTED_STRINGIFIED_GRAPHQL_ERROR_JSON);
});
});
describe('logs nothing to debug if graphQLErrors not present', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const logDebugSpy = jest.spyOn(Log, 'debug');
const error = new Error('Generic error');
try {
handleBuildRequestError(error, platform);
} catch {}
expect(logDebugSpy).toBeCalledWith(undefined);
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const logDebugSpy = jest.spyOn(Log, 'debug');
const error = new Error('Generic error');
try {
handleBuildRequestError(error, platform);
} catch {}
expect(logDebugSpy).toBeCalledWith(undefined);
});
});
describe('error is server-side defined error', () => {
describe('throws correct EasCommandError subclass', () => {
describe('for TURTLE_DEPRECATED_JOB_FORMAT', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'TURTLE_DEPRECATED_JOB_FORMAT');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, TurtleDeprecatedJobFormatError);
}
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'TURTLE_DEPRECATED_JOB_FORMAT');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, TurtleDeprecatedJobFormatError);
}
});
});
describe('for EAS_BUILD_FREE_TIER_DISABLED', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, EasBuildFreeTierDisabledError);
}
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, EasBuildFreeTierDisabledError);
}
});
});
it('does it for EAS_BUILD_FREE_TIER_DISABLED_IOS', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED_IOS');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, EasBuildFreeTierDisabledIOSError);
}
});
it('does it for EAS_BUILD_FREE_TIER_DISABLED_ANDROID', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED_ANDROID');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, EasBuildFreeTierDisabledAndroidError);
}
});
describe('for VALIDATION_ERROR', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'VALIDATION_ERROR');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, RequestValidationError);
}
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'VALIDATION_ERROR');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, RequestValidationError);
}
});
});
});
});
describe('error is other expected graphQL-related error', () => {
describe('throws correct EasCommandError subclass', () => {
describe('for EAS_BUILD_DOWN_FOR_MAINTENANCE', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_DOWN_FOR_MAINTENANCE');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(
caughtError as Error,
EasBuildDownForMaintenanceError,
EXPECTED_EAS_BUILD_DOWN_MESSAGE
);
}
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_DOWN_FOR_MAINTENANCE');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(
caughtError as Error,
EasBuildDownForMaintenanceError,
EXPECTED_EAS_BUILD_DOWN_MESSAGE
);
}
});
});
describe('for EAS_BUILD_TOO_MANY_PENDING_BUILDS', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_TOO_MANY_PENDING_BUILDS');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(
caughtError as Error,
EasBuildTooManyPendingBuildsError,
EXPECTED_MAX_BUILD_COUNT_MESSAGE_ANDROID
);
}
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_TOO_MANY_PENDING_BUILDS');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(
caughtError as Error,
EasBuildTooManyPendingBuildsError,
EXPECTED_MAX_BUILD_COUNT_MESSAGE_IOS
);
}
});
});
});
});
describe('error is unexpected graphQL-related error', () => {
describe('throws base Error class with custom message', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, Error, EXPECTED_GENERIC_MESSAGE);
}
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, Error, EXPECTED_GENERIC_MESSAGE);
}
});
});
});
describe('error is unexpected non-graphQL-related error', () => {
describe('throws base Error class with propagated message', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const error = new Error('Non-graphQL-related error');
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
expect(caughtError).toStrictEqual(error);
}
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const error = new Error('Non-graphQL-related error');
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
expect(caughtError).toStrictEqual(error);
}
});
});
});
});
});
+3 -2
View File
@@ -62,7 +62,7 @@ This means that it will most likely produce an AAB and you will not be able to i
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
if (ctx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp, ctx.user);
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx);
}
const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext);
@@ -124,7 +124,8 @@ export async function prepareAndroidBuildAsync(
}
function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean {
return !ctx.buildProfile.withoutCredentials;
const isCustomBuild = ctx.buildProfile.config !== undefined;
return !ctx.buildProfile.withoutCredentials && !isCustomBuild;
}
async function ensureAndroidCredentialsAsync(
@@ -1,11 +1,17 @@
import { Android } from '@expo/eas-build-job';
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
import { transformProjectArchive, transformWorkflow } from '../graphql';
import {
transformBuildMode,
transformBuildTrigger,
transformProjectArchive,
transformWorkflow,
} from '../graphql';
export function transformJob(job: Android.Job): AndroidJobInput {
return {
type: transformWorkflow(job.type),
triggeredBy: transformBuildTrigger(job.triggeredBy),
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
@@ -21,6 +27,8 @@ export function transformJob(job: Android.Job): AndroidJobInput {
buildType: job.buildType && transformBuildType(job.buildType),
developmentClient: job.developmentClient,
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
};
}
@@ -1,9 +1,18 @@
import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
import {
Android,
ArchiveSource,
BuildMode,
BuildTrigger,
Job,
Platform,
sanitizeJob,
} from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import path from 'path';
import slash from 'slash';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { getCustomBuildConfigPath } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
@@ -16,7 +25,6 @@ interface JobData {
const cacheDefaults = {
disabled: false,
customPaths: [],
cacheDefaultPaths: true,
};
export async function prepareJobAsync(
@@ -41,11 +49,15 @@ export async function prepareJobAsync(
}
: {};
let buildType = ctx.buildProfile.buildType;
if (!buildType && !buildProfile.gradleCommand && ctx.buildProfile.distribution === 'internal') {
let buildType = buildProfile.buildType;
if (!buildType && !buildProfile.gradleCommand && buildProfile.distribution === 'internal') {
buildType = Android.BuildType.APK;
}
const maybeCustomBuildConfigPath = buildProfile.config
? getCustomBuildConfigPath(buildProfile.config)
: undefined;
const job: Android.Job = {
type: ctx.workflow,
platform: Platform.ANDROID,
@@ -67,8 +79,8 @@ export async function prepareJobAsync(
secrets: {
...buildCredentials,
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
developmentClient: buildProfile.developmentClient,
gradleCommand: buildProfile.gradleCommand,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
@@ -81,8 +93,15 @@ export async function prepareJobAsync(
},
}),
experimental: {
prebuildCommand: ctx.buildProfile.prebuildCommand,
prebuildCommand: buildProfile.prebuildCommand,
},
mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
triggeredBy: BuildTrigger.EAS_CLI,
...(maybeCustomBuildConfigPath && {
customBuildConfig: {
path: maybeCustomBuildConfigPath,
},
}),
};
return sanitizeJob(job);
+157 -58
View File
@@ -1,5 +1,6 @@
import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import { CredentialsSource, EasJsonAccessor, EasJsonUtils, ResourceClass } from '@expo/eas-json';
import assert from 'assert';
import chalk from 'chalk';
import cliProgress from 'cli-progress';
import fs from 'fs-extra';
@@ -9,15 +10,17 @@ import { BuildEvent } from '../analytics/AnalyticsManager';
import { withAnalyticsAsync } from '../analytics/common';
import { getExpoWebsiteBaseUrl } from '../api';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EasCommandError } from '../commandUtils/errors';
import {
AppPlatform,
BuildFragment,
BuildParamsInput,
BuildPriority,
BuildResourceClass,
BuildStatus,
UploadSessionType,
} from '../graphql/generated';
import { BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildMutation, BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log, { learnMore, link } from '../log';
import { Ora, ora } from '../ora';
@@ -26,13 +29,25 @@ import {
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../platform';
import { confirmAsync } from '../prompts';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { printJsonOnlyOutput } from '../utils/json';
import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
import { getVcsClient } from '../vcs';
import { BuildContext } from './context';
import { runLocalBuildAsync } from './local';
import {
EasBuildDownForMaintenanceError,
EasBuildFreeTierDisabledAndroidError,
EasBuildFreeTierDisabledError,
EasBuildFreeTierDisabledIOSError,
EasBuildTooManyPendingBuildsError,
RequestValidationError,
TurtleDeprecatedJobFormatError,
} from './errors';
import { transformMetadata } from './graphql';
import { LocalBuildMode, runLocalBuildAsync } from './local';
import { collectMetadataAsync } from './metadata';
import { printDeprecationWarnings } from './utils/printBuildInfo';
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
@@ -107,15 +122,24 @@ export async function prepareBuildRequestForPlatformAsync<
);
}
const projectArchive = ctx.localBuildOptions.enable
? ({
type: ArchiveSourceType.PATH,
path: (await makeProjectTarballAsync()).path,
} as const)
: ({
type: ArchiveSourceType.GCS,
bucketKey: await uploadProjectAsync(ctx),
} as const);
let projectArchive: ArchiveSource | undefined;
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
projectArchive = {
type: ArchiveSourceType.PATH,
path: (await makeProjectTarballAsync()).path,
};
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
projectArchive = {
type: ArchiveSourceType.PATH,
path: process.cwd(),
};
} else if (!ctx.localBuildOptions.localBuildMode) {
projectArchive = {
type: ArchiveSourceType.GCS,
bucketKey: await uploadProjectAsync(ctx),
};
}
assert(projectArchive);
const metadata = await collectMetadataAsync(ctx);
const buildParams = resolveBuildParamsInput(ctx);
@@ -125,66 +149,55 @@ export async function prepareBuildRequestForPlatformAsync<
});
return async () => {
if (ctx.localBuildOptions.enable) {
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions);
return undefined;
} else {
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
await BuildMutation.updateBuildMetadataAsync(ctx.graphqlClient, {
buildId: nullthrows(process.env.EAS_BUILD_ID),
metadata: transformMetadata(metadata),
});
printJsonOnlyOutput({ job, metadata });
return undefined;
} else if (!ctx.localBuildOptions.localBuildMode) {
try {
return await sendBuildRequestAsync(builder, job, metadata, buildParams);
} catch (error: any) {
handleBuildRequestError(error, job.platform);
}
} else {
throw new Error('Unknown localBuildMode.');
}
};
}
function handleBuildRequestError(error: any, platform: Platform): never {
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
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(
const SERVER_SIDE_DEFINED_ERRORS: Record<string, typeof EasCommandError> = {
TURTLE_DEPRECATED_JOB_FORMAT: TurtleDeprecatedJobFormatError,
EAS_BUILD_FREE_TIER_DISABLED: EasBuildFreeTierDisabledError,
EAS_BUILD_FREE_TIER_DISABLED_IOS: EasBuildFreeTierDisabledIOSError,
EAS_BUILD_FREE_TIER_DISABLED_ANDROID: EasBuildFreeTierDisabledAndroidError,
VALIDATION_ERROR: RequestValidationError,
};
export function handleBuildRequestError(error: any, platform: Platform): never {
Log.debug(JSON.stringify(error.graphQLErrors, null, 2));
const graphQLErrorCode: string = error?.graphQLErrors?.[0]?.extensions?.errorCode;
if (graphQLErrorCode in SERVER_SIDE_DEFINED_ERRORS) {
const ErrorClass: typeof EasCommandError = SERVER_SIDE_DEFINED_ERRORS[graphQLErrorCode];
throw new ErrorClass(error?.graphQLErrors?.[0]?.message);
} else if (graphQLErrorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE') {
throw new EasBuildDownForMaintenanceError(
`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 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 (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
) {
Log.error(
} else if (graphQLErrorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS') {
throw new EasBuildTooManyPendingBuildsError(
`You have already reached the maximum number of pending ${requestedPlatformDisplayNames[platform]} builds for your account. Try again later.`
);
throw new Error('Build request failed.');
} else if (error?.graphQLErrors) {
Log.error(
throw new Error(
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, report the issue.'
);
}
@@ -285,7 +298,12 @@ export type MaybeBuildFragment = BuildFragment | null;
export async function waitForBuildEndAsync(
graphqlClient: ExpoGraphqlClient,
{ buildIds, accountName }: { buildIds: string[]; accountName: string },
{
buildIds,
accountName,
projectDir,
nonInteractive,
}: { buildIds: string[]; accountName: string; projectDir: string; nonInteractive: boolean },
{ intervalSec = 10 } = {}
): Promise<MaybeBuildFragment[]> {
let spinner;
@@ -302,7 +320,10 @@ export async function waitForBuildEndAsync(
const builds = await getBuildsSafelyAsync(graphqlClient, buildIds);
const { refetch } =
builds.length === 1
? await handleSingleBuildProgressAsync({ build: builds[0], accountName }, { spinner })
? await handleSingleBuildProgressAsync(
{ build: builds[0], accountName, projectDir, nonInteractive },
{ spinner }
)
: await handleMultipleBuildsProgressAsync({ builds }, { spinner, originalSpinnerText });
if (!refetch) {
return builds;
@@ -337,7 +358,17 @@ const queueProgressBar = new cliProgress.SingleBar(
);
async function handleSingleBuildProgressAsync(
{ build, accountName }: { build: MaybeBuildFragment; accountName: string },
{
build,
accountName,
projectDir,
nonInteractive,
}: {
build: MaybeBuildFragment;
accountName: string;
projectDir: string;
nonInteractive: boolean;
},
{ spinner }: { spinner: Ora }
): Promise<BuildProgressResult> {
if (build === null) {
@@ -376,7 +407,8 @@ async function handleSingleBuildProgressAsync(
if (
!queueProgressBarStarted &&
typeof build.initialQueuePosition === 'number' &&
typeof build.queuePosition === 'number'
typeof build.queuePosition === 'number' &&
typeof build.estimatedWaitTimeLeftSeconds === 'number'
) {
spinner.stopAndPersist();
if (build.priority !== BuildPriority.High) {
@@ -388,6 +420,55 @@ async function handleSingleBuildProgressAsync(
)}`
);
}
if (
build.platform === AppPlatform.Ios &&
[BuildResourceClass.IosIntelLarge, BuildResourceClass.IosIntelMedium].includes(
build.resourceClass
)
) {
let askToSwitchToM1 = false;
if (
build.priority === BuildPriority.High &&
build.estimatedWaitTimeLeftSeconds >= /* 10 minutes */ 10 * 60
) {
Log.newLine();
Log.warn(
`Warning: Priority queue wait time for legacy iOS Intel workers is longer than usual (more than 10 minutes).`
);
Log.warn(`We recommend switching to the new, faster M1 workers for iOS builds.`);
Log.warn(
learnMore('https://blog.expo.dev/m1-workers-on-eas-build-dcaa2c1333ad', {
learnMoreMessage: 'Learn more on switching to M1 workers.',
})
);
askToSwitchToM1 = true;
} else if (
build.priority !== BuildPriority.High &&
build.estimatedWaitTimeLeftSeconds >= /* 120 minutes */ 120 * 60
) {
Log.newLine();
Log.warn(
`Warning: Free tier queue wait time for legacy iOS Intel workers is longer than usual.`
);
Log.warn(`We recommend switching to the new, faster M1 workers for iOS builds.`);
Log.warn(
learnMore('https://blog.expo.dev/m1-workers-on-eas-build-dcaa2c1333ad', {
learnMoreMessage: 'Learn more on switching to M1 workers.',
})
);
askToSwitchToM1 = true;
}
if (!nonInteractive && askToSwitchToM1) {
const shouldSwitchToM1 = await confirmAsync({
message: `Switch iOS builds to M1 workers (modifies build profiles in eas.json)?`,
});
if (shouldSwitchToM1) {
await updateIosBuildProfilesToUseM1WorkersAsync(projectDir);
}
}
}
Log.newLine();
Log.log(`Waiting in ${priorityToQueueDisplayName[build.priority]}`);
queueProgressBar.start(
@@ -529,3 +610,21 @@ function formatAccountSubscriptionsUrl(accountName: string): string {
getExpoWebsiteBaseUrl()
).toString();
}
async function updateIosBuildProfilesToUseM1WorkersAsync(projectDir: string): Promise<void> {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
await easJsonAccessor.readRawJsonAsync();
const profileNames = await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor);
easJsonAccessor.patch(easJsonRawObject => {
for (const profileName of profileNames) {
easJsonRawObject.build[profileName].ios = {
...easJsonRawObject.build[profileName].ios,
resourceClass: ResourceClass.M_MEDIUM,
};
}
return easJsonRawObject;
});
await easJsonAccessor.writeAsync();
Log.withTick('Updated eas.json. Your next builds will run on M1 workers.');
}
+21 -3
View File
@@ -1,5 +1,5 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJson, EasJsonAccessor } from '@expo/eas-json';
import { EasJson, EasJsonAccessor, ResourceClass } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
@@ -53,11 +53,21 @@ const EAS_JSON_MANAGED_DEFAULT: EasJson = {
development: {
developmentClient: true,
distribution: 'internal',
ios: {
resourceClass: ResourceClass.M_MEDIUM,
},
},
preview: {
distribution: 'internal',
ios: {
resourceClass: ResourceClass.M_MEDIUM,
},
},
production: {
ios: {
resourceClass: ResourceClass.M_MEDIUM,
},
},
production: {},
},
submit: {
production: {},
@@ -76,12 +86,20 @@ const EAS_JSON_BARE_DEFAULT: EasJson = {
},
ios: {
buildConfiguration: 'Debug',
resourceClass: ResourceClass.M_MEDIUM,
},
},
preview: {
distribution: 'internal',
ios: {
resourceClass: ResourceClass.M_MEDIUM,
},
},
production: {
ios: {
resourceClass: ResourceClass.M_MEDIUM,
},
},
production: {},
},
submit: {
production: {},
+12 -5
View File
@@ -1,5 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJson } from '@expo/eas-json';
import { BuildProfile, EasJson, ResourceClass } from '@expo/eas-json';
import JsonFile from '@expo/json-file';
import getenv from 'getenv';
import resolveFrom from 'resolve-from';
@@ -9,7 +9,6 @@ import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/An
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { CredentialsContext } from '../credentials/context';
import { BuildResourceClass } from '../graphql/generated';
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { Actor } from '../user/User';
@@ -17,6 +16,7 @@ import { createAndroidContextAsync } from './android/build';
import { BuildContext, CommonContext } from './context';
import { createIosContextAsync } from './ios/build';
import { LocalBuildOptions } from './local';
import { resolveBuildResourceClassAsync } from './utils/resourceClass';
export async function createBuildContextAsync<T extends Platform>({
buildProfileName,
@@ -28,7 +28,7 @@ export async function createBuildContextAsync<T extends Platform>({
noWait,
platform,
projectDir,
resourceClass,
resourceClassFlag,
message,
actor,
graphqlClient,
@@ -44,7 +44,7 @@ export async function createBuildContextAsync<T extends Platform>({
noWait: boolean;
platform: T;
projectDir: string;
resourceClass: BuildResourceClass;
resourceClassFlag?: ResourceClass;
message?: string;
actor: Actor;
graphqlClient: ExpoGraphqlClient;
@@ -52,7 +52,6 @@ export async function createBuildContextAsync<T extends Platform>({
getDynamicProjectConfigAsync: DynamicConfigContextFn;
}): Promise<BuildContext<T>> {
const { exp, projectId } = await getDynamicProjectConfigAsync({ env: buildProfile.env });
const projectName = exp.slug;
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const workflow = await resolveWorkflowAsync(projectDir, platform);
@@ -88,6 +87,14 @@ export async function createBuildContextAsync<T extends Platform>({
};
analytics.logEvent(BuildEvent.BUILD_COMMAND, analyticsEventProperties);
const resourceClass = await resolveBuildResourceClassAsync(
buildProfile,
platform,
projectDir,
exp,
resourceClassFlag
);
const commonContext: CommonContext<T> = {
accountName: account.name,
buildProfile,
+15
View File
@@ -0,0 +1,15 @@
import { EasCommandError } from '../commandUtils/errors';
export class TurtleDeprecatedJobFormatError extends EasCommandError {}
export class EasBuildFreeTierDisabledError extends EasCommandError {}
export class EasBuildFreeTierDisabledIOSError extends EasCommandError {}
export class EasBuildFreeTierDisabledAndroidError extends EasCommandError {}
export class RequestValidationError extends EasCommandError {}
export class EasBuildDownForMaintenanceError extends EasCommandError {}
export class EasBuildTooManyPendingBuildsError extends EasCommandError {}
+32 -1
View File
@@ -1,11 +1,19 @@
import { ArchiveSource, ArchiveSourceType, Metadata, Workflow } from '@expo/eas-build-job';
import {
ArchiveSource,
ArchiveSourceType,
BuildTrigger,
Metadata,
Workflow,
} from '@expo/eas-build-job';
import {
BuildCredentialsSource,
BuildIosEnterpriseProvisioning,
BuildMetadataInput,
BuildMode,
BuildWorkflow,
DistributionType,
BuildTrigger as GraphQLBuildTrigger,
ProjectArchiveSourceInput,
ProjectArchiveSourceType,
} from '../graphql/generated';
@@ -34,6 +42,7 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
export function transformMetadata(metadata: Metadata): BuildMetadataInput {
return {
...metadata,
buildMode: metadata.buildMode && transformBuildMode(metadata.buildMode),
credentialsSource:
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
distribution: metadata.distribution && transformDistribution(metadata.distribution),
@@ -81,3 +90,25 @@ export function transformIosEnterpriseProvisioning(
return BuildIosEnterpriseProvisioning.Universal;
}
}
// TODO: check what in metadata
export function transformBuildMode(buildMode: string): BuildMode {
if (buildMode === 'build') {
return BuildMode.Build;
} else if (buildMode === 'resign') {
return BuildMode.Resign;
} else if (buildMode === 'custom') {
return BuildMode.Custom;
} else {
throw new Error(`Unsupported build mode: ${buildMode}`);
}
}
export function transformBuildTrigger(buildTrigger: BuildTrigger): GraphQLBuildTrigger {
if (buildTrigger === BuildTrigger.EAS_CLI) {
return GraphQLBuildTrigger.EasCli;
} else if (buildTrigger === BuildTrigger.GIT_BASED_INTEGRATION) {
return GraphQLBuildTrigger.GitBasedIntegration;
}
throw new Error('Unknown build trigger');
}
+1 -1
View File
@@ -27,7 +27,7 @@ export async function createIosContextAsync(
const { buildProfile } = ctx;
if (ctx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp, ctx.user);
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx);
}
checkNodeEnvVariable(ctx);
@@ -55,5 +55,6 @@ export async function ensureIosCredentialsForBuildResignAsync(
}
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
return !buildCtx.buildProfile.simulator;
const isCustomBuild = buildCtx.buildProfile.config !== undefined;
return !buildCtx.buildProfile.simulator && !isCustomBuild;
}
+11 -3
View File
@@ -2,16 +2,22 @@ import { Ios } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
import { transformProjectArchive, transformWorkflow } from '../graphql';
import {
transformBuildMode,
transformBuildTrigger,
transformProjectArchive,
transformWorkflow,
} from '../graphql';
export function transformJob(job: Ios.Job): IosJobInput {
return {
type: transformWorkflow(job.type),
triggeredBy: transformBuildTrigger(job.triggeredBy),
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
projectRootDirectory: nullthrows(job.projectRootDirectory),
releaseChannel: job.releaseChannel,
updates: job.updates,
secrets: transformIosSecrets(job.secrets),
secrets: job.secrets ? transformIosSecrets(job.secrets) : undefined,
builderEnvironment: job.builderEnvironment,
cache: job.cache,
version: job.version?.buildNumber ? { buildNumber: job.version.buildNumber } : undefined,
@@ -23,6 +29,8 @@ export function transformJob(job: Ios.Job): IosJobInput {
developmentClient: job.developmentClient,
simulator: job.simulator,
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
};
}
+42 -22
View File
@@ -1,10 +1,20 @@
import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
import {
ArchiveSource,
BuildMode,
BuildTrigger,
Ios,
Job,
Platform,
sanitizeJob,
} from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import nullthrows from 'nullthrows';
import path from 'path';
import slash from 'slash';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { IosJobSecretsInput } from '../../graphql/generated';
import { getCustomBuildConfigPath } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
@@ -18,17 +28,17 @@ interface JobData {
const cacheDefaults = {
disabled: false,
customPaths: [],
cacheDefaultPaths: true,
};
export async function prepareJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData
): Promise<Job> {
const username = getUsername(ctx.exp, ctx.user);
const buildProfile: BuildProfile<Platform.IOS> = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const username = getUsername(ctx.exp, ctx.user);
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
const buildCredentials: Ios.BuildSecrets['buildCredentials'] = {};
if (jobData.credentials) {
const targetNames = Object.keys(jobData.credentials);
for (const targetName of targetNames) {
@@ -36,38 +46,41 @@ export async function prepareJobAsync(
}
}
const maybeCustomBuildConfigPath = buildProfile.config
? getCustomBuildConfigPath(buildProfile.config)
: undefined;
const job: Ios.Job = {
type: ctx.workflow,
platform: Platform.IOS,
projectArchive: jobData.projectArchive,
projectRootDirectory,
builderEnvironment: {
image: ctx.buildProfile.image,
node: ctx.buildProfile.node,
yarn: ctx.buildProfile.yarn,
bundler: ctx.buildProfile.bundler,
cocoapods: ctx.buildProfile.cocoapods,
fastlane: ctx.buildProfile.fastlane,
expoCli: ctx.buildProfile.expoCli,
env: ctx.buildProfile.env,
image: buildProfile.image,
node: buildProfile.node,
yarn: buildProfile.yarn,
bundler: buildProfile.bundler,
cocoapods: buildProfile.cocoapods,
fastlane: buildProfile.fastlane,
expoCli: buildProfile.expoCli,
env: buildProfile.env,
},
cache: {
...cacheDefaults,
...ctx.buildProfile.cache,
...buildProfile.cache,
clear: ctx.clearCache,
},
secrets: {
buildCredentials,
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
developmentClient: ctx.buildProfile.developmentClient,
simulator: ctx.buildProfile.simulator,
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
developmentClient: buildProfile.developmentClient,
simulator: buildProfile.simulator,
scheme: jobData.buildScheme,
buildConfiguration: ctx.buildProfile.buildConfiguration,
applicationArchivePath:
ctx.buildProfile.applicationArchivePath ?? ctx.buildProfile.artifactPath,
buildArtifactPaths: ctx.buildProfile.buildArtifactPaths,
buildConfiguration: buildProfile.buildConfiguration,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths,
username,
...(ctx.ios.buildNumberOverride && {
version: {
@@ -75,8 +88,15 @@ export async function prepareJobAsync(
},
}),
experimental: {
prebuildCommand: ctx.buildProfile.prebuildCommand,
prebuildCommand: buildProfile.prebuildCommand,
},
mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
triggeredBy: BuildTrigger.EAS_CLI,
...(maybeCustomBuildConfigPath && {
customBuildConfig: {
path: maybeCustomBuildConfigPath,
},
}),
};
return sanitizeJob(job);
}
+20 -2
View File
@@ -6,10 +6,28 @@ import semver from 'semver';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.120';
const PLUGIN_PACKAGE_VERSION = '0.0.128';
export enum LocalBuildMode {
/**
* Local build that users can run on their own machines. Instead
* of sending build request to EAS Servers it's passing it as an argument
* to local-build-plugin, that will run the build locally.
*
* Triggered when running `eas build --local`.
*/
LOCAL_BUILD_PLUGIN = 'local-build-plugin',
/**
* Type of local build that is not accessible to users directly. When
* cloud build is triggered by git based integration, we are running
* in this mode. Instead of sending build request to EAS Servers it's
* printing it to the stdout as JSON, so EAS Build worker can read it.
*/
INTERNAL = 'internal',
}
export interface LocalBuildOptions {
enable: boolean;
localBuildMode?: LocalBuildMode;
skipCleanup?: boolean;
skipNativeBuild?: boolean;
artifactsDir?: string;
+5 -6
View File
@@ -1,5 +1,5 @@
import { Updates } from '@expo/config-plugins';
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
import { BuildMode, Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
import { IosEnterpriseProvisioning } from '@expo/eas-json';
import fs from 'fs-extra';
import resolveFrom from 'resolve-from';
@@ -37,7 +37,7 @@ export async function collectMetadataAsync<T extends Platform>(
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: Updates.getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
reactNativeVersion: await getReactNativeVersionAsync(ctx),
reactNativeVersion: await getReactNativeVersionAsync(ctx.projectDir),
...channelOrReleaseChannel,
distribution,
appName: ctx.exp.name,
@@ -57,6 +57,7 @@ export async function collectMetadataAsync<T extends Platform>(
}),
runWithNoWaitFlag: ctx.noWait,
runFromCI: ctx.runFromCI,
buildMode: ctx.buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
};
return sanitizeMetadata(metadata);
}
@@ -155,11 +156,9 @@ async function getNativeChannelAsync<T extends Platform>(
return undefined;
}
async function getReactNativeVersionAsync(
ctx: BuildContext<Platform>
): Promise<string | undefined> {
export async function getReactNativeVersionAsync(projectDir: string): Promise<string | undefined> {
try {
const reactNativePackageJsonPath = resolveFrom(ctx.projectDir, 'react-native/package.json');
const reactNativePackageJsonPath = resolveFrom(projectDir, 'react-native/package.json');
return (await fs.readJson(reactNativePackageJsonPath)).version;
} catch (err) {
Log.debug('Failed to resolve react-native version:');
+23 -12
View File
@@ -99,10 +99,6 @@ export async function listAndSelectBuildOnAppAsync(
}
}
function formatBuildChoiceValue(value: string | undefined | null): string {
return value ? chalk.bold(value) : 'Unknown';
}
function createBuildToPartialChoiceMaker(
selectPromptDisabledFunction?: (build: BuildFragment) => boolean
) {
@@ -119,19 +115,34 @@ function createBuildToPartialChoiceMaker(
? `${build.gitCommitHash.slice(0, 7)} "${chalk.bold(
splitCommitMessage[0] + (splitCommitMessage.length > 1 ? '…' : '')
)}"`
: 'Unknown';
: null;
const descriptionItems: { name: string; value: string | null }[] = [
{ name: 'Version', value: build.appVersion ? chalk.bold(build.appVersion) : null },
{ name: 'Commit', value: formattedCommitData },
{
name: 'Message',
value: build.message
? chalk.bold(
build.message.length > 200 ? `${build.message.slice(0, 200)}...` : build.message
)
: null,
},
{
name: build.platform === AppPlatform.Ios ? 'Build number' : 'Version code',
value: build.appBuildVersion ? chalk.bold(build.appBuildVersion) : null,
},
];
const filteredDescriptionArray: string[] = descriptionItems
.filter(item => item.value)
.map(item => `${chalk.bold(item.name)}: ${item.value}`);
return {
title: `${chalk.bold(`ID:`)} ${build.id} (${chalk.bold(
`${fromNow(new Date(build.completedAt))} ago`
)})`,
description: [
`\t${chalk.bold(`Version:`)} ${formatBuildChoiceValue(build.appVersion)}`,
`\t${chalk.bold(
build.platform === AppPlatform.Ios ? 'Build number:' : 'Version code:'
)} ${formatBuildChoiceValue(build.appBuildVersion)}`,
`\t${chalk.bold(`Commit:`)} ${formattedCommitData}`,
].join('\n'),
description: filteredDescriptionArray.length > 0 ? filteredDescriptionArray.join('\n') : '',
disabled: selectPromptDisabledFunction?.(build),
};
};
@@ -18,7 +18,6 @@ import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGr
import {
AppPlatform,
BuildFragment,
BuildResourceClass,
BuildStatus,
BuildWithSubmissionsFragment,
SubmissionFragment,
@@ -32,6 +31,7 @@ import {
appPlatformEmojis,
toPlatforms,
} from '../platform';
import { validateCustomBuildConfigAsync } from '../project/customBuildConfig';
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
import { validateAppVersionRuntimePolicySupportAsync } from '../project/projectUtils';
@@ -84,81 +84,6 @@ export interface BuildFlags {
message?: string;
}
const iosResourceClassToBuildResourceClassMapping: Record<ResourceClass, BuildResourceClass> = {
[ResourceClass.DEFAULT]: BuildResourceClass.IosDefault,
[ResourceClass.LARGE]: BuildResourceClass.IosLarge,
[ResourceClass.M1_EXPERIMENTAL]: BuildResourceClass.IosM1Large,
[ResourceClass.M1_MEDIUM]: BuildResourceClass.IosM1Medium,
[ResourceClass.M1_LARGE]: BuildResourceClass.IosM1Large,
[ResourceClass.INTEL_MEDIUM]: BuildResourceClass.IosIntelMedium,
[ResourceClass.MEDIUM]: BuildResourceClass.IosMedium,
};
const androidResourceClassToBuildResourceClassMapping: Record<
Exclude<
ResourceClass,
| ResourceClass.M1_EXPERIMENTAL
| ResourceClass.M1_MEDIUM
| ResourceClass.M1_LARGE
| ResourceClass.INTEL_MEDIUM
>,
BuildResourceClass
> = {
[ResourceClass.DEFAULT]: BuildResourceClass.AndroidDefault,
[ResourceClass.LARGE]: BuildResourceClass.AndroidLarge,
[ResourceClass.MEDIUM]: BuildResourceClass.AndroidMedium,
};
function resolveBuildResourceClass(
profile: ProfileData<'build'>,
resourceClassFlag?: ResourceClass
): BuildResourceClass {
if (
profile.platform !== Platform.IOS &&
resourceClassFlag &&
[
ResourceClass.M1_EXPERIMENTAL,
ResourceClass.M1_MEDIUM,
ResourceClass.M1_LARGE,
ResourceClass.INTEL_MEDIUM,
].includes(resourceClassFlag)
) {
throw new Error(`Resource class ${resourceClassFlag} is only available for iOS builds`);
}
const profileResourceClass = profile.profile.resourceClass;
if (profileResourceClass && resourceClassFlag && resourceClassFlag !== profileResourceClass) {
Log.warn(
`Build profile specifies the "${profileResourceClass}" resource class but you passed "${resourceClassFlag}" to --resource-class.\nUsing the "${resourceClassFlag}" as the override.`
);
}
const resourceClass = resourceClassFlag ?? profileResourceClass ?? ResourceClass.DEFAULT;
if (profile.platform === Platform.IOS && resourceClass === ResourceClass.M1_EXPERIMENTAL) {
Log.warn(`Resource class ${chalk.bold('m1-experimental')} is deprecated.`);
}
if (
profile.platform === Platform.IOS &&
[ResourceClass.LARGE, ResourceClass.M1_LARGE].includes(resourceClass)
) {
Log.warn(
`Large resource classes are not available for iOS builds yet. Your build will use the medium resource class.`
);
}
return profile.platform === Platform.ANDROID
? androidResourceClassToBuildResourceClassMapping[
resourceClass as Exclude<
ResourceClass,
| ResourceClass.M1_EXPERIMENTAL
| ResourceClass.M1_MEDIUM
| ResourceClass.M1_LARGE
| ResourceClass.INTEL_MEDIUM
>
]
: iosResourceClassToBuildResourceClassMapping[resourceClass];
}
export async function runBuildAndSubmitAsync(
graphqlClient: ExpoGraphqlClient,
analytics: Analytics,
@@ -174,7 +99,7 @@ export async function runBuildAndSubmitAsync(
projectDir,
nonInteractive: flags.nonInteractive,
});
const easJsonAccessor = new EasJsonAccessor(projectDir);
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
@@ -194,6 +119,7 @@ export async function runBuildAndSubmitAsync(
for (const buildProfile of buildProfiles) {
validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig);
await validateCustomBuildConfigAsync(projectDir, buildProfile.profile);
}
const startedBuilds: {
@@ -208,7 +134,6 @@ export async function runBuildAndSubmitAsync(
flags,
moreBuilds: platforms.length > 1,
buildProfile,
resourceClass: resolveBuildResourceClass(buildProfile, flags.resourceClass),
easJsonCliConfig,
actor,
graphqlClient,
@@ -221,7 +146,7 @@ export async function runBuildAndSubmitAsync(
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
}
if (flags.localBuildOptions.enable) {
if (flags.localBuildOptions.localBuildMode) {
return;
}
@@ -275,6 +200,8 @@ export async function runBuildAndSubmitAsync(
const builds = await waitForBuildEndAsync(graphqlClient, {
buildIds: startedBuilds.map(({ build }) => build.id),
accountName,
projectDir,
nonInteractive: flags.nonInteractive,
});
if (!flags.json) {
printBuildResults(builds);
@@ -314,7 +241,6 @@ async function prepareAndStartBuildAsync({
flags,
moreBuilds,
buildProfile,
resourceClass,
easJsonCliConfig,
actor,
graphqlClient,
@@ -325,7 +251,6 @@ async function prepareAndStartBuildAsync({
flags: BuildFlags;
moreBuilds: boolean;
buildProfile: ProfileData<'build'>;
resourceClass: BuildResourceClass;
easJsonCliConfig: EasJson['cli'];
actor: Actor;
graphqlClient: ExpoGraphqlClient;
@@ -334,7 +259,7 @@ async function prepareAndStartBuildAsync({
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName,
resourceClass,
resourceClassFlag: flags.resourceClass,
clearCache: flags.clearCache,
buildProfile: buildProfile.profile,
nonInteractive: flags.nonInteractive,
@@ -38,6 +38,10 @@ export function formatGraphQLBuild(build: BuildFragment): string {
}
},
},
{
label: 'Message',
value: build.message,
},
{
label: 'Distribution',
value: build.distribution?.toLowerCase(),
@@ -15,6 +15,32 @@ import Log, { learnMore, link } from '../../log';
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
function terminalLinkFallback(url: string, text: string): string {
return `${text} (${url})`;
}
const errorCodeToErrorMessageOverride: Record<string, (build: BuildFragment) => string> = {
EAS_BUILD_UNKNOWN_FASTLANE_ERROR: build =>
`The ${link(getBuildLogsUrl(build, 'run-fastlane'), {
text: '"Run fastlane"',
fallback: terminalLinkFallback(getBuildLogsUrl(build, 'run-fastlane'), '"Run fastlane"'),
})} step failed with an unknown error. Refer to the ${link(
getBuildLogsUrl(build, 'xcode-logs'),
{
text: '"Xcode logs"',
fallback: terminalLinkFallback(getBuildLogsUrl(build, 'xcode-logs'), '"Xcode logs"'),
}
)} phase for additional, more detailed logs`,
EAS_BUILD_UNKNOWN_GRADLE_ERROR: build =>
`Gradle build failed with unknown error. See logs for the ${link(
getBuildLogsUrl(build, 'run-gradlew'),
{
text: '"Run gradlew"',
fallback: terminalLinkFallback(getBuildLogsUrl(build, 'run-gradlew'), '"Run gradlew"'),
}
)} phase for more information.`,
};
export function printLogsUrls(builds: BuildFragment[]): void {
if (builds.length === 1) {
Log.log(`Build details: ${link(getBuildLogsUrl(builds[0]))}`);
@@ -31,7 +57,6 @@ export function printLogsUrls(builds: BuildFragment[]): void {
}
export function printBuildResults(builds: (BuildFragment | null)[]): void {
Log.newLine();
if (builds.length === 1) {
const [build] = builds;
assert(build, 'Build should be defined');
@@ -42,8 +67,8 @@ export function printBuildResults(builds: (BuildFragment | null)[]): void {
}
function printBuildResult(build: BuildFragment): void {
Log.addNewLineIfNone();
if (build.status === BuildStatus.Errored) {
Log.addNewLineIfNone();
const userError = build.error;
Log.error(
`${appPlatformEmojis[build.platform]} ${
@@ -51,11 +76,12 @@ function printBuildResult(build: BuildFragment): void {
} build failed${userError ? ':' : ''}`
);
if (userError) {
printUserError(userError);
printUserError(userError, build);
}
return;
}
if (build.status === BuildStatus.Canceled) {
Log.addNewLineIfNone();
Log.error(
`${appPlatformEmojis[build.platform]} ${
appPlatformDisplayNames[build.platform]
@@ -65,6 +91,7 @@ function printBuildResult(build: BuildFragment): void {
}
if (build.distribution === DistributionType.Internal) {
Log.addNewLineIfNone();
const logsUrl = getBuildLogsUrl(build);
// It's tricky to install the .apk file directly on Android so let's fallback
// to the build details page and let people press the button to download there
@@ -82,9 +109,14 @@ function printBuildResult(build: BuildFragment): void {
} else {
// TODO: it looks like buildUrl could possibly be undefined, based on the code below.
// we should account for this case better if it is possible
const url = build.artifacts?.buildUrl ?? '';
Log.log(`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`);
Log.log(`${link(url)}`);
const url = build.artifacts?.buildUrl;
if (url) {
Log.addNewLineIfNone();
Log.log(
`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`
);
Log.log(link(url));
}
}
}
@@ -105,9 +137,22 @@ export function printDeprecationWarnings(deprecationInfo?: EasBuildDeprecationIn
}
}
export function printUserError(error: BuildError): void {
Log.error(error.message);
if (error.docsUrl) {
Log.error(learnMore(error.docsUrl, { dim: false }));
export function printUserError(error: BuildError, build: BuildFragment): void {
const maybeErrorMessageOverride = maybeGetErrorMessageOverride(error, build);
if (maybeErrorMessageOverride) {
Log.error(maybeErrorMessageOverride);
} else {
Log.error(error.message);
if (error.docsUrl) {
Log.error(learnMore(error.docsUrl, { dim: false }));
}
}
}
function maybeGetErrorMessageOverride(error: BuildError, build: BuildFragment): string | null {
if (!(error.errorCode in errorCodeToErrorMessageOverride)) {
return null;
}
return errorCodeToErrorMessageOverride[error.errorCode](build);
}
@@ -0,0 +1,123 @@
import { ExpoConfig } from '@expo/config-types';
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, ResourceClass } from '@expo/eas-json';
import chalk from 'chalk';
import semver from 'semver';
import { BuildResourceClass } from '../../graphql/generated';
import Log from '../../log';
import { getReactNativeVersionAsync } from '../metadata';
type AndroidResourceClass = Exclude<
ResourceClass,
| ResourceClass.M1_EXPERIMENTAL
| ResourceClass.M1_MEDIUM
| ResourceClass.M1_LARGE
| ResourceClass.INTEL_MEDIUM
| ResourceClass.M_MEDIUM
| ResourceClass.M_LARGE
>;
const iosResourceClassToBuildResourceClassMapping: Record<ResourceClass, BuildResourceClass> = {
[ResourceClass.DEFAULT]: BuildResourceClass.IosDefault,
[ResourceClass.LARGE]: BuildResourceClass.IosLarge,
[ResourceClass.M1_EXPERIMENTAL]: BuildResourceClass.IosMMedium,
[ResourceClass.M1_MEDIUM]: BuildResourceClass.IosMMedium,
[ResourceClass.M1_LARGE]: BuildResourceClass.IosMLarge,
[ResourceClass.INTEL_MEDIUM]: BuildResourceClass.IosIntelMedium,
[ResourceClass.MEDIUM]: BuildResourceClass.IosMedium,
[ResourceClass.M_MEDIUM]: BuildResourceClass.IosMMedium,
[ResourceClass.M_LARGE]: BuildResourceClass.IosMLarge,
};
const androidResourceClassToBuildResourceClassMapping: Record<
AndroidResourceClass,
BuildResourceClass
> = {
[ResourceClass.DEFAULT]: BuildResourceClass.AndroidDefault,
[ResourceClass.LARGE]: BuildResourceClass.AndroidLarge,
[ResourceClass.MEDIUM]: BuildResourceClass.AndroidMedium,
};
export async function resolveBuildResourceClassAsync<T extends Platform>(
profile: BuildProfile<T>,
platform: Platform,
projectDir: string,
exp: ExpoConfig,
resourceClassFlag?: ResourceClass
): Promise<BuildResourceClass> {
const profileResourceClass = profile.resourceClass;
if (profileResourceClass && resourceClassFlag && resourceClassFlag !== profileResourceClass) {
Log.warn(
`Build profile specifies the "${profileResourceClass}" resource class but you passed "${resourceClassFlag}" to --resource-class.\nUsing the "${resourceClassFlag}" as the override.`
);
}
const selectedResourceClass = resourceClassFlag ?? profileResourceClass;
return platform === Platform.IOS
? await resolveIosResourceClassAsync(exp, projectDir, resourceClassFlag ?? profileResourceClass)
: resolveAndroidResourceClass(selectedResourceClass);
}
function resolveAndroidResourceClass(selectedResourceClass?: ResourceClass): BuildResourceClass {
if (
selectedResourceClass &&
[
ResourceClass.M1_EXPERIMENTAL,
ResourceClass.M1_MEDIUM,
ResourceClass.M1_LARGE,
ResourceClass.INTEL_MEDIUM,
].includes(selectedResourceClass)
) {
throw new Error(`Resource class ${selectedResourceClass} is only available for iOS builds`);
}
const resourceClass = selectedResourceClass ?? ResourceClass.DEFAULT;
return androidResourceClassToBuildResourceClassMapping[resourceClass as AndroidResourceClass];
}
async function resolveIosResourceClassAsync(
exp: ExpoConfig,
projectDir: string,
selectedResourceClass?: ResourceClass
): Promise<BuildResourceClass> {
const resourceClass =
selectedResourceClass ?? (await resolveIosDefaultRequestedResourceClassAsync(exp, projectDir));
if ([ResourceClass.M1_EXPERIMENTAL, ResourceClass.M1_MEDIUM].includes(resourceClass)) {
Log.warn(
`Resource class ${chalk.bold(resourceClass)} is deprecated. Use ${chalk.bold(
'm-medium'
)} instead.`
);
}
if (resourceClass === ResourceClass.M1_LARGE) {
Log.warn(
`Resource class ${chalk.bold('m1-large')} is deprecated. Use ${chalk.bold(
'm-large'
)} instead.`
);
}
return iosResourceClassToBuildResourceClassMapping[resourceClass];
}
async function resolveIosDefaultRequestedResourceClassAsync(
exp: ExpoConfig,
projectDir: string
): Promise<ResourceClass> {
const { sdkVersion } = exp;
const reactNativeVersion = await getReactNativeVersionAsync(projectDir);
if (
(sdkVersion && semver.satisfies(sdkVersion, '>=48')) ||
(reactNativeVersion && semver.satisfies(reactNativeVersion, '>=0.71.0'))
) {
return ResourceClass.M_MEDIUM;
} else {
return ResourceClass.DEFAULT;
}
}
+4 -2
View File
@@ -10,12 +10,14 @@ export function getProjectDashboardUrl(accountName: string, projectName: string)
).toString();
}
export function getBuildLogsUrl(build: BuildFragment): string {
export function getBuildLogsUrl(build: BuildFragment, hash?: string): string {
const { project } = build;
const url =
project.__typename !== 'App'
? `/builds/${build.id}`
: `/accounts/${project.ownerAccount.name}/projects/${project.slug}/builds/${build.id}`;
: `/accounts/${project.ownerAccount.name}/projects/${project.slug}/builds/${build.id}${
hash ? `#${hash}` : ''
}`;
return new URL(url, getExpoWebsiteBaseUrl()).toString();
}
+1 -1
View File
@@ -46,7 +46,7 @@ export function getBranchMapping(branchMappingString?: string): {
}
const isRollout = branchMapping.data.length === 2;
const rolloutPercent = branchMapping.data[0].branchMappingLogic.operand;
const rolloutPercent = branchMapping.data[0]?.branchMappingLogic.operand;
switch (branchMapping.data.length) {
case 0:
+32 -10
View File
@@ -1,4 +1,5 @@
import { Command } from '@oclif/core';
import { CombinedError } from '@urql/core';
import nullthrows from 'nullthrows';
import {
@@ -6,6 +7,7 @@ import {
CommandEvent,
createAnalyticsAsync,
} from '../analytics/AnalyticsManager';
import Log from '../log';
import SessionManager from '../user/SessionManager';
import AnalyticsContextField from './context/AnalyticsContextField';
import ContextField from './context/ContextField';
@@ -16,6 +18,7 @@ import { OptionalProjectConfigContextField } from './context/OptionalProjectConf
import ProjectConfigContextField from './context/ProjectConfigContextField';
import ProjectDirContextField from './context/ProjectDirContextField';
import SessionManagementContextField from './context/SessionManagementContextField';
import { EasCommandError } from './errors';
type ContextInput<
T extends {
@@ -33,6 +36,8 @@ type ContextOutput<
[P in keyof T]: T[P];
};
const BASE_GRAPHQL_ERROR_MESSAGE: string = 'GraphQL request failed.';
export default abstract class EasCommand extends Command {
protected static readonly ContextOptions = {
/**
@@ -103,6 +108,18 @@ export default abstract class EasCommand extends Command {
*/
static contextDefinition: ContextInput = {};
/**
* The user session manager. Responsible for coordinating all user session related state.
* If needed in a subclass, use the SessionManager ContextOption.
*/
private sessionManagerInternal?: SessionManager;
/**
* The analytics manager. Used for logging analytics.
* It is set up here to ensure a consistent setup.
*/
private analyticsInternal?: AnalyticsWithOrchestration;
/**
* Execute the context in the contextDefinition to satisfy command prerequisites.
*/
@@ -132,20 +149,10 @@ export default abstract class EasCommand extends Command {
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);
}
@@ -175,4 +182,19 @@ export default abstract class EasCommand extends Command {
await this.analytics.flushAsync();
return super.finally(err);
}
protected override catch(err: Error): Promise<any> {
let baseMessage = `${this.id} command failed.`;
if (err instanceof EasCommandError) {
Log.error(err.message);
} else if (err instanceof CombinedError && err?.graphQLErrors) {
const cleanMessage = err.message.replace('[GraphQL] ', '');
Log.error(cleanMessage);
baseMessage = BASE_GRAPHQL_ERROR_MESSAGE;
} else {
Log.error(err.message);
}
Log.debug(err);
throw new Error(baseMessage);
}
}
@@ -1,4 +1,7 @@
import { CombinedError } from '@urql/core';
import { AnalyticsWithOrchestration, createAnalyticsAsync } from '../../analytics/AnalyticsManager';
import Log from '../../log';
import SessionManager from '../../user/SessionManager';
import EasCommand from '../EasCommand';
@@ -11,6 +14,7 @@ jest.mock('../../analytics/AnalyticsManager', () => {
createAnalyticsAsync: jest.fn(),
};
});
jest.mock('../../log');
let originalProcessArgv: string[];
@@ -40,7 +44,7 @@ const createTestEasCommand = (): typeof EasCommand => {
async runAsync(): Promise<void> {}
}
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
TestEasCommand.id = 'testEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
return TestEasCommand;
};
@@ -96,5 +100,71 @@ describe(EasCommand.name, () => {
expect(analytics.flushAsync).toHaveBeenCalled();
});
describe('catch', () => {
it('logs the message', async () => {
const TestEasCommand = createTestEasCommand();
const logErrorSpy = jest.spyOn(Log, 'error');
const logDebugSpy = jest.spyOn(Log, 'debug');
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
const error = new Error('Unexpected, internal error message');
runAsyncMock.mockImplementation(() => {
throw error;
});
try {
await TestEasCommand.run();
} catch {}
expect(logErrorSpy).toBeCalledWith('Unexpected, internal error message');
expect(logDebugSpy).toBeCalledWith(error);
});
it('logs the cleaned message if needed', async () => {
const TestEasCommand = createTestEasCommand();
const logErrorSpy = jest.spyOn(Log, 'error');
const logDebugSpy = jest.spyOn(Log, 'debug');
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
const graphQLErrors = ['Unexpected GraphQL error message'];
const error = new CombinedError({ graphQLErrors });
runAsyncMock.mockImplementation(() => {
throw error;
});
try {
await TestEasCommand.run();
} catch {}
expect(logErrorSpy).toBeCalledWith('Unexpected GraphQL error message');
expect(logDebugSpy).toBeCalledWith(error);
});
it('re-throws the error with default base message', async () => {
const TestEasCommand = createTestEasCommand();
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
runAsyncMock.mockImplementation(() => {
throw new Error('Error message');
});
try {
await TestEasCommand.run();
} catch (caughtError) {
expect(caughtError).toBeInstanceOf(Error);
expect((caughtError as Error).message).toEqual('testEasCommand command failed.');
}
});
it('re-throws the error with a different default base message in case of graphQLError', async () => {
const TestEasCommand = createTestEasCommand();
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
runAsyncMock.mockImplementation(() => {
const graphQLErrors = ['Unexpected GraphQL error message'];
throw new CombinedError({ graphQLErrors });
});
try {
await TestEasCommand.run();
} catch (caughtError) {
expect(caughtError).toBeInstanceOf(Error);
expect((caughtError as Error).message).toEqual('GraphQL request failed.');
}
});
});
});
});
@@ -5,7 +5,7 @@ import ContextField, { ContextOptions } from './ContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
export type DynamicConfigContextFn = (options?: ExpoConfigOptions) => Promise<{
export type DynamicConfigContextFn = (options: ExpoConfigOptions) => Promise<{
projectId: string;
exp: ExpoConfig;
projectDir: string;
@@ -16,8 +16,8 @@ export class DynamicProjectConfigContextField extends ContextField<DynamicConfig
nonInteractive,
sessionManager,
}: ContextOptions): Promise<DynamicConfigContextFn> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
return async (options?: ExpoConfigOptions) => {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
const expBefore = getExpoConfig(projectDir, options);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
@@ -15,7 +15,6 @@ jest.mock('fs');
jest.mock('../../../../graphql/queries/AppQuery');
jest.mock('../../contextUtils/findProjectDirAndVerifyProjectSetupAsync');
jest.mock('../../../../user/User');
jest.mock('../../../../ora', () => ({
ora: () => ({
start: () => ({ succeed: () => {}, fail: () => {} }),
@@ -107,7 +106,79 @@ describe(getProjectIdAsync, () => {
{ 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(
`Project config: Owner of project identified by "extra.eas.projectId" (notnotbrent) does not match owner specified in the "owner" field (wat). ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
});
it('throws when the owner is not specified and is different than logged in user', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) does not match the logged in user (notnotbrent) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
});
it('throws when the owner is not specified and is different than logged in user which is a robot', async () => {
const sessionManagerMock = mock<SessionManager>();
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
actor: {
__typename: 'Robot',
id: 'robot_id',
accounts: [
{
id: 'account_id_1',
name: 'notnotbrent',
users: [{ role: Role.Admin, actor: { id: 'robot_id' } }],
},
{
id: 'account_id_2',
name: 'dominik',
users: [{ role: Role.ViewOnly, actor: { id: 'robot_id' } }],
},
],
isExpoAdmin: false,
featureGates: {},
},
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
});
const sessionManagerRobot = instance(sessionManagerMock);
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManagerRobot,
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
@@ -12,7 +12,7 @@ import { getVcsClient, setVcsClient } from '../../../vcs';
import GitClient from '../../../vcs/clients/git';
async function applyCliConfigAsync(projectDir: string): Promise<void> {
const easJsonAccessor = new EasJsonAccessor(projectDir);
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
throw new Error(
@@ -10,7 +10,7 @@ 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 { Actor, getActorUsername } from '../../../user/User';
import { createGraphqlClient } from './createGraphqlClient';
import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync';
@@ -93,14 +93,40 @@ export async function getProjectIdAsync(
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" (${
`Project config: Owner of project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) is not owned by owner specified in the "owner" field (${exp.owner}). ${learnMore(
}) does not match owner specified in the "owner" field (${exp.owner}). ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
}
const actorUsername = getActorUsername(actor);
if (!exp.owner && appForProjectId.ownerAccount.name !== actorUsername) {
if (actorUsername) {
throw new Error(
`Project config: Owner of project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) does not match the logged in user (${actorUsername}) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "${
appForProjectId.ownerAccount.name
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
} else {
// robot caller
throw new Error(
`Project config: Owner of project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "${
appForProjectId.ownerAccount.name
}" should be added to the project config, which can be done automatically by re-running "eas init". ${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" (${
@@ -0,0 +1,7 @@
export class EasCommandError extends Error {
// constructor is not useless, since the constructor for Error allows for optional `message`
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor(message: string) {
super(message);
}
}
+13 -7
View File
@@ -6,6 +6,7 @@ import figures from 'figures';
import fs from 'fs-extra';
import path from 'path';
import { LocalBuildMode } from '../../build/local';
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
@@ -126,7 +127,7 @@ export default class Build extends EasCommand {
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
if (!flags.localBuildOptions.enable) {
if (!flags.localBuildOptions.localBuildMode) {
await maybeWarnAboutEasOutagesAsync(
graphqlClient,
flags.autoSubmit
@@ -194,13 +195,11 @@ export default class Build extends EasCommand {
nonInteractive,
localBuildOptions: flags['local']
? {
enable: true,
localBuildMode: LocalBuildMode.LOCAL_BUILD_PLUGIN,
verbose: true,
artifactPath: flags.output && path.resolve(process.cwd(), flags.output),
}
: {
enable: false,
},
: {},
wait: flags['wait'],
clearCache: flags['clear-cache'],
json: flags['json'],
@@ -216,7 +215,7 @@ export default class Build extends EasCommand {
): Promise<BuildFlags> {
const requestedPlatform = await selectRequestedPlatformAsync(flags.requestedPlatform);
if (flags.localBuildOptions.enable) {
if (flags.localBuildOptions.localBuildMode) {
if (flags.autoSubmit) {
// TODO: implement this
Errors.error('Auto-submits are not yet supported when building locally', { exit: 1 });
@@ -228,6 +227,13 @@ export default class Build extends EasCommand {
});
} else if (process.platform !== 'darwin' && requestedPlatform === RequestedPlatform.Ios) {
Errors.error('Unsupported platform, macOS is required to build apps for iOS', { exit: 1 });
} else if (
!['linux', 'darwin'].includes(process.platform) &&
requestedPlatform === RequestedPlatform.Android
) {
Errors.error('Unsupported platform, macOS or Linux is required to build apps for Android', {
exit: 1,
});
}
}
@@ -245,7 +251,7 @@ export async function handleDeprecatedEasJsonAsync(
if (!(await fs.pathExists(EasJsonAccessor.formatEasJsonPath(projectDir)))) {
return;
}
const easJsonAccessor = new EasJsonAccessor(projectDir);
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const profileNames = await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor);
const platformAndProfileNames: [Platform, string][] = profileNames.flatMap(profileName => [
[Platform.ANDROID, profileName],
@@ -4,6 +4,7 @@ import fs from 'fs-extra';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { LocalBuildMode } from '../../build/local';
import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import EasCommand from '../../commandUtils/EasCommand';
import Log from '../../log';
@@ -110,7 +111,7 @@ export default class BuildInspect extends EasCommand {
requestedPlatform: flags.platform,
profile: flags.profile,
localBuildOptions: {
enable: true,
localBuildMode: LocalBuildMode.LOCAL_BUILD_PLUGIN,
...(flags.stage === InspectStage.PRE_BUILD ? { skipNativeBuild: true } : {}),
...(flags.stage === InspectStage.POST_BUILD ? { skipCleanup: true } : {}),
verbose: flags.verbose,
@@ -0,0 +1,80 @@
import { Flags } from '@oclif/core';
import { handleDeprecatedEasJsonAsync } from '.';
import { LocalBuildMode } from '../../build/local';
import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import EasCommand from '../../commandUtils/EasCommand';
import { RequestedPlatform } from '../../platform';
import { enableJsonOutput } from '../../utils/json';
import { setVcsClient } from '../../vcs';
import GitNoCommitClient from '../../vcs/clients/gitNoCommit';
/**
* This command will be run on the EAS Build workers, when building
* directly from git. This command resolves credentials and other
* build configuration, that normally would be included in the
* job and metadata objects, and prints them to stdout.
*/
export default class BuildInternal extends EasCommand {
static override hidden = true;
static override flags = {
platform: Flags.enum({
char: 'p',
options: ['android', 'ios'],
required: true,
}),
profile: Flags.string({
char: 'e',
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
}),
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Analytics,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildInternal);
// This command is always run with implicit --non-interactive and --json options
enableJsonOutput();
const {
loggedIn: { actor, graphqlClient },
getDynamicProjectConfigAsync,
projectDir,
analytics,
} = await this.getContextAsync(BuildInternal, {
nonInteractive: true,
});
setVcsClient(new GitNoCommitClient());
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await runBuildAndSubmitAsync(
graphqlClient,
analytics,
projectDir,
{
requestedPlatform: flags.platform as RequestedPlatform,
profile: flags.profile,
nonInteractive: true,
wait: false,
clearCache: false,
json: true,
autoSubmit: false,
localBuildOptions: {
localBuildMode: LocalBuildMode.INTERNAL,
},
},
actor,
getDynamicProjectConfigAsync
);
}
}
@@ -121,7 +121,7 @@ export default class BuildResign extends EasCommand {
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasBuild]);
const easJsonAccessor = new EasJsonAccessor(projectDir);
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
@@ -199,6 +199,8 @@ export default class BuildResign extends EasCommand {
const buildResult = await waitForBuildEndAsync(graphqlClient, {
buildIds: [newBuild.id],
accountName: account.name,
nonInteractive,
projectDir,
});
if (!flags.json) {
printBuildResults(buildResult);
@@ -46,7 +46,7 @@ export default class BuildVersionSetView extends EasCommand {
public async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildVersionSetView);
const {
loggedIn: { actor, graphqlClient },
loggedIn: { graphqlClient },
getDynamicProjectConfigAsync,
projectDir,
} = await this.getContextAsync(BuildVersionSetView, {
@@ -54,7 +54,7 @@ export default class BuildVersionSetView extends EasCommand {
});
const platform = await selectPlatformAsync(flags.platform);
const easJsonAccessor = new EasJsonAccessor(projectDir);
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
await ensureVersionSourceIsRemoteAsync(easJsonAccessor);
const profile = await EasJsonUtils.getBuildProfileAsync(
easJsonAccessor,
@@ -67,13 +67,14 @@ export default class BuildVersionSetView extends EasCommand {
validateAppConfigForRemoteVersionSource(exp, platform);
const applicationIdentifier = await getApplicationIdentifierAsync(
const applicationIdentifier = await getApplicationIdentifierAsync({
graphqlClient,
projectDir,
projectId,
exp,
profile,
buildProfile: profile,
platform,
actor
);
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
projectId,
@@ -63,7 +63,7 @@ export default class BuildVersionSyncView extends EasCommand {
public async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildVersionSyncView);
const {
loggedIn: { actor, graphqlClient },
loggedIn: { graphqlClient },
getDynamicProjectConfigAsync,
projectDir,
} = await this.getContextAsync(BuildVersionSyncView, {
@@ -71,7 +71,7 @@ export default class BuildVersionSyncView extends EasCommand {
});
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
const easJsonAccessor = new EasJsonAccessor(projectDir);
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
await ensureVersionSourceIsRemoteAsync(easJsonAccessor);
const platforms = toPlatforms(requestedPlatform);
@@ -89,13 +89,14 @@ export default class BuildVersionSyncView extends EasCommand {
validateAppConfigForRemoteVersionSource(exp, profileInfo.platform);
const platformDisplayName = appPlatformDisplayNames[toAppPlatform(profileInfo.platform)];
const applicationIdentifier = await getApplicationIdentifierAsync(
const applicationIdentifier = await getApplicationIdentifierAsync({
graphqlClient,
projectDir,
projectId,
exp,
profileInfo.profile,
profileInfo.platform,
actor
);
buildProfile: profileInfo.profile,
platform: profileInfo.platform,
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
projectId,
@@ -1,6 +1,8 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { selectBranchOnAppAsync } from '../../branch/queries';
import { selectChannelOnAppAsync } from '../../channel/queries';
import { BranchMapping, getBranchMapping } from '../../channel/utils';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
@@ -132,13 +134,11 @@ async function startRolloutAsync(
);
}
const logMessage = `️Started a rollout of branch ${chalk.bold(
const logMessage = `Started a rollout of branch ${chalk.bold(branchName)} on channel ${chalk.bold(
channelName!
)}! ${chalk.bold(percent)}% of users will be directed to branch ${chalk.bold(
branchName
)} onto channel ${chalk.bold(channelName!)}! ${chalk.bold(
percent
)}% of users will be directed to branch ${chalk.bold(branchName)}, ${chalk.bold(
100 - percent
)}% to branch ${chalk.bold(oldBranch.name)}.`;
)}, ${chalk.bold(100 - percent)}% to branch ${chalk.bold(oldBranch.name)}.`;
return { newChannelInfo, logMessage };
}
@@ -190,7 +190,7 @@ async function editRolloutAsync(
branchMapping: JSON.stringify(newBranchMapping),
});
const logMessage = `️Rollout of branch ${chalk.bold(newBranch.name)} onto channel ${chalk.bold(
const logMessage = `Rollout of branch ${chalk.bold(newBranch.name)} on channel ${chalk.bold(
channelName!
)} updated from ${chalk.bold(currentPercent)}% to ${chalk.bold(percent)}%. ${chalk.bold(
percent
@@ -284,7 +284,7 @@ async function endRolloutAsync(
channelId: channel.id,
branchMapping: JSON.stringify(newBranchMapping),
});
const logMessage = `️Rollout on channel ${chalk.bold(
const logMessage = `Rollout on channel ${chalk.bold(
channelName
)} ended. All traffic is now sent to branch ${chalk.bold(
endOnNewBranch ? newBranch.name : oldBranch.name
@@ -294,14 +294,12 @@ async function endRolloutAsync(
}
export default class ChannelRollout extends EasCommand {
static override hidden = true;
static override description = 'Rollout a new branch out to a channel incrementally.';
static override description = 'Roll a new branch out on a channel incrementally.';
static override args = [
{
name: 'channel',
required: true,
description: 'rollout that the channel is on',
description: 'channel on which the rollout should be done',
},
];
@@ -311,7 +309,7 @@ export default class ChannelRollout extends EasCommand {
required: false,
}),
percent: Flags.integer({
description: 'percent of traffic to redirect to the new branch',
description: 'percent of users to send to the new branch',
required: false,
}),
end: Flags.boolean({
@@ -328,7 +326,7 @@ export default class ChannelRollout extends EasCommand {
async runAsync(): Promise<void> {
const {
args: { channel: channelName },
args: { channel: channelNameArg },
flags: {
json: jsonFlag,
end: endFlag,
@@ -349,9 +347,23 @@ export default class ChannelRollout extends EasCommand {
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
let channelName: string = channelNameArg;
if (!channelName) {
const { name } = await selectChannelOnAppAsync(graphqlClient, {
projectId,
selectionPromptTitle: 'Select a channel on which to perform a rollout',
paginatedQueryOptions: {
json: jsonFlag,
nonInteractive,
offset: 0,
},
});
channelName = name;
}
const channel = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
appId: projectId,
channelName: channelName!,
channelName,
});
if (!channel) {
throw new Error(
@@ -396,7 +408,15 @@ export default class ChannelRollout extends EasCommand {
if (!isRollout) {
rolloutMutationResult = await startRolloutAsync(graphqlClient, {
channelName,
branchName: branchName ?? (await promptForBranchNameAsync(channelName, nonInteractive)),
branchName:
branchName ??
(await promptForBranchNameAsync({
graphqlClient,
projectId,
channelName,
nonInteractive,
json: jsonFlag,
})),
percent,
nonInteractive,
projectId,
@@ -433,18 +453,36 @@ export default class ChannelRollout extends EasCommand {
}
}
async function promptForBranchNameAsync(
channelName: string,
nonInteractive: boolean
): Promise<string> {
async function promptForBranchNameAsync({
graphqlClient,
projectId,
channelName,
nonInteractive,
json,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
channelName: string;
nonInteractive: boolean;
json: boolean;
}): Promise<string> {
if (nonInteractive) {
throw new Error('Must supply branch flag in non-interactive mode');
}
const { name } = await promptAsync({
type: 'text',
name: 'name',
message: `Select a branch to rollout onto ${channelName}`,
validate: value => (value ? true : 'A branch must be specified.'),
const { name: branchName } = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: `Which branch would you like roll out on ${channelName}?`,
displayTextForListItem: updateBranch => ({
title: updateBranch.name,
}),
// discard limit and offset because this query is not their intended target
paginatedQueryOptions: {
json,
nonInteractive,
offset: 0,
},
});
return name;
return branchName;
}
+42 -16
View File
@@ -5,10 +5,12 @@ import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../commandUtils/flags';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log from '../log';
import { appPlatformEmojis } from '../platform';
import { selectAsync } from '../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../utils/json';
export default class Config extends EasCommand {
static override description = 'display project configuration (app.json + eas.json)';
@@ -21,6 +23,11 @@ export default class Config extends EasCommand {
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
}),
// This option is used only on EAS Build worker to read build profile from eas.json.
'eas-json-only': Flags.boolean({
hidden: true,
}),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
@@ -30,12 +37,15 @@ export default class Config extends EasCommand {
async runAsync(): Promise<void> {
const { flags } = await this.parse(Config);
if (flags.json) {
enableJsonOutput();
}
const { platform: maybePlatform, profile: maybeProfile } = flags;
const { getDynamicProjectConfigAsync, projectDir } = await this.getContextAsync(Config, {
nonInteractive: false,
});
const accessor = new EasJsonAccessor(projectDir);
const accessor = EasJsonAccessor.fromProjectPath(projectDir);
const profileName =
maybeProfile ??
(await selectAsync(
@@ -61,21 +71,37 @@ export default class Config extends EasCommand {
]));
const profile = await EasJsonUtils.getBuildProfileAsync(accessor, platform, profileName);
const { exp: config } = await getDynamicProjectConfigAsync({
env: profile.env,
isPublicConfig: true,
});
if (flags['eas-json-only']) {
if (flags.json) {
printJsonOnlyOutput({ buildProfile: profile });
} else {
const appPlatform = toAppPlatform(platform);
const platformEmoji = appPlatformEmojis[appPlatform];
Log.log(`${platformEmoji} ${chalk.bold(`Build profile "${profileName}"`)}`);
Log.newLine();
Log.log(JSON.stringify(profile, null, 2));
}
} else {
const { exp: appConfig } = await getDynamicProjectConfigAsync({
env: profile.env,
isPublicConfig: true,
});
Log.addNewLineIfNone();
Log.log(chalk.bold(getProjectConfigDescription(projectDir)));
Log.newLine();
Log.log(JSON.stringify(config, null, 2));
Log.newLine();
Log.newLine();
const appPlatform = toAppPlatform(platform);
const platformEmoji = appPlatformEmojis[appPlatform];
Log.log(`${platformEmoji} ${chalk.bold(`Build profile "${profileName}"`)}`);
Log.newLine();
Log.log(JSON.stringify(profile, null, 2));
if (flags.json) {
printJsonOnlyOutput({ buildProfile: profile, appConfig });
} else {
Log.addNewLineIfNone();
Log.log(chalk.bold(getProjectConfigDescription(projectDir)));
Log.newLine();
Log.log(JSON.stringify(appConfig, null, 2));
Log.newLine();
Log.newLine();
const appPlatform = toAppPlatform(platform);
const platformEmoji = appPlatformEmojis[appPlatform];
Log.log(`${platformEmoji} ${chalk.bold(`Build profile "${profileName}"`)}`);
Log.newLine();
Log.log(JSON.stringify(profile, null, 2));
}
}
}
}
@@ -44,7 +44,7 @@ export default class MetadataLint extends EasCommand {
const submitProfiles = await getProfilesAsync({
type: 'submit',
easJsonAccessor: new EasJsonAccessor(projectDir),
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
platforms: [Platform.IOS],
profileName: flags.profile,
});
@@ -46,7 +46,7 @@ export default class MetadataPull extends EasCommand {
const submitProfiles = await getProfilesAsync({
type: 'submit',
easJsonAccessor: new EasJsonAccessor(projectDir),
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
platforms: [Platform.IOS],
profileName: flags.profile,
});
@@ -44,7 +44,7 @@ export default class MetadataPush extends EasCommand {
const submitProfiles = await getProfilesAsync({
type: 'submit',
easJsonAccessor: new EasJsonAccessor(projectDir),
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
platforms: [Platform.IOS],
profileName: flags.profile,
});
@@ -344,12 +344,38 @@ describe(ProjectInit.name, () => {
describe('when it is already configured', () => {
beforeEach(() => {
mockTestProject({ configuredProjectId: '1234', configuredOwner: jester.accounts[0].name });
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
});
it('does not configure', async () => {
await new ProjectInit([], commandOptions).run();
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
});
it('checks owner and slug consistency', async () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-124',
fullName: '@jester2/testing-124',
ownerAccount: jester2.accounts[0],
});
jest.mocked(confirmAsync).mockResolvedValue(true);
jest.mocked(modifyConfigAsync).mockResolvedValue({ type: 'success', config: {} as any });
await new ProjectInit([], commandOptions).run();
expect(confirmAsync).toHaveBeenCalled();
expect(modifyConfigAsync).toHaveBeenCalledTimes(2);
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { owner: 'jester2' });
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { slug: 'testing-124' });
});
});
describe('when it is not yet configured', () => {
@@ -365,6 +391,14 @@ describe(ProjectInit.name, () => {
it('prompts for confirmation to link', async () => {
jest.mocked(confirmAsync).mockResolvedValue(true);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
await new ProjectInit([], commandOptions).run();
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '123456');
expect(confirmAsync).toHaveBeenCalledTimes(1);
@@ -384,6 +418,14 @@ describe(ProjectInit.name, () => {
it('asks to create it', async () => {
jest.mocked(confirmAsync).mockResolvedValue(true);
jest.mocked(AppMutation.createAppAsync).mockResolvedValue('0129');
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '0129',
slug: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
await new ProjectInit([], commandOptions).run();
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '0129');
expect(confirmAsync).toHaveBeenCalledTimes(1);
+19 -12
View File
@@ -194,7 +194,6 @@ export default class ProjectInit extends EasCommand {
}
private static async initializeWithExplicitIDAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
projectDir: string,
{ force, nonInteractive }: InitializeMethodOptions
@@ -203,17 +202,13 @@ export default class ProjectInit extends EasCommand {
force,
nonInteractive,
});
await this.ensureOwnerSlugConsistencyAsync(graphqlClient, projectId, projectDir, {
force,
nonInteractive,
});
}
private static async initializeWithInteractiveSelectionAsync(
graphqlClient: ExpoGraphqlClient,
actor: Actor,
projectDir: string
): Promise<void> {
): Promise<string> {
const exp = getExpoConfig(projectDir);
const existingProjectId = exp.extra?.eas?.projectId;
@@ -223,7 +218,7 @@ export default class ProjectInit extends EasCommand {
existingProjectId
)}). To re-configure, remove the "extra.eas.projectId" field from your app config.`
);
return;
return existingProjectId;
}
const allAccounts = actor.accounts;
@@ -287,7 +282,7 @@ export default class ProjectInit extends EasCommand {
}
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, existingProjectIdOnServer);
return;
return existingProjectIdOnServer;
}
if (!accountNamesWhereUserHasSufficientPermissionsToCreateApp.has(accountName)) {
@@ -323,24 +318,36 @@ export default class ProjectInit extends EasCommand {
}
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, createdProjectId);
return createdProjectId;
}
async runAsync(): Promise<void> {
const {
flags: { id, force, 'non-interactive': nonInteractive },
flags: { id: idArgument, force, 'non-interactive': nonInteractive },
} = await this.parse(ProjectInit);
const {
loggedIn: { actor, graphqlClient },
projectDir,
} = await this.getContextAsync(ProjectInit, { nonInteractive });
if (id) {
await ProjectInit.initializeWithExplicitIDAsync(graphqlClient, id, projectDir, {
let idForConsistency: string;
if (idArgument) {
await ProjectInit.initializeWithExplicitIDAsync(idArgument, projectDir, {
force,
nonInteractive,
});
idForConsistency = idArgument;
} else {
await ProjectInit.initializeWithInteractiveSelectionAsync(graphqlClient, actor, projectDir);
idForConsistency = await ProjectInit.initializeWithInteractiveSelectionAsync(
graphqlClient,
actor,
projectDir
);
}
await ProjectInit.ensureOwnerSlugConsistencyAsync(graphqlClient, idForConsistency, projectDir, {
force,
nonInteractive,
});
}
}
+1 -1
View File
@@ -115,7 +115,7 @@ export default class Submit extends EasCommand {
const platforms = toPlatforms(flagsWithPlatform.requestedPlatform);
const submissionProfiles = await getProfilesAsync({
type: 'submit',
easJsonAccessor: new EasJsonAccessor(projectDir),
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
platforms,
profileName: flagsWithPlatform.profile,
});
@@ -1,6 +1,14 @@
import { AppJSONConfig, PackageJSONConfig, Platform, getConfig } from '@expo/config';
import {
AppJSONConfig,
ExpoConfig,
GetConfigOptions,
PackageJSONConfig,
Platform,
getConfig,
} from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { vol } from 'memfs';
import nullthrows from 'nullthrows';
import path from 'path';
import { instance, mock } from 'ts-mockito';
@@ -29,6 +37,7 @@ const updateStub: UpdateFragment = {
platform: 'ios',
gitCommitHash: 'commit',
manifestFragment: JSON.stringify({ fake: 'manifest' }),
isRollBackToEmbedded: false,
manifestPermalink: 'https://expo.dev/fake/manifest/link',
codeSigningInfo: null,
createdAt: '2022-01-01T12:00:00Z',
@@ -46,7 +55,7 @@ jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/UpdateQuery');
jest.mock('../../../ora', () => ({
ora: () => ({
start: () => ({ succeed: () => {}, fail: () => {} }),
start: () => ({ succeed: () => {}, fail: () => {}, stop: () => {} }),
}),
}));
jest.mock('../../../project/publish', () => ({
@@ -58,7 +67,10 @@ jest.mock('../../../project/publish', () => ({
}));
describe(UpdatePublish.name, () => {
afterEach(() => vol.reset());
afterEach(() => {
vol.reset();
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockClear();
});
// Deprecated and split to a new command: update:republish
it('errors with --republish', async () => {
@@ -80,7 +92,7 @@ describe(UpdatePublish.name, () => {
mockTestProject();
await expect(new UpdatePublish(flags, commandOptions).run()).rejects.toThrow(
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto'
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto.'
);
});
@@ -136,14 +148,59 @@ describe(UpdatePublish.name, () => {
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
});
it('creates a new update with the public expo config', async () => {
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
// Add configuration to the project that should not be included in the update
const { appJson } = mockTestProject({
expoConfig: {
hooks: {
postPublish: [
{
file: 'custom-hook.js',
config: { some: 'config' },
},
],
},
},
});
const { platforms, runtimeVersion } = mockTestExport({ platforms: ['ios'] });
// Mock an existing branch, so we don't create a new one
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
createdBranch: false,
});
jest
.mocked(PublishMutation.publishUpdateGroupAsync)
.mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion })));
await new UpdatePublish(flags, commandOptions).run();
// Pull the publish data from the mocked publish function
const publishData = jest.mocked(PublishMutation.publishUpdateGroupAsync).mock.calls[0][1][0];
// Pull the Expo config from the publish data
const expoConfig = nullthrows(publishData.updateInfoGroup).ios!.extra.expoClient;
// Ensure the basic properties are present
expect(expoConfig).toHaveProperty('name', appJson.expo.name);
expect(expoConfig).toHaveProperty('slug', appJson.expo.slug);
// Ensure non-public config is not present
expect(expoConfig).not.toHaveProperty('hooks');
});
});
/** Create a new in-memory project, copied from src/commands/update/__tests__/republish.test.ts */
function mockTestProject({
configuredProjectId = '1234',
expoConfig = {},
}: {
configuredProjectId?: string;
} = {}): { projectId: string } {
expoConfig?: Partial<ExpoConfig>;
} = {}): { projectId: string; appJson: AppJSONConfig } {
const packageJSON: PackageJSONConfig = {
name: 'testing123',
version: '0.1.0',
@@ -163,6 +220,7 @@ function mockTestProject({
projectId: configuredProjectId,
},
},
...expoConfig,
},
};
@@ -180,11 +238,27 @@ function mockTestProject({
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
jest
.spyOn(DynamicProjectConfigContextField.prototype, 'getValueAsync')
.mockResolvedValue(async () => ({
exp: mockManifest.exp,
projectDir: projectRoot,
projectId: configuredProjectId,
}));
.mockResolvedValue(async (options?: GetConfigOptions) => {
let exp = { ...mockManifest.exp };
// Fake a limited public config
if (options?.isPublicConfig) {
exp = {
name: mockManifest.exp.name,
version: mockManifest.exp.version,
slug: mockManifest.exp.slug,
sdkVersion: mockManifest.exp.sdkVersion,
owner: mockManifest.exp.owner,
extra: mockManifest.exp.extra,
};
}
return {
exp,
projectDir: projectRoot,
projectId: configuredProjectId,
};
});
jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
actor: jester,
@@ -199,7 +273,7 @@ function mockTestProject({
ownerAccount: jester.accounts[0],
});
return { projectId: configuredProjectId };
return { projectId: configuredProjectId, appJson: appJSON };
}
/** Create a new in-memory export of the project */
@@ -246,6 +320,8 @@ function mockTestExport({
uniqueAssetCount: platforms.length,
uniqueUploadedAssetCount: platforms.length,
assetLimitPerUpdateGroup: 9001,
launchAssetCount: 2,
uniqueUploadedAssetPaths: [],
});
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
@@ -27,6 +27,7 @@ const updateStub: UpdateFragment = {
platform: 'ios',
gitCommitHash: 'commit',
manifestFragment: JSON.stringify({ fake: 'manifest' }),
isRollBackToEmbedded: false,
manifestPermalink: 'https://expo.dev/fake/manifest/link',
codeSigningInfo: null,
createdAt: '2022-01-01T12:00:00Z',
@@ -0,0 +1,257 @@
import {
AppJSONConfig,
ExpoConfig,
GetConfigOptions,
PackageJSONConfig,
getConfig,
} from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { vol } from 'memfs';
import nullthrows from 'nullthrows';
import path from 'path';
import { instance, mock } from 'ts-mockito';
import { ensureBranchExistsAsync } from '../../../branch/queries';
import { DynamicProjectConfigContextField } from '../../../commandUtils/context/DynamicProjectConfigContextField';
import LoggedInContextField from '../../../commandUtils/context/LoggedInContextField';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
import { jester } from '../../../credentials/__tests__/fixtures-constants';
import { UpdateFragment } from '../../../graphql/generated';
import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchNameFromChannelNameAsync';
import UpdateRollBackToEmbedded from '../roll-back-to-embedded';
const projectRoot = '/test-project';
const commandOptions = { root: projectRoot } as any;
const updateStub: UpdateFragment = {
id: 'update-1234',
group: 'group-1234',
branch: { id: 'branch-1234', name: 'main' },
message: 'test message',
runtimeVersion: 'exposdk:47.0.0',
platform: 'ios',
gitCommitHash: 'commit',
manifestFragment: JSON.stringify({ fake: 'manifest' }),
isRollBackToEmbedded: false,
manifestPermalink: 'https://expo.dev/fake/manifest/link',
codeSigningInfo: null,
createdAt: '2022-01-01T12:00:00Z',
};
jest.mock('fs');
jest.mock('@expo/config');
jest.mock('@expo/config-plugins');
jest.mock('../../../branch/queries');
jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
jest.mock('../../../update/configure');
jest.mock('../../../update/getBranchNameFromChannelNameAsync');
jest.mock('../../../graphql/mutations/PublishMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/UpdateQuery');
jest.mock('../../../ora', () => ({
ora: () => ({
start: () => ({ succeed: () => {}, fail: () => {}, stop: () => {} }),
}),
}));
jest.mock('../../../project/publish', () => ({
...jest.requireActual('../../../project/publish'),
buildBundlesAsync: jest.fn(),
collectAssetsAsync: jest.fn(),
resolveInputDirectoryAsync: jest.fn((inputDir = 'dist') => path.join(projectRoot, inputDir)),
uploadAssetsAsync: jest.fn(),
}));
describe(UpdateRollBackToEmbedded.name, () => {
afterEach(() => vol.reset());
it('errors with both --channel and --branch', async () => {
const flags = ['--channel=channel123', '--branch=branch123'];
mockTestProject();
await expect(new UpdateRollBackToEmbedded(flags, commandOptions).run()).rejects.toThrow(
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto.'
);
});
it('creates a roll back to embedded with --non-interactive, --branch, and --message', async () => {
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
mockTestProject();
const platforms = ['android', 'ios'];
const runtimeVersion = 'exposdk:47.0.0';
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
createdBranch: false,
});
jest
.mocked(PublishMutation.publishUpdateGroupAsync)
.mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion })));
await new UpdateRollBackToEmbedded(flags, commandOptions).run();
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
});
it('creates a roll back to embedded with --non-interactive, --channel, and --message', async () => {
const flags = ['--non-interactive', '--channel=channel123', '--message=abc'];
const { projectId } = mockTestProject();
const platforms = ['android', 'ios'];
const runtimeVersion = 'exposdk:47.0.0';
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
jest.mocked(getBranchNameFromChannelNameAsync).mockResolvedValue('branchFromChannel');
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
createdBranch: false,
});
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue(
platforms.map(platform => ({
...updateStub,
runtimeVersion,
platform,
}))
);
await new UpdateRollBackToEmbedded(flags, commandOptions).run();
expect(ensureBranchExistsAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
{
appId: projectId,
branchName: 'branchFromChannel',
}
);
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
});
it('creates a roll back to embedded with the public expo config', async () => {
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
// Add configuration to the project that should not be included in the update
mockTestProject({
expoConfig: {
hooks: {
postPublish: [
{
file: 'custom-hook.js',
config: { some: 'config' },
},
],
},
},
});
const platforms = ['ios'];
const runtimeVersion = 'exposdk:47.0.0';
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
// Mock an existing branch, so we don't create a new one
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
createdBranch: false,
});
jest
.mocked(PublishMutation.publishUpdateGroupAsync)
.mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion })));
await new UpdateRollBackToEmbedded(flags, commandOptions).run();
// Pull the publish data from the mocked publish function
const publishData = jest.mocked(PublishMutation.publishUpdateGroupAsync).mock.calls[0][1][0];
expect(nullthrows(publishData.rollBackToEmbeddedInfoGroup).ios).toBe(true);
});
});
/** Create a new in-memory project, based on src/commands/project/__tests__/init.test.ts */
function mockTestProject({
configuredProjectId = '1234',
expoConfig = {},
}: {
configuredProjectId?: string;
expoConfig?: Partial<ExpoConfig>;
} = {}): { projectId: string; appJson: AppJSONConfig } {
const packageJSON: PackageJSONConfig = {
name: 'testing123',
version: '0.1.0',
description: 'fake description',
main: 'index.js',
};
const appJSON: AppJSONConfig = {
expo: {
name: 'testing 123',
version: '0.1.0',
slug: 'testing-123',
sdkVersion: '33.0.0',
owner: jester.accounts[0].name,
extra: {
eas: {
projectId: configuredProjectId,
},
},
...expoConfig,
},
};
vol.fromJSON(
{
'package.json': JSON.stringify(packageJSON),
'app.json': JSON.stringify(appJSON),
},
projectRoot
);
const mockManifest = { exp: appJSON.expo };
const graphqlClient = instance(mock<ExpoGraphqlClient>({}));
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
jest
.spyOn(DynamicProjectConfigContextField.prototype, 'getValueAsync')
.mockResolvedValue(async (options?: GetConfigOptions) => {
let exp = { ...mockManifest.exp };
// Fake a limited public config
if (options?.isPublicConfig) {
exp = {
name: mockManifest.exp.name,
version: mockManifest.exp.version,
slug: mockManifest.exp.slug,
sdkVersion: mockManifest.exp.sdkVersion,
owner: mockManifest.exp.owner,
extra: mockManifest.exp.extra,
};
}
return {
exp,
projectDir: projectRoot,
projectId: configuredProjectId,
};
});
jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
actor: jester,
featureGating: new FeatureGating({}, new FeatureGateEnvOverrides()),
graphqlClient,
});
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
return { projectId: configuredProjectId, appJson: appJSON };
}
+75 -169
View File
@@ -1,13 +1,9 @@
import { ExpoConfig, Platform as PublishPlatform } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Platform as PublishPlatform } from '@expo/config';
import { Errors, Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { ensureBranchExistsAsync, selectBranchOnAppAsync } from '../../branch/queries';
import { getDefaultBranchNameAsync } from '../../branch/utils';
import { ensureBranchExistsAsync } from '../../branch/queries';
import { getUpdateGroupUrl } from '../../build/utils/url';
import { ensureChannelExistsAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
@@ -23,32 +19,31 @@ import {
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import Log, { learnMore, link } from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform, requestedPlatformDisplayNames } from '../../platform';
import { RequestedPlatform } from '../../platform';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import {
ExpoCLIExportPlatformFlag,
buildBundlesAsync,
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
defaultPublishPlatforms,
filterExportedPlatformsByFlag,
getBranchNameForCommandAsync,
getRequestedPlatform,
getRuntimeToPlatformMappingFromRuntimeVersions,
getRuntimeVersionObjectAsync,
getUpdateMessageForCommandAsync,
isUploadedAssetCountAboveWarningThreshold,
resolveInputDirectoryAsync,
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
import { getBranchNameFromChannelNameAsync } from '../../update/getBranchNameFromChannelNameAsync';
import {
formatUpdateMessage,
getUpdateGroupJsonInfo,
truncateString as truncateUpdateMessage,
} from '../../update/utils';
import { getUpdateGroupJsonInfo } from '../../update/utils';
import {
checkManifestBodyAgainstUpdateInfoGroup,
getCodeSigningInfoAsync,
getManifestBodyAsync,
signManifestBody,
signBody,
} from '../../utils/code-signing';
import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
@@ -56,8 +51,6 @@ import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
import { getVcsClient } from '../../vcs';
export const defaultPublishPlatforms: Partial<PublishPlatform>[] = ['android', 'ios'];
type RawUpdateFlags = {
auto: boolean;
branch?: string;
@@ -88,21 +81,6 @@ type UpdateFlags = {
nonInteractive: boolean;
};
function getRequestedPlatform(platform: ExpoCLIExportPlatformFlag): RequestedPlatform | null {
switch (platform) {
case 'android':
return RequestedPlatform.Android;
case 'ios':
return RequestedPlatform.Ios;
case 'web':
return null;
case 'all':
return RequestedPlatform.All;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
export default class UpdatePublish extends EasCommand {
static override description = 'publish an update group';
@@ -166,18 +144,18 @@ export default class UpdatePublish extends EasCommand {
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(UpdatePublish);
const paginatedQueryOptions = getPaginatedQueryOptions(rawFlags);
let {
const {
auto: autoFlag,
platform: platformFlag,
channelName,
updateMessage,
channelName: channelNameArg,
updateMessage: updateMessageArg,
inputDir,
skipBundler,
privateKeyPath,
json: jsonFlag,
nonInteractive,
branchName: branchNameArg,
} = this.sanitizeFlags(rawFlags);
let branchName = this.sanitizeFlags(rawFlags).branchName;
const {
getDynamicProjectConfigAsync,
@@ -198,10 +176,6 @@ export default class UpdatePublish extends EasCommand {
isPublicConfig: true,
});
const { exp: expPrivate } = await getDynamicProjectConfigAsync({
isPublicConfig: false,
});
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
@@ -211,75 +185,28 @@ export default class UpdatePublish extends EasCommand {
projectId,
});
const { exp } = await getDynamicProjectConfigAsync();
const { exp } = await getDynamicProjectConfigAsync({ isPublicConfig: true });
const { exp: expPrivate } = await getDynamicProjectConfigAsync({
isPublicConfig: false,
});
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
let realizedPlatforms: PublishPlatform[] = [];
const branchName = await getBranchNameForCommandAsync({
graphqlClient,
projectId,
channelNameArg,
branchNameArg,
autoFlag,
nonInteractive,
paginatedQueryOptions,
});
if (channelName && branchName) {
throw new Error(
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto'
);
}
if (channelName) {
branchName = await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelName);
}
if (!branchName) {
if (autoFlag) {
branchName = await getDefaultBranchNameAsync();
} else if (nonInteractive) {
throw new Error('Must supply --channel, --branch or --auto when in non-interactive mode');
} else {
try {
const branch = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: `Which branch would you like to publish on?`,
displayTextForListItem: updateBranch => ({
title: `${updateBranch.name} ${chalk.grey(
`- current update: ${formatUpdateMessage(updateBranch.updates[0])}`
)}`,
}),
paginatedQueryOptions,
});
branchName = branch.name;
} catch {
// unable to select a branch (network error or no branches for project)
({ name: branchName } = await promptAsync({
type: 'text',
name: 'name',
message: 'No branches found. Provide a branch name:',
initial: await getDefaultBranchNameAsync(),
validate: value => (value ? true : 'Branch name may not be empty.'),
}));
}
assert(branchName, 'Branch name must be specified.');
}
}
if (!updateMessage && autoFlag) {
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
}
if (!updateMessage) {
if (nonInteractive) {
throw new Error('Must supply --message or use --auto when in non-interactive mode');
}
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ updateMessage } = await promptAsync({
type: 'text',
name: 'updateMessage',
message: `Provide an update message.`,
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
validate: (value: any) => (value ? true : validationMessage),
}));
}
const updateMessage = await getUpdateMessageForCommandAsync({
updateMessageArg,
autoFlag,
nonInteractive,
jsonFlag,
});
// build bundle and upload assets for a new publish
if (!skipBundler) {
@@ -300,6 +227,7 @@ export default class UpdatePublish extends EasCommand {
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
let uploadedAssetCount = 0;
let assetLimitPerUpdateGroup = 0;
let realizedPlatforms: PublishPlatform[] = [];
try {
const collectedAssets = await collectAssetsAsync(distRoot);
@@ -318,31 +246,42 @@ export default class UpdatePublish extends EasCommand {
uploadedAssetCount = uploadResults.uniqueUploadedAssetCount;
assetLimitPerUpdateGroup = uploadResults.assetLimitPerUpdateGroup;
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
const uploadAssetSuccessMessage = uploadedAssetCount
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'platform' : 'platforms'}`
: `Uploaded: No changes detected`;
assetSpinner.succeed(uploadAssetSuccessMessage);
// NOTE(cedric): we assume that bundles are always uploaded, and always are part of
// `uploadedAssetCount`, perferably we don't assume. For that, we need to refactor the
// `uploadAssetsAsync` and be able to determine asset type from the uploaded assets.
const uploadedBundleCount = uploadResults.launchAssetCount;
const uploadedNormalAssetCount = Math.max(0, uploadedAssetCount - uploadedBundleCount);
const reusedNormalAssetCount = uploadResults.uniqueAssetCount - uploadedNormalAssetCount;
assetSpinner.stop();
Log.withTick(
`Uploaded ${uploadedBundleCount} app ${uploadedBundleCount === 1 ? 'bundle' : 'bundles'}`
);
if (uploadedNormalAssetCount === 0) {
Log.withTick(`Uploading assets skipped - no new assets found`);
} else {
let message = `Uploaded ${uploadedNormalAssetCount} ${
uploadedNormalAssetCount === 1 ? 'asset' : 'assets'
}`;
if (reusedNormalAssetCount > 0) {
message += ` (reused ${reusedNormalAssetCount} ${
reusedNormalAssetCount === 1 ? 'asset' : 'assets'
})`;
}
Log.withTick(message);
}
for (const uploadedAssetPath of uploadResults.uniqueUploadedAssetPaths) {
Log.debug(chalk.dim(`- ${uploadedAssetPath}`));
}
} catch (e) {
assetSpinner.fail('Failed to upload');
throw e;
}
const truncatedMessage = truncateUpdateMessage(updateMessage!, 1024);
if (truncatedMessage !== updateMessage) {
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
}
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, realizedPlatforms, projectDir);
const runtimeToPlatformMapping: { runtimeVersion: string; platforms: string[] }[] = [];
for (const runtime of runtimeVersions) {
const platforms = runtimeVersions
.filter(({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion)
.map(({ platform }) => platform);
if (!runtimeToPlatformMapping.find(item => item.runtimeVersion === runtime.runtimeVersion)) {
runtimeToPlatformMapping.push({ runtimeVersion: runtime.runtimeVersion, platforms });
}
}
const runtimeToPlatformMapping =
getRuntimeToPlatformMappingFromRuntimeVersions(runtimeVersions);
const { branchId, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
@@ -377,7 +316,7 @@ export default class UpdatePublish extends EasCommand {
branchId,
updateInfoGroup: localUpdateInfoGroup,
runtimeVersion,
message: truncatedMessage,
message: updateMessage,
gitCommitHash,
isGitWorkingTreeDirty,
awaitingCodeSigningInfo: !!codeSigningInfo,
@@ -394,7 +333,10 @@ export default class UpdatePublish extends EasCommand {
const updatesTemp = [...newUpdates];
const updateGroupsAndTheirUpdates = updateGroups.map(updateGroup => {
const newUpdates = updatesTemp.splice(0, Object.keys(updateGroup.updateInfoGroup).length);
const newUpdates = updatesTemp.splice(
0,
Object.keys(nullthrows(updateGroup.updateInfoGroup)).length
);
return {
updateGroup,
newUpdates,
@@ -414,11 +356,13 @@ export default class UpdatePublish extends EasCommand {
checkManifestBodyAgainstUpdateInfoGroup(
manifestBody,
nullthrows(
updateGroup.updateInfoGroup[newUpdate.platform as keyof UpdateInfoGroup]
nullthrows(updateGroup.updateInfoGroup)[
newUpdate.platform as keyof UpdateInfoGroup
]
)
);
const manifestSignature = signManifestBody(manifestBody, codeSigningInfo);
const manifestSignature = signBody(manifestBody, codeSigningInfo);
await PublishMutation.setCodeSigningInfoAsync(graphqlClient, newUpdate.id, {
alg: codeSigningInfo.codeSigningMetadata.alg,
@@ -480,7 +424,7 @@ export default class UpdatePublish extends EasCommand {
? [{ label: 'Android update ID', value: newAndroidUpdate.id }]
: []),
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
{ label: 'Message', value: truncatedMessage },
{ label: 'Message', value: updateMessage },
...(gitCommitHash
? [
{
@@ -551,41 +495,3 @@ export default class UpdatePublish extends EasCommand {
};
}
}
/** Get runtime versions grouped by platform. Runtime version is always `null` on web where the platform is always backwards compatible. */
async function getRuntimeVersionObjectAsync(
exp: ExpoConfig,
platforms: PublishPlatform[],
projectDir: string
): Promise<{ platform: string; runtimeVersion: string }[]> {
for (const platform of platforms) {
if (platform === 'web') {
continue;
}
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
if (isPolicy) {
const isManaged =
(await resolveWorkflowAsync(projectDir, platform as Platform)) === Workflow.MANAGED;
if (!isManaged) {
throw new Error(
'Runtime version policies are only supported in the managed workflow. In the bare workflow, runtime version needs to be set manually.'
);
}
}
}
return [...new Set(platforms)].map(platform => {
if (platform === 'web') {
return { platform: 'web', runtimeVersion: 'UNVERSIONED' };
}
return {
platform,
runtimeVersion: nullthrows(
Updates.getRuntimeVersion(exp, platform),
`Unable to determine runtime version for ${
requestedPlatformDisplayNames[platform]
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
),
};
});
}
@@ -56,7 +56,7 @@ type UpdateToRepublish = {
>;
export default class UpdateRepublish extends EasCommand {
static override description = 'rollback to an existing update';
static override description = 'roll back to an existing update';
static override flags = {
channel: Flags.string({
@@ -0,0 +1,389 @@
import { Platform as PublishPlatform } from '@expo/config';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { ensureBranchExistsAsync } from '../../branch/queries';
import { getUpdateGroupUrl } from '../../build/utils/url';
import { ensureChannelExistsAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import fetch from '../../fetch';
import {
PublishUpdateGroupInput,
StatuspageServiceName,
UpdatePublishMutation,
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import Log, { link } from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import {
ExpoCLIExportPlatformFlag,
defaultPublishPlatforms,
getBranchNameForCommandAsync,
getRequestedPlatform,
getRuntimeToPlatformMappingFromRuntimeVersions,
getRuntimeVersionObjectAsync,
getUpdateMessageForCommandAsync,
} from '../../project/publish';
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
import { getUpdateGroupJsonInfo } from '../../update/utils';
import {
CodeSigningInfo,
checkDirectiveBodyAgainstUpdateInfoGroup,
getCodeSigningInfoAsync,
getDirectiveBodyAsync,
signBody,
} from '../../utils/code-signing';
import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
import { getVcsClient } from '../../vcs';
type RawUpdateFlags = {
auto: boolean;
branch?: string;
channel?: string;
message?: string;
platform: string;
'private-key-path'?: string;
'non-interactive': boolean;
json: boolean;
};
type UpdateFlags = {
auto: boolean;
platform: ExpoCLIExportPlatformFlag;
branchName?: string;
channelName?: string;
updateMessage?: string;
privateKeyPath?: string;
json: boolean;
nonInteractive: boolean;
};
export default class UpdateRollBackToEmbedded extends EasCommand {
static override hidden = true; // until we launch
static override description = 'roll back to the embedded update';
static override flags = {
branch: Flags.string({
description: 'Branch to publish the rollback to embedded update group on',
required: false,
}),
channel: Flags.string({
description: 'Channel that the published rollback to embedded update should affect',
required: false,
}),
message: Flags.string({
description: 'A short message describing the rollback to embedded update',
required: false,
}),
platform: Flags.enum({
char: 'p',
options: [...defaultPublishPlatforms, 'all'],
default: 'all',
required: false,
}),
auto: Flags.boolean({
description:
'Use the current git branch and commit message for the EAS branch and update message',
default: false,
}),
'private-key-path': Flags.string({
description: `File containing the PEM-encoded private key corresponding to the certificate in expo-updates' configuration. Defaults to a file named "private-key.pem" in the certificate's directory.`,
required: false,
}),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(UpdateRollBackToEmbedded);
const paginatedQueryOptions = getPaginatedQueryOptions(rawFlags);
const {
auto: autoFlag,
platform: platformFlag,
channelName: channelNameArg,
updateMessage: updateMessageArg,
privateKeyPath,
json: jsonFlag,
nonInteractive,
branchName: branchNameArg,
} = this.sanitizeFlags(rawFlags);
const {
getDynamicProjectConfigAsync,
loggedIn: { graphqlClient },
} = await this.getContextAsync(UpdateRollBackToEmbedded, {
nonInteractive,
});
if (jsonFlag) {
enableJsonOutput();
}
const {
exp: expPossiblyWithoutEasUpdateConfigured,
projectId,
projectDir,
} = await getDynamicProjectConfigAsync({
isPublicConfig: true,
});
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
exp: expPossiblyWithoutEasUpdateConfigured,
platform: getRequestedPlatform(platformFlag),
projectDir,
projectId,
});
const { exp } = await getDynamicProjectConfigAsync({ isPublicConfig: true });
const { exp: expPrivate } = await getDynamicProjectConfigAsync({
isPublicConfig: false,
});
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
const branchName = await getBranchNameForCommandAsync({
graphqlClient,
projectId,
channelNameArg,
branchNameArg,
autoFlag,
nonInteractive,
paginatedQueryOptions,
});
const updateMessage = await getUpdateMessageForCommandAsync({
updateMessageArg,
autoFlag,
nonInteractive,
jsonFlag,
});
const realizedPlatforms: PublishPlatform[] =
platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
const { branchId, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName,
});
if (createdBranch) {
await ensureChannelExistsAsync(graphqlClient, {
appId: projectId,
branchId,
channelName: branchName,
});
}
Log.withTick(`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`);
const vcsClient = getVcsClient();
const gitCommitHash = await vcsClient.getCommitHashAsync();
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, realizedPlatforms, projectDir);
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
const publishSpinner = ora('Publishing...').start();
try {
newUpdates = await this.publishRollbacksAsync({
graphqlClient,
isGitWorkingTreeDirty,
gitCommitHash,
updateMessage,
branchId,
codeSigningInfo,
runtimeVersions,
realizedPlatforms,
});
publishSpinner.succeed('Published!');
} catch (e) {
publishSpinner.fail('Failed to publish updates');
throw e;
}
if (jsonFlag) {
printJsonOnlyOutput(getUpdateGroupJsonInfo(newUpdates));
} else {
if (new Set(newUpdates.map(update => update.group)).size > 1) {
Log.addNewLineIfNone();
Log.log(
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
);
}
Log.addNewLineIfNone();
for (const runtime of uniqBy(runtimeVersions, version => version.runtimeVersion)) {
const newUpdatesForRuntimeVersion = newUpdates.filter(
update => update.runtimeVersion === runtime.runtimeVersion
);
if (newUpdatesForRuntimeVersion.length === 0) {
throw new Error(
`Publish response is missing updates with runtime ${runtime.runtimeVersion}.`
);
}
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
update => update.platform === 'android'
);
const newIosUpdate = newUpdatesForRuntimeVersion.find(update => update.platform === 'ios');
const updateGroupId = newUpdatesForRuntimeVersion[0].group;
const projectName = exp.slug;
const accountName = (await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name;
const updateGroupUrl = getUpdateGroupUrl(accountName, projectName, updateGroupId);
const updateGroupLink = link(updateGroupUrl, { dim: false });
Log.log(
formatFields([
{ label: 'Branch', value: branchName },
{ label: 'Runtime version', value: runtime.runtimeVersion },
{ label: 'Platform', value: platforms.join(', ') },
{ label: 'Update group ID', value: updateGroupId },
...(newAndroidUpdate
? [{ label: 'Android update ID', value: newAndroidUpdate.id }]
: []),
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
{ label: 'Message', value: updateMessage },
...(gitCommitHash
? [
{
label: 'Commit',
value: `${gitCommitHash}${isGitWorkingTreeDirty ? '*' : ''}`,
},
]
: []),
{ label: 'Website link', value: updateGroupLink },
])
);
Log.addNewLineIfNone();
}
}
}
private async publishRollbacksAsync({
graphqlClient,
isGitWorkingTreeDirty,
gitCommitHash,
updateMessage,
branchId,
codeSigningInfo,
runtimeVersions,
realizedPlatforms,
}: {
graphqlClient: ExpoGraphqlClient;
isGitWorkingTreeDirty: boolean | undefined;
gitCommitHash: string | undefined;
updateMessage: string;
branchId: string;
codeSigningInfo: CodeSigningInfo | undefined;
runtimeVersions: { platform: string; runtimeVersion: string }[];
realizedPlatforms: PublishPlatform[];
}): Promise<UpdatePublishMutation['updateBranch']['publishUpdateGroups']> {
const runtimeToPlatformMapping =
getRuntimeToPlatformMappingFromRuntimeVersions(runtimeVersions);
const rollbackInfoGroups = Object.fromEntries(
realizedPlatforms.map(platform => [platform, true])
);
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformMapping.map(
({ runtimeVersion, platforms }) => {
const localRollbackInfoGroup = Object.fromEntries(
platforms.map(platform => [platform, rollbackInfoGroups[platform]])
);
return {
branchId,
rollBackToEmbeddedInfoGroup: localRollbackInfoGroup,
runtimeVersion,
message: updateMessage,
gitCommitHash,
isGitWorkingTreeDirty,
awaitingCodeSigningInfo: !!codeSigningInfo,
};
}
);
const newUpdates = await PublishMutation.publishUpdateGroupAsync(graphqlClient, updateGroups);
if (codeSigningInfo) {
Log.log('🔒 Signing roll back');
const updatesTemp = [...newUpdates];
const updateGroupsAndTheirUpdates = updateGroups.map(updateGroup => {
const newUpdates = updatesTemp.splice(
0,
Object.keys(nullthrows(updateGroup.rollBackToEmbeddedInfoGroup)).length
);
return {
updateGroup,
newUpdates,
};
});
await Promise.all(
updateGroupsAndTheirUpdates.map(async ({ newUpdates }) => {
await Promise.all(
newUpdates.map(async newUpdate => {
const response = await fetch(newUpdate.manifestPermalink, {
method: 'GET',
headers: { accept: 'multipart/mixed' },
});
const directiveBody = nullthrows(await getDirectiveBodyAsync(response));
checkDirectiveBodyAgainstUpdateInfoGroup(directiveBody);
const directiveSignature = signBody(directiveBody, codeSigningInfo);
await PublishMutation.setCodeSigningInfoAsync(graphqlClient, newUpdate.id, {
alg: codeSigningInfo.codeSigningMetadata.alg,
keyid: codeSigningInfo.codeSigningMetadata.keyid,
sig: directiveSignature,
});
})
);
})
);
}
return newUpdates;
}
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
const nonInteractive = flags['non-interactive'] ?? false;
const { auto, branch: branchName, channel: channelName, message: updateMessage } = flags;
if (nonInteractive && !auto && !(updateMessage && (branchName || channelName))) {
Errors.error(
'--branch and --message, or --channel and --message are required when updating in non-interactive mode unless --auto is specified',
{ exit: 1 }
);
}
return {
auto,
branchName,
channelName,
updateMessage,
platform: flags.platform as RequestedPlatform,
privateKeyPath: flags['private-key-path'],
nonInteractive,
json: flags.json ?? false,
};
}
}
@@ -17,7 +17,7 @@ import { getAppFromContextAsync } from './actions/BuildCredentialsUtils';
import { SetUpBuildCredentials } from './actions/SetUpBuildCredentials';
import { SetUpPushKey } from './actions/SetUpPushKey';
import { App, IosCredentials, Target } from './types';
import { isAdHocProfile } from './utils/provisioningProfile';
import { isAdHocProfile, isEnterpriseUniversalProfile } from './utils/provisioningProfile';
interface Options {
app: App;
@@ -128,7 +128,7 @@ export default class IosCredentialsProvider {
private async disablePushNotificationsSetupInEasJsonAsync(
ctx: CredentialsContext
): Promise<void> {
const easJsonAccessor = new EasJsonAccessor(ctx.projectDir);
const easJsonAccessor = EasJsonAccessor.fromProjectPath(ctx.projectDir);
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
easJsonRawObject.cli = {
@@ -143,17 +143,32 @@ export default class IosCredentialsProvider {
private assertProvisioningProfileType(provisioningProfile: string, targetName?: string): void {
const isAdHoc = isAdHocProfile(provisioningProfile);
if (this.options.distribution === 'internal' && !isAdHoc) {
throw new Error(
`You must use an adhoc provisioning profile${
targetName ? ` (target '${targetName})'` : ''
} for internal distribution`
);
} else if (this.options.distribution !== 'internal' && isAdHoc) {
const isEnterprise = isEnterpriseUniversalProfile(provisioningProfile);
if (this.options.distribution === 'internal') {
if (this.options.enterpriseProvisioning === 'universal' && !isEnterprise) {
throw new Error(
`You must use a universal provisioning profile${
targetName ? ` (target '${targetName})'` : ''
} for internal distribution if you specified "enterpriseProvisioning": "universal" in eas.json`
);
} else if (this.options.enterpriseProvisioning === 'adhoc' && !isAdHoc) {
throw new Error(
`You must use an adhoc provisioning profile${
targetName ? ` (target '${targetName})'` : ''
} for internal distribution if you specified "enterpriseProvisioning": "adhoc" in eas.json`
);
} else if (!this.options.enterpriseProvisioning && !isEnterprise && !isAdHoc) {
throw new Error(
`You must use an adhoc provisioning profile${
targetName ? ` (target '${targetName})'` : ''
} for internal distribution.`
);
}
} else if (isAdHoc) {
throw new Error(
`You can't use an adhoc provisioning profile${
targetName ? ` (target '${targetName}')` : ''
} for app store distribution`
} for app store distribution.`
);
}
}
@@ -348,7 +348,7 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
'com.apple.developer.healthkit': true,
})
).rejects.toThrowError(
`https://developer.apple.com/account/resources/identifiers/bundleId/edit/XXX333`
`https://developer-mdn.apple.com/account/resources/identifiers/bundleId/edit/XXX333`
);
});
@@ -5,7 +5,7 @@ import { formatContractMessage } from '../contractMessages';
const expiredMembership = {
subject: 'Developer Program Membership Expired',
message:
"Your membership has expired, and your apps have been removed from the App Store until you renew your membership. To renew, a user with the Account Holder role must <a href='http://developer.apple.com/membercenter/index.action' target='_blank'>sign in</a> and renew the membership on the Apple Developer website. <a href='https://developer.apple.com/support/renewal/' target='_blank'>Learn More</a>.",
"Your membership has expired, and your apps have been removed from the App Store until you renew your membership. To renew, a user with the Account Holder role must <a href='http://developer-mdn.apple.com/membercenter/index.action' target='_blank'>sign in</a> and renew the membership on the Apple Developer website. <a href='https://developer-mdn.apple.com/support/renewal/' target='_blank'>Learn More</a>.",
};
const paymentReturned = {
subject: 'Payment Returned',
@@ -15,12 +15,12 @@ const paymentReturned = {
const licenseAgreementUpdated = {
subject: 'Apple Developer Program License Agreement Updated',
message:
'The updated Apple Developer Program License Agreement needs to be reviewed. In order to update your existing apps and submit new apps to the App Store, the Account Holder must review and accept the updated agreement by signing in to their <a href="https://developer.apple.com/account" target="_blank">account</a> on the Apple Developer website.',
'The updated Apple Developer Program License Agreement needs to be reviewed. In order to update your existing apps and submit new apps to the App Store, the Account Holder must review and accept the updated agreement by signing in to their <a href="https://developer-mdn.apple.com/account" target="_blank">account</a> on the Apple Developer website.',
};
const paidLicenseAgreementUpdated = {
subject: '',
message:
'<b>Review the updated Paid Applications Schedule.</b><br />In order to update your existing apps, create new in-app purchases, and submit new apps to the App Store, the user with the Legal role (Account Holder) must review and accept the Paid Applications Schedule (Schedule 2 to the Apple Developer Program License Agreement) in the Agreements, Tax, and Banking module.<br /><br /> To accept this agreement, they must have already accepted the latest version of the Apple Developer Program License Agreement in their <a href="http://developer.apple.com/membercenter/index.action">account on the developer website<a/>.<br />',
'<b>Review the updated Paid Applications Schedule.</b><br />In order to update your existing apps, create new in-app purchases, and submit new apps to the App Store, the user with the Legal role (Account Holder) must review and accept the Paid Applications Schedule (Schedule 2 to the Apple Developer Program License Agreement) in the Agreements, Tax, and Banking module.<br /><br /> To accept this agreement, they must have already accepted the latest version of the Apple Developer Program License Agreement in their <a href="http://developer-mdn.apple.com/membercenter/index.action">account on the developer website<a/>.<br />',
};
describe(formatContractMessage, () => {
@@ -111,7 +111,7 @@ export async function syncCapabilitiesForEntitlementsAsync(
inspect(modifiedRequest, { depth: null, colors: true })
);
throw new Error(
`Unexpected error occurred while attempting to update capabilities for app "${bundleId.attributes.identifier}".\nCapabilities can be modified manually in the Apple developer console at https://developer.apple.com/account/resources/identifiers/bundleId/edit/${bundleId.id}.\nAuto capability syncing can be disabled with the environment variable \`EXPO_NO_CAPABILITY_SYNC=1\`.\n${error.message}`
`Unexpected error occurred while attempting to update capabilities for app "${bundleId.attributes.identifier}".\nCapabilities can be modified manually in the Apple developer console at https://developer-mdn.apple.com/account/resources/identifiers/bundleId/edit/${bundleId.id}.\nAuto capability syncing can be disabled with the environment variable \`EXPO_NO_CAPABILITY_SYNC=1\`.\n${error.message}`
);
}
}
@@ -266,7 +266,7 @@ type CapabilityClassifier = {
// NOTE(Bacon): From manually toggling values in Xcode and checking the git diff and network requests.
// Last Updated: July 22nd, 2021
// https://developer.apple.com/documentation/bundleresources/entitlements
// https://developer-mdn.apple.com/documentation/bundleresources/entitlements
export const CapabilityMapping: CapabilityClassifier[] = [
{
name: 'HomeKit',
@@ -413,7 +413,7 @@ export const CapabilityMapping: CapabilityClassifier[] = [
getOptions: getBooleanOptions,
},
{
// https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_default-data-protection
// https://developer-mdn.apple.com/documentation/bundleresources/entitlements/com_apple_developer_default-data-protection
name: 'Data Protection',
entitlement: 'com.apple.developer.default-data-protection',
capability: CapabilityType.DATA_PROTECTION,
@@ -446,7 +446,7 @@ export const CapabilityMapping: CapabilityClassifier[] = [
getOptions: getBooleanOptions,
},
{
// https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_networking_networkextension
// https://developer-mdn.apple.com/documentation/bundleresources/entitlements/com_apple_developer_networking_networkextension
name: 'Network Extensions',
entitlement: 'com.apple.developer.networking.networkextension',
capability: CapabilityType.NETWORK_EXTENSIONS,
@@ -465,7 +465,7 @@ export const CapabilityMapping: CapabilityClassifier[] = [
getOptions: getDefinedOptions,
},
{
// https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_nfc_readersession_formats
// https://developer-mdn.apple.com/documentation/bundleresources/entitlements/com_apple_developer_nfc_readersession_formats
name: 'NFC Tag Reading',
entitlement: 'com.apple.developer.nfc.readersession.formats',
capability: CapabilityType.NFC_TAG_READING,
@@ -482,7 +482,7 @@ export const CapabilityMapping: CapabilityClassifier[] = [
getOptions: getDefinedOptions,
},
{
// https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_networking_vpn_api
// https://developer-mdn.apple.com/documentation/bundleresources/entitlements/com_apple_developer_networking_vpn_api
name: 'Push Notifications',
// com.apple.developer.aps-environment
entitlement: 'aps-environment',
@@ -515,8 +515,8 @@ export const CapabilityMapping: CapabilityClassifier[] = [
},
{
// Not in Xcode
// https://developer.apple.com/documentation/devicecheck/preparing_to_use_the_app_attest_service
// https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_devicecheck_appattest-environment
// https://developer-mdn.apple.com/documentation/devicecheck/preparing_to_use_the_app_attest_service
// https://developer-mdn.apple.com/documentation/bundleresources/entitlements/com_apple_developer_devicecheck_appattest-environment
name: 'App Attest',
entitlement: 'com.apple.developer.devicecheck.appattest-environment',
capability: CapabilityType.APP_ATTEST,
@@ -725,10 +725,10 @@ export const CapabilityMapping: CapabilityClassifier[] = [
// it's not clear if it needs to be updated.
// "Contact Notes" requires the user to ask Apple in a form:
// https://developer.apple.com/contact/request/contact-note-field
// com.apple.developer.contacts.notes: https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_contacts_notes/
// https://developer-mdn.apple.com/contact/request/contact-note-field
// com.apple.developer.contacts.notes: https://developer-mdn.apple.com/documentation/bundleresources/entitlements/com_apple_developer_contacts_notes/
// "Exposure Notification" requires the user to ask Apple in a form:
// https://developer.apple.com/contact/request/exposure-notification-entitlement
// com.apple.developer.exposure-notification: https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_exposure-notification/
// https://developer-mdn.apple.com/contact/request/exposure-notification-entitlement
// com.apple.developer.exposure-notification: https://developer-mdn.apple.com/documentation/bundleresources/entitlements/com_apple_developer_exposure-notification/
];
@@ -26,6 +26,11 @@ export function isAdHocProfile(dataBase64: string): boolean {
return Array.isArray(provisionedDevices);
}
export function isEnterpriseUniversalProfile(dataBase64: string): boolean {
const profilePlist = parse(dataBase64);
return !!profilePlist['ProvisionsAllDevices'];
}
export function parse(dataBase64: string): PlistObject {
try {
const buffer = Buffer.from(dataBase64, 'base64');
@@ -8,7 +8,7 @@ export class SelectBuildProfileFromEasJson<T extends Platform> {
private easJsonAccessor: EasJsonAccessor;
constructor(projectDir: string, private platform: T) {
this.easJsonAccessor = new EasJsonAccessor(projectDir);
this.easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
}
async runAsync(): Promise<BuildProfile<T>> {
+468 -130
View File
File diff suppressed because it is too large. Load diff
@@ -18,6 +18,8 @@ import {
IosJobOverridesInput,
RetryIosBuildMutation,
RetryIosBuildMutationVariables,
UpdateBuildMetadataMutation,
UpdateBuildMetadataMutationVariables,
} from '../generated';
import { BuildFragmentNode } from '../types/Build';
@@ -119,6 +121,33 @@ export const BuildMutation = {
);
return nullthrows(data.build?.createIosBuild);
},
async updateBuildMetadataAsync(
graphqlClient: ExpoGraphqlClient,
input: {
buildId: string;
metadata: BuildMetadataInput;
}
): Promise<BuildFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<UpdateBuildMetadataMutation, UpdateBuildMetadataMutationVariables>(
gql`
mutation UpdateBuildMetadataMutation($buildId: ID!, $metadata: BuildMetadataInput!) {
build {
updateBuildMetadata(buildId: $buildId, metadata: $metadata) {
id
...BuildFragment
}
}
}
${print(BuildFragmentNode)}
`,
input
)
.toPromise()
);
return nullthrows(data.build?.updateBuildMetadata);
},
async retryIosBuildAsync(
graphqlClient: ExpoGraphqlClient,
input: {
@@ -1,3 +1,4 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
@@ -8,8 +9,10 @@ import {
GetSignedUploadMutationVariables,
PublishUpdateGroupInput,
SetCodeSigningInfoMutation,
UpdateFragment,
UpdatePublishMutation,
} from '../generated';
import { UpdateFragmentNode } from '../types/Update';
export const PublishMutation = {
async getUploadURLsAsync(
@@ -39,7 +42,7 @@ export const PublishMutation = {
async publishUpdateGroupAsync(
graphqlClient: ExpoGraphqlClient,
publishUpdateGroupsInput: PublishUpdateGroupInput[]
): Promise<UpdatePublishMutation['updateBranch']['publishUpdateGroups']> {
): Promise<UpdateFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<UpdatePublishMutation>(
@@ -52,6 +55,7 @@ export const PublishMutation = {
}
}
}
${print(UpdateFragmentNode)}
`,
{ publishUpdateGroupsInput }
)
@@ -26,6 +26,9 @@ export const UserQuery = {
... on Robot {
firstName
}
... on SSOUser {
username
}
accounts {
id
...AccountFragment
@@ -52,7 +52,9 @@ export const BuildFragmentNode = gql`
priority
createdAt
updatedAt
message
completedAt
resourceClass
}
`;
@@ -9,6 +9,7 @@ export const UpdateFragmentNode = gql`
runtimeVersion
platform
manifestFragment
isRollBackToEmbedded
manifestPermalink
gitCommitHash
actor {
+3 -2
View File
@@ -90,11 +90,12 @@ export default class Log {
*/
export function link(
url: string,
{ text = url, dim = true }: { text?: string; dim?: boolean } = {}
{ text = url, fallback, dim = true }: { text?: string; dim?: boolean; fallback?: string } = {}
): string {
// Links can be disabled via env variables https://github.com/jamestalmage/supports-hyperlinks/blob/master/index.js
const output = terminalLink(text, url, {
fallback: () => (text === url ? chalk.underline(url) : `${text}: ${chalk.underline(url)}`),
fallback: () =>
fallback ?? (text === url ? chalk.underline(url) : `${text}: ${chalk.underline(url)}`),
});
return dim ? chalk.dim(output) : output;
}
@@ -5,7 +5,7 @@ const KEYWORD_CHARACTER_LIMIT = 100;
/**
* Keywords are limited to 100 characters when converted to a comma separated string.
* @see https://developer.apple.com/app-store/search/
* @see https://developer-mdn.apple.com/app-store/search/
*/
export const infoKeywordLength: IssueRule = {
id: 'apple.info.keyword.length',
+1 -1
View File
@@ -26,7 +26,7 @@ export interface AppleRelease {
export interface AppleInfo {
title: string;
subtitle?: string;
/** Does not effect ASO https://developer.apple.com/app-store/product-page/ */
/** Does not effect ASO https://developer-mdn.apple.com/app-store/product-page/ */
promoText?: string;
description?: string;
keywords?: string[];
@@ -5,7 +5,6 @@ import { instance, mock } from 'ts-mockito';
import { v4 as uuidv4 } from 'uuid';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { defaultPublishPlatforms } from '../../commands/update';
import { AssetMetadataStatus } from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { PublishQuery } from '../../graphql/queries/PublishQuery';
@@ -14,9 +13,12 @@ import {
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
convertAssetToUpdateInfoGroupFormatAsync,
defaultPublishPlatforms,
filterExportedPlatformsByFlag,
filterOutAssetsThatAlreadyExistAsync,
getAssetHashFromPath,
getBase64URLEncoding,
getOriginalPathFromAssetMap,
getStorageKey,
getStorageKeyForAssetAsync,
guessContentTypeFromExtension,
@@ -279,9 +281,55 @@ describe(resolveInputDirectoryAsync, () => {
});
});
describe(getAssetHashFromPath, () => {
it('returns asset hash from path', () => {
expect(getAssetHashFromPath('assets/5b2a819c71d035ca45d223e4c47ed4f9')).toBe(
'5b2a819c71d035ca45d223e4c47ed4f9'
);
});
it('returns null for incorrect path', () => {
expect(getAssetHashFromPath('assets/this/is/not/a/hash.jpg')).toBeNull();
});
});
describe(getOriginalPathFromAssetMap, () => {
// Partial assetmap.json, with only fields we need
const fakeAssetMap = {
'5b2a819c71d035ca45d223e4c47ed4f9': {
httpServerLocation: '/assets/src/assets',
name: 'asset-420',
type: 'jpg',
},
};
it('returns null path when asset map is null', () => {
expect(
getOriginalPathFromAssetMap(null, {
path: 'assets/5b2a819c71d035ca45d223e4c47ed4f9',
ext: 'jpg',
})
).toBeNull();
});
it('returns null when asset is not found in asset map', () => {
expect(
getOriginalPathFromAssetMap(fakeAssetMap, {
path: 'assets/fb64d3b2fb71b3d739ad5c13a93e12c5',
ext: 'jpg',
})
).toBeNull();
});
it('returns reconstructed original path from existing asset in asset map', () => {
expect(
getOriginalPathFromAssetMap(fakeAssetMap, {
path: 'assets/5b2a819c71d035ca45d223e4c47ed4f9',
ext: 'jpg',
})
).toBe('/src/assets/asset-420.jpg');
});
});
describe(collectAssetsAsync, () => {
it('builds an update info group', async () => {
const fakeHash = 'md5-hash-of-jpg';
const fakeHash = 'hdahukw8adhawi8fawhfa8';
const bundles = {
android: 'android-bundle-code',
ios: 'ios-bundle-code',
@@ -294,6 +342,7 @@ describe(collectAssetsAsync, () => {
fileExtension: '.jpg',
contentType: 'image/jpeg',
path: `${inputDir}/assets/${fakeHash}`,
originalPath: `assets/wat.jpg`,
},
];
@@ -326,6 +375,24 @@ describe(collectAssetsAsync, () => {
},
})
);
await fs.writeFile(
path.resolve(inputDir, 'assetmap.json'),
JSON.stringify({
[fakeHash]: {
__packager_asset: true,
fileSystemLocation: '/Users/blah/temp/assets',
httpServerLocation: 'assets/assets',
width: 2339,
height: 1560,
scales: [1],
files: ['/Users/blah/temp/assets/wat.jpg'],
hash: fakeHash,
name: 'wat',
type: 'jpg',
fileHashes: [fakeHash],
},
})
);
expect(await collectAssetsAsync(inputDir)).toEqual({
android: {
@@ -410,6 +477,7 @@ describe(uploadAssetsAsync, () => {
const androidBundlePath = uuidv4();
const iosBundlePath = uuidv4();
const dummyFilePath = uuidv4();
const dummyOriginalFilePath = uuidv4();
const userDefinedPath = uuidv4();
const testProjectId = uuidv4();
const expectedAssetLimit = 1400;
@@ -427,7 +495,15 @@ describe(uploadAssetsAsync, () => {
contentType: 'application/javascript',
path: androidBundlePath,
},
assets: [userDefinedAsset, { type: 'jpg', contentType: 'image/jpeg', path: dummyFilePath }],
assets: [
userDefinedAsset,
{
type: 'jpg',
contentType: 'image/jpeg',
path: dummyFilePath,
originalPath: dummyOriginalFilePath,
},
],
},
ios: {
launchAsset: {
@@ -435,7 +511,15 @@ describe(uploadAssetsAsync, () => {
contentType: 'application/javascript',
path: androidBundlePath,
},
assets: [userDefinedAsset, { type: 'jpg', contentType: 'image/jpeg', path: dummyFilePath }],
assets: [
userDefinedAsset,
{
type: 'jpg',
contentType: 'image/jpeg',
path: dummyFilePath,
originalPath: dummyOriginalFilePath,
},
],
},
};
@@ -490,8 +574,10 @@ describe(uploadAssetsAsync, () => {
uploadAssetsAsync(graphqlClient, assetsForUpdateInfoGroup, testProjectId)
).resolves.toEqual({
assetCount: 6,
launchAssetCount: 2,
uniqueAssetCount: 3,
uniqueUploadedAssetCount: 0,
uniqueUploadedAssetPaths: [],
assetLimitPerUpdateGroup: expectedAssetLimit,
});
});
@@ -526,8 +612,10 @@ describe(uploadAssetsAsync, () => {
uploadAssetsAsync(graphqlClient, assetsForUpdateInfoGroup, testProjectId)
).resolves.toEqual({
assetCount: 6,
launchAssetCount: 2,
uniqueAssetCount: 3,
uniqueUploadedAssetCount: 2,
uniqueUploadedAssetPaths: [],
assetLimitPerUpdateGroup: expectedAssetLimit,
});
});
@@ -1,8 +1,11 @@
import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { instance, mock } from 'ts-mockito';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { jester, jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { promptAsync } from '../../../prompts';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
@@ -11,6 +14,7 @@ import {
jest.mock('fs');
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
beforeEach(async () => {
@@ -103,6 +107,8 @@ describe(getApplicationIdAsync, () => {
describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
describe('managed project + android.package missing in app config', () => {
it('throws an error if using app.config.js', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
vol.fromJSON(
{
'app.config.js': 'module.exports = { blah: {} };',
@@ -110,10 +116,23 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
'/app'
);
await expect(
ensureApplicationIdIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
ensureApplicationIdIsDefinedForManagedProjectAsync({
graphqlClient,
projectDir: '/app',
projectId: '',
exp: {} as any,
})
).rejects.toThrowError(/we can't update this file programmatically/);
});
it('prompts for the Android package if using app.json', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
vol.fromJSON(
{
'app.json': '{ "expo": {} }',
@@ -126,11 +145,26 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
}));
await expect(
ensureApplicationIdIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
ensureApplicationIdIsDefinedForManagedProjectAsync({
graphqlClient,
projectDir: '/app',
projectId: '',
exp: {} as any,
})
).resolves.toBe('com.expo.notdominik');
expect(promptAsync).toHaveBeenCalled();
});
it('puts the Android package in app.json', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
vol.fromJSON(
{
'app.json': '{ "expo": {} }',
@@ -143,7 +177,12 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
}));
await expect(
ensureApplicationIdIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
ensureApplicationIdIsDefinedForManagedProjectAsync({
graphqlClient,
projectDir: '/app',
projectId: '',
exp: {} as any,
})
).resolves.toBe('com.expo.notdominik');
const appJson = JSON.parse(await fs.readFile('/app/app.json', 'utf-8'));
expect(appJson).toMatchObject({
@@ -7,22 +7,31 @@ import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { readAppJson } from '../../build/utils/appJson';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import env from '../../env';
import Log, { learnMore } from '../../log';
import { getProjectConfigDescription, getUsername } from '../../project/projectUtils';
import {
getOwnerAccountForProjectIdAsync,
getProjectConfigDescription,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { Actor } from '../../user/User';
import { resolveWorkflowAsync } from '../workflow';
import { GradleBuildContext } from './gradle';
import * as gradleUtils from './gradleUtils';
export const INVALID_APPLICATION_ID_MESSAGE = `Invalid format of Android applicationId. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`;
export async function ensureApplicationIdIsDefinedForManagedProjectAsync(
projectDir: string,
exp: ExpoConfig,
actor: Actor
): Promise<string> {
export async function ensureApplicationIdIsDefinedForManagedProjectAsync({
graphqlClient,
projectDir,
projectId,
exp,
}: {
graphqlClient: ExpoGraphqlClient;
projectDir: string;
projectId: string;
exp: ExpoConfig;
}): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
assert(workflow === Workflow.MANAGED, 'This function should be called only for managed projects');
@@ -31,7 +40,7 @@ export async function ensureApplicationIdIsDefinedForManagedProjectAsync(
moduleName: gradleUtils.DEFAULT_MODULE_NAME,
});
} catch {
return await configureApplicationIdAsync(projectDir, exp, actor);
return await configureApplicationIdAsync({ graphqlClient, projectDir, projectId, exp });
}
}
@@ -110,11 +119,17 @@ export async function getApplicationIdAsync(
}
}
async function configureApplicationIdAsync(
projectDir: string,
exp: ExpoConfig,
actor: Actor
): Promise<string> {
async function configureApplicationIdAsync({
graphqlClient,
projectDir,
projectId,
exp,
}: {
graphqlClient: ExpoGraphqlClient;
projectDir: string;
projectId: string;
exp: ExpoConfig;
}): Promise<string> {
const paths = getConfigFilePaths(projectDir);
// we can't automatically update app.config.js
if (paths.dynamicConfigPath) {
@@ -132,7 +147,11 @@ async function configureApplicationIdAsync(
)}`
);
const suggestedAndroidApplicationId = await getSuggestedApplicationIdAsync(exp, actor);
const suggestedAndroidApplicationId = await getSuggestedApplicationIdAsync(
graphqlClient,
exp,
projectId
);
const { packageName } = await promptAsync({
name: 'packageName',
type: 'text',
@@ -174,8 +193,9 @@ export function warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(
}
async function getSuggestedApplicationIdAsync(
graphqlClient: ExpoGraphqlClient,
exp: ExpoConfig,
actor: Actor
projectId: string
): Promise<string | undefined> {
// Attempt to use the ios bundle id first since it's convenient to have them aligned.
const maybeBundleId = IOSConfig.BundleIdentifier.getBundleIdentifier(exp);
@@ -183,9 +203,9 @@ async function getSuggestedApplicationIdAsync(
return maybeBundleId;
} else {
// the only callsite is heavily interactive
const username = getUsername(exp, actor);
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
// It's common to use dashes in your node project name, strip them from the suggested package name.
const possibleId = `com.${username}.${exp.slug}`.split('-').join('');
const possibleId = `com.${account.name}.${exp.slug}`.split('-').join('');
if (isApplicationIdValid(possibleId)) {
return possibleId;
}
@@ -74,7 +74,7 @@ export function parseGradleCommand(cmd: string, buildGradle: AppBuildGradle): Gr
const [moduleName, taskName] =
splitCmd.length > 1 ? [splitCmd[0], splitCmd[1]] : [undefined, splitCmd[0]];
const matchResult = taskName.match(/(build|bundle|assemble)(.*)(Release|Debug)/);
const matchResult = taskName.match(/(build|bundle|assemble|package)(.*)(Release|Debug)/);
if (!matchResult) {
throw new Error(`Failed to parse gradle command: ${cmd}`);
}
@@ -2,7 +2,7 @@ import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { Actor } from '../user/User';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
getApplicationIdAsync,
@@ -16,19 +16,32 @@ import { resolveXcodeBuildContextAsync } from './ios/scheme';
import { findApplicationTarget, resolveTargetsAsync } from './ios/target';
import { resolveWorkflowAsync } from './workflow';
export async function getApplicationIdentifierAsync(
projectDir: string,
exp: ExpoConfig,
buildProfile: BuildProfile,
platform: Platform,
actor: Actor
): Promise<string> {
export async function getApplicationIdentifierAsync({
graphqlClient,
projectDir,
projectId,
exp,
buildProfile,
platform,
}: {
graphqlClient: ExpoGraphqlClient;
projectDir: string;
projectId: string;
exp: ExpoConfig;
buildProfile: BuildProfile;
platform: Platform;
}): Promise<string> {
if (platform === Platform.ANDROID) {
const profile = buildProfile as BuildProfile<Platform.ANDROID>;
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.MANAGED) {
return await ensureApplicationIdIsDefinedForManagedProjectAsync(projectDir, exp, actor);
return await ensureApplicationIdIsDefinedForManagedProjectAsync({
graphqlClient,
projectDir,
projectId,
exp,
});
}
const gradleContext = await resolveGradleBuildContextAsync(projectDir, profile);
@@ -37,7 +50,12 @@ export async function getApplicationIdentifierAsync(
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const profile = buildProfile as BuildProfile<Platform.IOS>;
if (workflow === Workflow.MANAGED) {
return await ensureBundleIdentifierIsDefinedForManagedProjectAsync(projectDir, exp, actor);
return await ensureBundleIdentifierIsDefinedForManagedProjectAsync({
graphqlClient,
projectDir,
projectId,
exp,
});
}
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
@@ -0,0 +1,47 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { errors, readAndValidateBuildConfigAsync } from '@expo/steps';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
export async function validateCustomBuildConfigAsync(
projectDir: string,
profile: BuildProfile<Platform>
): Promise<void> {
if (!profile.config) {
return;
}
const relativeConfigPath = getCustomBuildConfigPath(profile.config);
const configPath = path.join(projectDir, relativeConfigPath);
if (!(await fs.pathExists(configPath))) {
throw new Error(
`Custom build configuration file ${chalk.bold(relativeConfigPath)} does not exist.`
);
}
try {
await readAndValidateBuildConfigAsync(configPath, { skipNamespacedFunctionsCheck: true });
} catch (err) {
if (err instanceof errors.BuildConfigYAMLError) {
throw new Error(
`Custom build configuration file ${chalk.bold(
relativeConfigPath
)} contains invalid YAML.\n\n${err.message}`
);
} else if (err instanceof errors.BuildConfigError) {
throw new Error(
`Custom build configuration file ${chalk.bold(
relativeConfigPath
)} contains invalid configuration. Please check the docs!\n${err.message}`
);
} else {
throw err;
}
}
}
export function getCustomBuildConfigPath(configFilename: string): string {
return path.join('.eas/build', configFilename);
}
@@ -2,8 +2,11 @@ import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import path from 'path';
import { instance, mock } from 'ts-mockito';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { jester, jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { promptAsync } from '../../../prompts';
import {
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
@@ -13,6 +16,7 @@ import {
jest.mock('fs');
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
beforeEach(async () => {
@@ -86,6 +90,7 @@ describe(getBundleIdentifierAsync, () => {
describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
describe('managed project + ios.bundleIdentifier missing in app config', () => {
it('throws an error if using app.config.js', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
vol.fromJSON(
{
'app.config.js': 'module.exports = { blah: {} };',
@@ -93,10 +98,23 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
'/app'
);
await expect(
ensureBundleIdentifierIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
ensureBundleIdentifierIsDefinedForManagedProjectAsync({
graphqlClient,
projectDir: '/app',
projectId: '1234',
exp: {} as any,
})
).rejects.toThrowError(/we can't update this file programmatically/);
});
it('prompts for the bundle identifier if using app.json', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
vol.fromJSON(
{
'app.json': '{ "expo": {} }',
@@ -109,11 +127,24 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
}));
await expect(
ensureBundleIdentifierIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
ensureBundleIdentifierIsDefinedForManagedProjectAsync({
graphqlClient,
projectDir: '/app',
projectId: '1234',
exp: {} as any,
})
).resolves.toBe('com.expo.notdominik');
expect(promptAsync).toHaveBeenCalled();
});
it('puts the bundle identifier in app.json', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
vol.fromJSON(
{
'app.json': '{ "expo": {} }',
@@ -126,7 +157,12 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
}));
await expect(
ensureBundleIdentifierIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
ensureBundleIdentifierIsDefinedForManagedProjectAsync({
graphqlClient,
projectDir: '/app',
projectId: '1234',
exp: {} as any,
})
).resolves.toBe('com.expo.notdominik');
const appJson = JSON.parse(await fs.readFile('/app/app.json', 'utf-8'));
expect(appJson).toMatchObject({
@@ -6,26 +6,37 @@ import chalk from 'chalk';
import fs from 'fs-extra';
import { readAppJson } from '../../build/utils/appJson';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import Log, { learnMore } from '../../log';
import { promptAsync } from '../../prompts';
import { Actor } from '../../user/User';
import { getProjectConfigDescription, getUsername } from '../projectUtils';
import { getOwnerAccountForProjectIdAsync, getProjectConfigDescription } from '../projectUtils';
import { resolveWorkflowAsync } from '../workflow';
export const INVALID_BUNDLE_IDENTIFIER_MESSAGE = `Invalid format of iOS bundle identifier. Only alphanumeric characters, '.' and '-' are allowed, and each '.' must be followed by a letter.`;
export async function ensureBundleIdentifierIsDefinedForManagedProjectAsync(
projectDir: string,
exp: ExpoConfig,
actor: Actor
): Promise<string> {
export async function ensureBundleIdentifierIsDefinedForManagedProjectAsync({
graphqlClient,
projectDir,
projectId,
exp,
}: {
graphqlClient: ExpoGraphqlClient;
projectDir: string;
projectId: string;
exp: ExpoConfig;
}): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
assert(workflow === Workflow.MANAGED, 'This function should be called only for managed projects');
try {
return await getBundleIdentifierAsync(projectDir, exp);
} catch {
return await configureBundleIdentifierAsync(projectDir, exp, actor);
return await configureBundleIdentifierAsync({
graphqlClient,
projectDir,
exp,
projectId,
});
}
}
@@ -100,11 +111,17 @@ export async function getBundleIdentifierAsync(
}
}
async function configureBundleIdentifierAsync(
projectDir: string,
exp: ExpoConfig,
actor: Actor
): Promise<string> {
async function configureBundleIdentifierAsync({
graphqlClient,
projectDir,
projectId,
exp,
}: {
graphqlClient: ExpoGraphqlClient;
projectDir: string;
projectId: string;
exp: ExpoConfig;
}): Promise<string> {
const paths = getConfigFilePaths(projectDir);
// we can't automatically update app.config.js
if (paths.dynamicConfigPath) {
@@ -122,7 +139,11 @@ async function configureBundleIdentifierAsync(
)}`
);
const suggestedBundleIdentifier = await getSuggestedBundleIdentifierAsync(exp, actor);
const suggestedBundleIdentifier = await getSuggestedBundleIdentifierAsync(
graphqlClient,
exp,
projectId
);
const { bundleIdentifier } = await promptAsync({
name: 'bundleIdentifier',
type: 'text',
@@ -169,8 +190,9 @@ export function isWildcardBundleIdentifier(bundleIdentifier: string): boolean {
}
async function getSuggestedBundleIdentifierAsync(
graphqlClient: ExpoGraphqlClient,
exp: ExpoConfig,
actor: Actor
projectId: string
): Promise<string | undefined> {
// Attempt to use the android package name first since it's convenient to have them aligned.
const maybeAndroidPackage = AndroidConfig.Package.getPackage(exp);
@@ -178,9 +200,9 @@ async function getSuggestedBundleIdentifierAsync(
return maybeAndroidPackage;
} else {
// the only callsite is heavily interactive
const username = getUsername(exp, actor);
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
// It's common to use dashes in your node project name, strip them from the suggested package name.
const possibleId = `com.${username}.${exp.slug}`.split('-').join('');
const possibleId = `com.${account.name}.${exp.slug}`.split('-').join('');
if (isBundleIdentifierValid(possibleId)) {
return possibleId;
}
+1 -1
View File
@@ -264,7 +264,7 @@ export function getApplePlatformFromSdkRoot(target: Target): ApplePlatform | nul
* Get Apple Platform from the Xcode TARGETED_DEVICE_FAMILY where possible.
*
* References:
* https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html
* https://developer-mdn.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html
* https://stackoverflow.com/questions/39677524/xcode-8-how-to-change-targeted-device-family#comment100316573_39677659
*
* @returns - Apple Platform when known, defaults to null when unknown
+268 -1
View File
@@ -1,21 +1,36 @@
import { ExpoConfig, Platform } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Platform as EASBuildJobPlatform, Workflow } from '@expo/eas-build-job';
import JsonFile from '@expo/json-file';
import assert from 'assert';
import chalk from 'chalk';
import crypto from 'crypto';
import fs from 'fs-extra';
import Joi from 'joi';
import mime from 'mime';
import nullthrows from 'nullthrows';
import path from 'path';
import promiseLimit from 'promise-limit';
import { selectBranchOnAppAsync } from '../branch/queries';
import { getDefaultBranchNameAsync } from '../branch/utils';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { AssetMetadataStatus, PartialManifestAsset } from '../graphql/generated';
import { PublishMutation } from '../graphql/mutations/PublishMutation';
import { PublishQuery } from '../graphql/queries/PublishQuery';
import Log from '../log';
import Log, { learnMore } from '../log';
import { RequestedPlatform, requestedPlatformDisplayNames } from '../platform';
import { promptAsync } from '../prompts';
import { getBranchNameFromChannelNameAsync } from '../update/getBranchNameFromChannelNameAsync';
import { formatUpdateMessage, truncateString as truncateUpdateMessage } from '../update/utils';
import { PresignedPost, uploadWithPresignedPostWithRetryAsync } from '../uploads';
import { expoCommandAsync, shouldUseVersionedExpoCLI } from '../utils/expoCli';
import chunk from '../utils/expodash/chunk';
import { truthy } from '../utils/expodash/filter';
import uniqBy from '../utils/expodash/uniqBy';
import { getVcsClient } from '../vcs';
import { resolveWorkflowAsync } from './workflow';
export type ExpoCLIExportPlatformFlag = Platform | 'all';
@@ -30,6 +45,8 @@ export type RawAsset = {
fileExtension?: string;
contentType: string;
path: string;
/** Original asset path derrived from asset map, or exported folder */
originalPath?: string;
};
type CollectedAssets = {
@@ -52,6 +69,16 @@ type UpdateInfoGroup = {
[key in Platform]: ManifestFragment;
};
// Partial copy of `@expo/dev-server` `BundleAssetWithFileHashes`
type AssetMap = Record<
string,
{
httpServerLocation: string;
name: string;
type: string;
}
>;
const fileMetadataJoi = Joi.object({
assets: Joi.array()
.required()
@@ -170,6 +197,7 @@ export async function buildBundlesAsync({
'--output-dir',
inputDir,
'--dump-sourcemap',
'--dump-assetmap',
'--platform',
platformFlag,
]);
@@ -182,6 +210,7 @@ export async function buildBundlesAsync({
'--experimental-bundle',
'--non-interactive',
'--dump-sourcemap',
'--dump-assetmap',
'--platform',
platformFlag,
]);
@@ -246,9 +275,48 @@ export function filterExportedPlatformsByFlag<T extends Partial<Record<Platform,
return { [platform]: record[platform] } as T;
}
/** Try to load the asset map for logging the names of assets published */
export async function loadAssetMapAsync(distRoot: string): Promise<AssetMap | null> {
const assetMapPath = path.join(distRoot, 'assetmap.json');
if (!(await fs.pathExists(assetMapPath))) {
return null;
}
const assetMap: AssetMap = JsonFile.read(path.join(distRoot, 'assetmap.json'));
// TODO: basic validation?
return assetMap;
}
// exposed for testing
export function getAssetHashFromPath(assetPath: string): string | null {
const [, hash] = assetPath.match(new RegExp(/assets\/([a-z0-9]+)$/, 'i')) ?? [];
return hash ?? null;
}
// exposed for testing
export function getOriginalPathFromAssetMap(
assetMap: AssetMap | null,
asset: { path: string; ext: string }
): string | null {
if (!assetMap) {
return null;
}
const assetHash = getAssetHashFromPath(asset.path);
const assetMapEntry = assetHash && assetMap[assetHash];
if (!assetMapEntry) {
return null;
}
const pathPrefix = assetMapEntry.httpServerLocation.substring('/assets'.length);
return `${pathPrefix}/${assetMapEntry.name}.${assetMapEntry.type}`;
}
/** Given a directory, load the metadata.json and collect the assets for each platform. */
export async function collectAssetsAsync(dir: string): Promise<CollectedAssets> {
const metadata = loadMetadata(dir);
const assetmap = await loadAssetMapAsync(dir);
const collectedAssets: CollectedAssets = {};
@@ -261,6 +329,7 @@ export async function collectAssetsAsync(dir: string): Promise<CollectedAssets>
},
assets: metadata.fileMetadata[platform].assets.map(asset => ({
fileExtension: asset.ext ? ensureLeadingPeriod(asset.ext) : undefined,
originalPath: getOriginalPathFromAssetMap(assetmap, asset) ?? undefined,
contentType: guessContentTypeFromExtension(asset.ext),
path: path.join(dir, asset.path),
})),
@@ -294,9 +363,17 @@ export async function filterOutAssetsThatAlreadyExistAsync(
}
type AssetUploadResult = {
/** All found assets within the exported folder per platform */
assetCount: number;
/** The uploaded JS bundles, per platform */
launchAssetCount: number;
/** All unique assets within the exported folder with platforms combined */
uniqueAssetCount: number;
/** All unique assets uploaded */
uniqueUploadedAssetCount: number;
/** All (non-launch) asset original paths, used for logging */
uniqueUploadedAssetPaths: string[];
/** The asset limit received from the server */
assetLimitPerUpdateGroup: number;
};
@@ -308,7 +385,9 @@ export async function uploadAssetsAsync(
): Promise<AssetUploadResult> {
let assets: RawAsset[] = [];
let platform: keyof CollectedAssets;
const launchAssets: RawAsset[] = [];
for (platform in assetsForUpdateInfoGroup) {
launchAssets.push(assetsForUpdateInfoGroup[platform]!.launchAsset);
assets = [
...assets,
assetsForUpdateInfoGroup[platform]!.launchAsset,
@@ -335,6 +414,7 @@ export async function uploadAssetsAsync(
updateSpinnerText?.(totalAssets, totalAssets);
let missingAssets = await filterOutAssetsThatAlreadyExistAsync(graphqlClient, uniqueAssets);
const uniqueUploadedAssetCount = missingAssets.length;
const uniqueUploadedAssetPaths = missingAssets.map(asset => asset.originalPath).filter(truthy);
const missingAssetChunks = chunk(missingAssets, 100);
const specifications: string[] = [];
@@ -372,8 +452,10 @@ export async function uploadAssetsAsync(
}
return {
assetCount: assets.length,
launchAssetCount: launchAssets.length,
uniqueAssetCount: uniqueAssets.length,
uniqueUploadedAssetCount,
uniqueUploadedAssetPaths,
assetLimitPerUpdateGroup,
};
}
@@ -385,3 +467,188 @@ export function isUploadedAssetCountAboveWarningThreshold(
const warningThreshold = Math.floor(assetLimitPerUpdateGroup * 0.75);
return uploadedAssetCount > warningThreshold;
}
export async function getBranchNameForCommandAsync({
graphqlClient,
projectId,
channelNameArg,
branchNameArg,
autoFlag,
nonInteractive,
paginatedQueryOptions,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
channelNameArg: string | undefined;
branchNameArg: string | undefined;
autoFlag: boolean;
nonInteractive: boolean;
paginatedQueryOptions: PaginatedQueryOptions;
}): Promise<string> {
if (channelNameArg && branchNameArg) {
throw new Error(
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto.'
);
}
if (channelNameArg) {
return await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelNameArg);
}
if (branchNameArg) {
return branchNameArg;
}
if (autoFlag) {
return await getDefaultBranchNameAsync();
} else if (nonInteractive) {
throw new Error('Must supply --channel, --branch or --auto when in non-interactive mode.');
} else {
let branchName: string;
try {
const branch = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: `Which branch would you like to roll back to embedded on?`,
displayTextForListItem: updateBranch => ({
title: `${updateBranch.name} ${chalk.grey(
`- current update: ${formatUpdateMessage(updateBranch.updates[0])}`
)}`,
}),
paginatedQueryOptions,
});
branchName = branch.name;
} catch {
// unable to select a branch (network error or no branches for project)
const { name } = await promptAsync({
type: 'text',
name: 'name',
message: 'No branches found. Provide a branch name:',
initial: await getDefaultBranchNameAsync(),
validate: value => (value ? true : 'Branch name may not be empty.'),
});
branchName = name;
}
assert(branchName, 'Branch name must be specified.');
return branchName;
}
}
export async function getUpdateMessageForCommandAsync({
updateMessageArg,
autoFlag,
nonInteractive,
jsonFlag,
}: {
updateMessageArg: string | undefined;
autoFlag: boolean;
nonInteractive: boolean;
jsonFlag: boolean;
}): Promise<string> {
let updateMessage = updateMessageArg;
if (!updateMessageArg && autoFlag) {
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
}
if (!updateMessage) {
if (nonInteractive) {
throw new Error('Must supply --message or use --auto when in non-interactive mode');
}
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
const { updateMessageLocal } = await promptAsync({
type: 'text',
name: 'updateMessageLocal',
message: `Provide an roll back message:`,
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
validate: (value: any) => (value ? true : validationMessage),
});
updateMessage = updateMessageLocal;
}
assert(updateMessage, 'Update message must be specified.');
const truncatedMessage = truncateUpdateMessage(updateMessage, 1024);
if (truncatedMessage !== updateMessage) {
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
}
return updateMessage;
}
export const defaultPublishPlatforms: Platform[] = ['android', 'ios'];
export function getRequestedPlatform(
platform: ExpoCLIExportPlatformFlag
): RequestedPlatform | null {
switch (platform) {
case 'android':
return RequestedPlatform.Android;
case 'ios':
return RequestedPlatform.Ios;
case 'web':
return null;
case 'all':
return RequestedPlatform.All;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
/** Get runtime versions grouped by platform. Runtime version is always `null` on web where the platform is always backwards compatible. */
export async function getRuntimeVersionObjectAsync(
exp: ExpoConfig,
platforms: Platform[],
projectDir: string
): Promise<{ platform: string; runtimeVersion: string }[]> {
for (const platform of platforms) {
if (platform === 'web') {
continue;
}
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
if (isPolicy) {
const isManaged =
(await resolveWorkflowAsync(projectDir, platform as EASBuildJobPlatform)) ===
Workflow.MANAGED;
if (!isManaged) {
throw new Error(
'Runtime version policies are only supported in the managed workflow. In the bare workflow, runtime version needs to be set manually.'
);
}
}
}
return [...new Set(platforms)].map(platform => {
if (platform === 'web') {
return { platform: 'web', runtimeVersion: 'UNVERSIONED' };
}
return {
platform,
runtimeVersion: nullthrows(
Updates.getRuntimeVersion(exp, platform),
`Unable to determine runtime version for ${
requestedPlatformDisplayNames[platform]
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
),
};
});
}
export function getRuntimeToPlatformMappingFromRuntimeVersions(
runtimeVersions: { platform: string; runtimeVersion: string }[]
): { runtimeVersion: string; platforms: string[] }[] {
const runtimeToPlatformMapping: { runtimeVersion: string; platforms: string[] }[] = [];
for (const runtime of runtimeVersions) {
const platforms = runtimeVersions
.filter(({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion)
.map(({ platform }) => platform);
if (!runtimeToPlatformMapping.find(item => item.runtimeVersion === runtime.runtimeVersion)) {
runtimeToPlatformMapping.push({ runtimeVersion: runtime.runtimeVersion, platforms });
}
}
return runtimeToPlatformMapping;
}
+33 -15
View File
@@ -317,30 +317,48 @@ async function handleBuildListSourceAsync(
}
function formatBuildChoice(build: BuildFragment, expiryDate: Date): prompts.Choice {
const { id, updatedAt, runtimeVersion, buildProfile, gitCommitHash, gitCommitMessage, channel } =
build;
const formatValue = (field?: string | null): string => (field ? chalk.bold(field) : 'Unknown');
const {
id,
updatedAt,
runtimeVersion,
buildProfile,
gitCommitHash,
gitCommitMessage,
channel,
message,
} = build;
const buildDate = new Date(updatedAt);
const splitCommitMessage = gitCommitMessage?.split('\n');
const formattedCommitData =
gitCommitHash && gitCommitMessage
? `${gitCommitHash.slice(0, 7)} "${chalk.bold(gitCommitMessage)}"`
: 'Unknown';
gitCommitHash && splitCommitMessage && splitCommitMessage.length > 0
? `${gitCommitHash.slice(0, 7)} "${chalk.bold(
splitCommitMessage[0] + (splitCommitMessage.length > 1 ? '…' : '')
)}"`
: null;
const title = `${chalk.bold(`ID:`)} ${id} (${chalk.bold(`${fromNow(buildDate)} ago`)})`;
const description = [
`\t${chalk.bold(`Profile:`)} ${formatValue(buildProfile)}`,
`\t${chalk.bold(`Channel:`)} ${formatValue(channel)}`,
`\t${chalk.bold(`Runtime version:`)} ${formatValue(runtimeVersion)}`,
`\t${chalk.bold(`Commit:`)} ${formattedCommitData}`,
].join('\n');
const descriptionItems: { name: string; value: string | null }[] = [
{ name: 'Profile', value: buildProfile ? chalk.bold(buildProfile) : null },
{ name: 'Channel', value: channel ? chalk.bold(channel) : null },
{ name: 'Runtime version', value: runtimeVersion ? chalk.bold(runtimeVersion) : null },
{ name: 'Commit', value: formattedCommitData },
{
name: 'Message',
value: message
? chalk.bold(message.length > 200 ? `${message.slice(0, 200)}...` : message)
: null,
},
];
const filteredDescriptionArray: string[] = descriptionItems
.filter(item => item.value)
.map(item => `${chalk.bold(item.name)}: ${item.value}`);
return {
title,
description,
description: filteredDescriptionArray.length > 0 ? filteredDescriptionArray.join('\n') : '',
value: build,
disabled: buildDate < expiryDate,
};
@@ -1,7 +1,7 @@
import { instance, mock } from 'ts-mockito';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { UpdateBranch, UpdateChannel } from '../../graphql/generated';
import { Runtime, UpdateBranch, UpdateChannel } from '../../graphql/generated';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import { getBranchNameFromChannelNameAsync } from '../getBranchNameFromChannelNameAsync';
@@ -121,8 +121,10 @@ function mockUpdateBranches(branchNames: string[]): UpdateBranch[] {
message: 'my message',
createdAt: '2022-12-07T02:24:43.487Z',
runtimeVersion: '1.0.0',
runtime: {} as Runtime, // Temporary fix to resolve type errors
platform: 'ios',
manifestFragment: '...',
isRollBackToEmbedded: false,
activityTimestamp: '2022-12-07T02:24:43.487Z',
branchId: '',
awaitingCodeSigningInfo: false,
+2 -2
View File
@@ -18,7 +18,7 @@ import { resolveWorkflowPerPlatformAsync } from '../project/workflow';
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from './android/UpdatesModule';
import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from './ios/UpdatesModule';
export const DEFAULT_MANAGED_RUNTIME_VERSION = { policy: 'appVersion' } as const;
export const DEFAULT_MANAGED_RUNTIME_VERSION = { policy: 'sdkVersion' } as const;
export const DEFAULT_BARE_RUNTIME_VERSION = '1.0.0' as const;
function getDefaultRuntimeVersion(workflow: Workflow): NonNullable<ExpoConfig['runtimeVersion']> {
@@ -273,7 +273,7 @@ export async function ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir: stri
}
try {
const easJsonAccessor = new EasJsonAccessor(projectDir);
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
+11
View File
@@ -48,6 +48,8 @@ export type FormattedUpdateGroupDescription = {
group: string;
platforms: string;
runtimeVersion: string;
codeSigningKey: string | undefined;
isRollBackToEmbedded: boolean;
};
export type FormattedBranchDescription = {
@@ -74,6 +76,8 @@ export function formatUpdateGroup(update: FormattedUpdateGroupDescription): stri
{ label: 'Platforms', value: update.platforms },
{ label: 'Runtime Version', value: update.runtimeVersion },
{ label: 'Message', value: update.message },
{ label: 'Code Signing Key', value: update.codeSigningKey ?? 'N/A' },
{ label: 'Is Roll Back to Embedded', value: update.isRollBackToEmbedded ? 'Yes' : 'No' },
{ label: 'Group ID', value: update.group },
]);
}
@@ -203,6 +207,7 @@ export function getUpdateGroupJsonInfo(updateGroups: UpdateFragment[]): UpdateJs
runtimeVersion: update.runtimeVersion,
platform: update.platform,
manifestPermalink: update.manifestPermalink,
isRollBackToEmbedded: update.isRollBackToEmbedded,
gitCommitHash: update.gitCommitHash,
}));
}
@@ -213,6 +218,8 @@ export function getUpdateGroupDescriptions(
return updateGroups.map(updateGroup => ({
message: formatUpdateMessage(updateGroup[0]),
runtimeVersion: updateGroup[0].runtimeVersion,
isRollBackToEmbedded: updateGroup[0].isRollBackToEmbedded,
codeSigningKey: updateGroup[0].codeSigningInfo?.keyid,
group: updateGroup[0].group,
platforms: formatPlatformForUpdateGroup(updateGroup),
}));
@@ -225,6 +232,8 @@ export function getUpdateGroupDescriptionsWithBranch(
branch: updateGroup[0].branch.name,
message: formatUpdateMessage(updateGroup[0]),
runtimeVersion: updateGroup[0].runtimeVersion,
isRollBackToEmbedded: updateGroup[0].isRollBackToEmbedded,
codeSigningKey: updateGroup[0].codeSigningInfo?.keyid,
group: updateGroup[0].group,
platforms: formatPlatformForUpdateGroup(updateGroup),
}));
@@ -241,6 +250,8 @@ export function getBranchDescription(branch: UpdateBranchFragment): FormattedBra
update: {
message: formatUpdateMessage(latestUpdate),
runtimeVersion: latestUpdate.runtimeVersion,
isRollBackToEmbedded: latestUpdate.isRollBackToEmbedded,
codeSigningKey: latestUpdate.codeSigningInfo?.keyid,
group: latestUpdate.group,
platforms: getPlatformsForGroup({
group: latestUpdate.group,
+15 -5
View File
@@ -7,15 +7,25 @@ export type Actor = NonNullable<CurrentUserQuery['meActor']>;
* This should be used whenever the "current user" needs to be displayed.
* The display name CANNOT be used as project owner.
*/
export function getActorDisplayName(user?: Actor): string {
switch (user?.__typename) {
export function getActorDisplayName(actor?: Actor): string {
switch (actor?.__typename) {
case 'User':
return user.username;
return actor.username;
case 'Robot':
return user.firstName ? `${user.firstName} (robot)` : 'robot';
return actor.firstName ? `${actor.firstName} (robot)` : 'robot';
case 'SSOUser':
return 'sso';
return actor.username ? `${actor.username} (sso user)` : 'sso user';
default:
return 'anonymous';
}
}
export function getActorUsername(actor?: Actor): string | null {
switch (actor?.__typename) {
case 'User':
case 'SSOUser':
return actor.username;
default:
return null;
}
}
@@ -1,17 +1,18 @@
-----BEGIN CERTIFICATE-----
MIICyzCCAbOgAwIBAgIJWVXVdVgS9u+DMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV
BAMTBFRlc3QwHhcNMjIwMjA0MDEzODU2WhcNMjMwMjA0MDEzODU2WjAPMQ0wCwYD
VQQDEwRUZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp1ILmaSl
1pUVFgt/nLtzoaGuBW4OPRFN5uU2rYPow3uy4NfULKiir4AWxGETcm4gFIOG3RIb
DJys6TAJJLnFEFnzpV+qaNeqaMqnXm4OBspx/WS2rSSGIwr7raKy4yrpPYJB1A2D
5fZ6oFoWROyjbHJvPddu0PqbbEWSM3PzdZYOKfF13ofzFOPlsTnlo12QWgksxrcT
wgeX7cjHsXaUc+I8U9DRR+9SgKhejWdFNPGcMkEc5BIXwtkTau+8+DwQzXz775Wl
hf0eJwfnNQeleblbLQIp8i+RmfIjPSt8W/iTSN64iBROpOvnOeJ9gKX922QDZoqW
z9o4RTNCPfyYBQIDAQABoyowKDAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAww
CgYIKwYBBQUHAwMwDQYJKoZIhvcNAQELBQADggEBAHTyG2f/CReXVrS3zZjpgnje
Vn2/XYevnaHNWYIxjlUVGaASE/k6qWkM832f0gaZwJYXsll2YxJa5Jcdqa1yq8QA
Ay5YBq8Vv6K6iGTeZ5wTQc3xNys7zcS95eNq7yhDDjbLDbSDdZ8T/1hZqiha2DG5
itLGfBH+HywW2G9njLgIzNcmKx3d76y3jlVXqug1/AVFK9in+r/0t0TYlk+opqZb
H7cjJSefHusnq2vvvR6gbkZzyzSYP5FMlj/RjRCSJ+VHNwCn3fS+KDlJgVOk0Cxc
7n52UGazeFwH3mem89DN+Bhw/uyU0IcIaDLykb6PsTPy2tjHsLpK5kpvfrp2m0o=
-----END CERTIFICATE-----
MIICzTCCAbWgAwIBAgIJWEL03IR99LhMMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV
BAMTBHRlc3QwIBcNMjIwNDEyMTcyODIzWhgPMjEyMjA0MTIxNzI4MjNaMA8xDTAL
BgNVBAMTBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBS7bz
ELiUKOB8QNh0e0Oit7j7vk7ZICdyvBv+AB7PYbgImTvJSZ3CJTfmDYOt57ENd+ke
V/RnD9b8u55ijroJBoR8QASVVsEYmgA1cv9owUYkIDxsAjxV9JZp3UgNeRmQ7IKI
1J7myNnUQ3RIclSZkJO7ViHGSwTMXBanaO/OwjY6SM6Eca89XMcpOc/0xCBidFkO
qn/jnIXk3uPAoLjPMQcX535+AUayevwSr6YqPV0zOGOYJy26n401eH3mu/UZveWy
6qp7VumcBmz8AkvTJDgfSv04Pk3gpi4d8q4ABrtM46qWlw6UMMh5R6/i6Ou/8526
xThOm3qEpEGXyZ2VAgMBAAGjKjAoMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8E
DDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEATeE0FXXheYjw77pjy6sZ
b6POytJdYLawMt7Chn11zP1TLx0LA1R01uH/ld0tVQQyt1XqG9AE3as1VCUYklPA
zmwFSOxcSMiAXobEa5NcJ4S490YTjYmHI8fkz6TS+eeLkIr7eZMbpe6ck9V2d1e3
0xHshYRc3NceNOKB018QYxbdxuTbV3a6f4yTlCSxKuyYfA5KDMoXaPAKE7Cwddrz
1dbQpSI6djWknFjhTh7Bu/zpGWEx7wXTZlx9nIhKrinVowvyUkTaaCfuzdCmZOzS
AJsADnJN0LEKDI4q0kDwl1v6U4cJ5Ru7VQv3xho03LPEA/fFKHK6OITDznspiXRs
IA==
-----END CERTIFICATE-----
@@ -1,27 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAp1ILmaSl1pUVFgt/nLtzoaGuBW4OPRFN5uU2rYPow3uy4NfU
LKiir4AWxGETcm4gFIOG3RIbDJys6TAJJLnFEFnzpV+qaNeqaMqnXm4OBspx/WS2
rSSGIwr7raKy4yrpPYJB1A2D5fZ6oFoWROyjbHJvPddu0PqbbEWSM3PzdZYOKfF1
3ofzFOPlsTnlo12QWgksxrcTwgeX7cjHsXaUc+I8U9DRR+9SgKhejWdFNPGcMkEc
5BIXwtkTau+8+DwQzXz775Wlhf0eJwfnNQeleblbLQIp8i+RmfIjPSt8W/iTSN64
iBROpOvnOeJ9gKX922QDZoqWz9o4RTNCPfyYBQIDAQABAoIBAEKMXVTEqbkJHpPg
Cud5nuoAdkhul3cudL+LFw44UtG9V04aSadhgyMuXN/KhIOUzWmbTn6K/vsrLZKp
qllTEdAJFuEFha+hZ4O6ZosmVqnYxzGzZvzCdB9n9OYAugmkPZRbRHdk0LscJ3Wz
nvvW6sDtWtVL5CV2J28O1LFmQsTXPtEE5aQ5KX3Ix0OW1HBdEMOXRDFFDXhf7AKz
nlvl3WRsXLyHYBfKs0pQHy8Gi+zl2OgKZDc9elMdU0Mx0jTJUUDtPSgNAsmdKO26
qmkhbGd/kM1+6h+zngY3m48yD9elDxR3WUIYE2x5tT9fv3T6/xn1QfMz/gtToplx
qY1DegECgYEA3bGUq91QMIH5rxrr2BBQpniXOTtOPAdvRcRh0/EsYhHcuL+uJEtG
W9o9MqrREhXMMeD9Y8RorL+qefJjGq+GRwnPHlDjr6NkGTZfDKUn4IFH1xMbspET
Nen5BsY5inUrQ/LSg1+HiVsSDyIJzGVcGMTOh9qC53M/zIGdiEwHgUECgYEAwTZ3
AVieag5ETYahGdverxCRKlJ0MFJq3VKEdt6EcZHfeY1gmi17vGZ39q6djmfTT9Il
QVP4/mxU2/mg/UgRC+rwiAZs3qlhOrtkrGd9qPOIeZ8PJkoNlvhM2WC2rGjTfXsb
2IKtDEVXetwGgm3SXqpjycI5Xj6/9VlHAmCc4cUCgYAv9dT2AWDxvYyopyhSi+UG
vpvok73vGqSl8UBAu7IgXUDk7wLbczV7dZE7vtyQDwsn10a6KKmEhcp5q0hpY4On
JqYaJuG7A5wKIEsbzzb7SLyj+MxLKzt+tGldX9De9U4w2v1T0nzd6EfV4kVAZMUx
zpHnrgwXykUJFxlffSM6gQKBgQCMxKXHsU0Zb/OLmD7fnDWNzsA02YYVfralMW2Z
PV25cNIkuUBclC7GgNF+RJI+Ip7uVOkXw5pxo3PgIOuOHWduC2nbcPL49ucD52vd
wDjpUyVnlt9uwh1MlPNInRH6YxVTItKS2AJEInEt7ghAFstidTnm0T8Czy0EEFuP
+9vREQKBgQDIQxJ5oPIOEibJqn34z6qm5EmUyme/evvHxtQTaVuQi5CrzD5+qw1C
jOTHmlJQQsb8b1g9wk7n+JbqWYhkRiAE6dwqvp/zDz1VfR90WbAPYQ8ZiXZ07D2P
8P6ZXR3ZyDchuT6oUBXz5WtHUWOzXRJgPbXnFhrkIU8PkZYiPs/4Bw==
-----END RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAwUu28xC4lCjgfEDYdHtDore4+75O2SAncrwb/gAez2G4CJk7
yUmdwiU35g2DreexDXfpHlf0Zw/W/LueYo66CQaEfEAElVbBGJoANXL/aMFGJCA8
bAI8VfSWad1IDXkZkOyCiNSe5sjZ1EN0SHJUmZCTu1YhxksEzFwWp2jvzsI2OkjO
hHGvPVzHKTnP9MQgYnRZDqp/45yF5N7jwKC4zzEHF+d+fgFGsnr8Eq+mKj1dMzhj
mCctup+NNXh95rv1Gb3lsuqqe1bpnAZs/AJL0yQ4H0r9OD5N4KYuHfKuAAa7TOOq
lpcOlDDIeUev4ujrv/OdusU4Tpt6hKRBl8mdlQIDAQABAoIBAEVijWCA/xDH/5T6
nfhqCuRM+Mz9CkasYRyxY4bwuh6NIEeN4cUmdMetHnypGzyAr7B6+6ZVwjiAmhaM
rpUIUMVOnp4PSNXml1fiZ/LHveD3h9sN60KGJuxf6OJFeUjE0KWSEGVXlVaYgIDO
Wd5rk+yv1ifoCiWo5icJY2RiqbpYouveqb/InkQU+irZEwCviNvN1ZyJKqquNgcG
FaY7iVd3WlHRealF1OfKHCBvSah9H5UBFWRBWUds2/xNzpCTAAGsmXdG6O9wPuEQ
IhJqnSkTiQw49vyOH708VQnBfz/MILV7uTYqQ0Tep3+xkFFy4BSUIb6fE2zJegs5
80MxIh0CgYEA4yf8cBcYidTAeoDbl2UXasDHAmNKeR4IxKAR3AJOiETIKDv4vAMP
oBvHnUnnUQa5LhihrVXCbVwGqwATDka73K8sW/TkCE7OmuLDpZYvU/XQRWI74rka
A/+nbD26NlwnM/a8mJNhbCVns/+PlLq1rK/5WlINcRNVk8PZ1UpG+nsCgYEA2dcM
1TBZs1iqa+9AQ6nco5MWbsp3roq3sM06UneK0EGffWXPcmQkBZI9Jg+p5P2o/H13
cYvkyKGmnL8qRZViXWEa/NI60kphQmFNGr+J/3PlKCOqZhlyqGhCFN9KLumJL17P
XUuzVAa14UsfIFCiv7R33D02o/xw89u5TN+3ky8CgYEAhmkzszPHXk9YcWRsC2JS
/+UAHQaZm59M+uPojXGD/JgOg9gwrzd0eH3XmNeRG13KF1+V01YKjOFGRMrve6QZ
J1Uz/1mh0NSo5fdGY2XBrYGnclbVLqvum+0bqS3BUMcon1PhdrrGi9J6UYTn9c6h
D4S1HF+u+njBS4U5ET85Tp8CgYEAno0bVf+/Cf467BxTFeyIHrZr2W+b9HoagKCf
Fm7TpghdYRO9DXE9lqB9yToVWgoV+NAJI6fCeRTPA79PsR2tXnHTBris/2oLqBjR
2eoXMsrTu4dZ+r4C6fgYQMDUaZiotMW5ABqdB0drEfNvUEHgcs+TfcVAA9M1EwiV
shIStxkCgYBnxccKAfXywLjhVUCa50TsCrL8ToQ7WmWjU1LqN2iO/htFoxeIx4xP
N7YauagBhyuypcq7SV3bKsdxlCzKi0cMNEonuRJb1Ejo3iU+4F7rzqsltGTjLtpB
pgp5HP8sALzFZlWWif6W1TjzNJ87MIphr/VPcitRc9n4hWcbmxDvZA==
-----END RSA PRIVATE KEY-----
Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff. Show more